Learning Go (Day 1)

I've started learning the Go Programming Language! 🎉

I'm teaching myself the language by reading the digital version of Learning Go from my Safari Books Online subscription. So far, I've covered:

  • Some basic sub-commands of the go executable:
    CommandPurpose
    go buildCompiles your code.
    go fmtFormats your code.
    go vetReports both syntactic and non-syntactic mistakes in your code.
    go run"Runs" your code (By quickly compiling it behind the scenes 🤫).
  • Primitive (erm... "Predeclared") data types:
    Data TypeDescription
    booltrue or false
    *int* familyIntegers.
    float* familyFloating-point numbers.
    complex* familyComplex numbers.
    byteAlias for uint8. Technically equivalent to unsigned char in C/C++.
    runeConceptually similar to char in C/C++, but it's actually an alias for int32 because it supports Unicode. Therefore... it's technically nothing like a char in C/C++! 😅
    stringSimilar to an immutable string in languages such as Python. It's implemented as a sequence of bytes; but, if you need to process the individual characters in the string, then it's better to think of it as an iterable of runes. It's weird.
    [*]* (Array)A fixed-length sequence of a given data type. The elements of an Array are mutable.
    []* (Slice)A variable-length sequence of a given data type... sorta. More accurately, there is an append(..) function that can create a new Slice in a memory-efficient manner. The elements of a Slice are mutable.
    map[*]* (Map)A key-value mapping, similar to something like dict in Python.
  • struct (which allows you to define composite data types)
  • Pointers (which are nerfed compared to pointers in C/C++ for language safety reasons. Pointer arithmetic is frowned upon, and therefore it is relegated to the unsafe package.)
  • Slice expressions for deriving views of Slices and Arrays, such as a[1:3].
  • Control structures:
    • if statements
    • for loops
    • switch statements
  • Functions
  • defer statements.
  • The basic mechanics of the Garbage Collector.

I'm not entirely sold on the language yet, but I certainly see some of the benefits that it has over C/C++. So, here are some initial thoughts:

Heaps of Joy!

Let's start off with a positive! Due to the restrictions on Pointers and Arrays, it's unnecessary for Programmers to actively distinguish between stack and heap allocations! Instead, the compiler will generally prefer stack allocations, but it will use heap allocations when the situation calls for it. This makes it impossible (?) for a function to return a pointer to a variable that was allocated on the stack, which is a common mistake in C/C++.

(Okay, I'm sure you could still do it if you tried really hard and believed in yourself. But, you'd probably need to abuse the unsafe package to pull this off.)

Hot Garbage

But wait -- there's more!

Developers are no longer responsible for freeing the heap allocations. Instead, Go's Garbage Collector will automatically clean up any heap allocations that are no longer accessible from the call stack. This eliminates (?) another class of common mistakes in C/C++!

(Again: I suspect there are ways to sabotage Go's garbage collection if you really want to punish yourself.)

var vs :=

Okay, we've had our fun. Let's discuss a few items that I find a little more questionable.

Frankly, I don't really understand why := seems to be the preferred way to initialize variables with a value. The := operator is just one typo away from the assignment (=) operator; therefore, it's very easy to accidentally shadow a variable when you intended to assign it a new value. Oops!

func main() {
	var x = 0
	for i := 0; i < 10; i++ {
		// Oops -- this shadows `x` from the outer scope,
		// making this line equivalent to `x := 0 + i`
		x := x + i

		// Oops -- `x` will always be equal to `i`
		fmt.Printf("The current value of `x` is: %d\n", x)
	}

	// Oops -- `x` is still equal to `0`!
	fmt.Printf("The final value of `x` is: %d\n", x)
}

Granted, var is able to shadow variables as well, but at least you're making a conscious decision to do so in that case. Is 3 extra keystrokes really that bad? 🤷‍♂️

++ and -- are statements, not expressions

Statements such as the following are illegal in Go:

doSomething(x++)

This is because x++ is a statement and not an expression; therefore, you can't embed it into another statement. Instead, you would need to write this snippet as:

x++
doSomething(x)

Of course, this also means that the prefix form of the operator is pointless now, and therefore it has been removed from the language:

++x // ERROR: This is illegal!

Changing the ++ and -- operators into statements eliminates a lot of ambiguity from the language. Still, it's going to take me a while to get used to this! 😵‍💫

Slices expose their implementation details

Pop Quiz! What does the following code print?

func main() {
	// Create a slice with length 0 and capacity 4
	var x = make([]int, 0, 4)
	y := append(x, 1)
	z := append(x, 2)
	fmt.Println(y, z)
}

That's right! It prints:

[2] [2]

If you understand the internals of Slices, then this behavior makes perfect sense. Under the hood, y and z both point to the same block of memory, so the creation of z inadvertently overwrites y.

I imagine that this behavior is probably surprising to anyone who hasn't programmed in a lower-level language before. Still, it seems necessary from an optimization standpoint.

Variables scoped to Conditionals

Go provides a shorthand syntax for defining variables that are scoped to an entire conditional. So, instead of doing something like this:

func main() {
	var word = "three"

	// Explicitly create a new block scope
	{
		// `number` and `present` vanish at the end of the block
		var number, present = wordToNumber[word]

		if present {
			fmt.Printf("The word \"%s\" represents the number %d\n", word, number)
		} else {
			fmt.Printf("\"%s\" must not be a number!\n", word)
		}
	}
}

You can just do this:

func main() {
	var word = "three"

	// `number` and `present` vanish at the end of the conditional
	if number, present := wordToNumber[word]; present {
		fmt.Printf("The word \"%s\" represents the number %d\n", word, number)
	} else {
		fmt.Printf("\"%s\" must not be a number!\n", word)
	}
}

Fancy! 🧐

However, conditionals can only be prefaced with a "simple" statement. I'm not sure why, but := is considered "simple", whereas var is not. So, if you utilize this language feature, then you must use the := statement. Boo!

Return values must be assigned to variables... sometimes

Sometimes, Go will report an error if you forget to assign a return value to a variable. For example:

func main() {
	var z = []int{1, 2, 3}
	append(z, 4, 5) // ERROR!
	fmt.Println(z)
}

This seems like a great way to detect and prevent pointless calls to functions! Unfortunately, there does not appear to be any way to introduce this behavior into your own functions. Darn!

Multiple Return Values

Functions in Go can return multiple values at once. For example:

func swap(x, y int) (int, int) {
  return y, x
}

In Python, this function would return a Tuple with 2 elements. In Go, these are 2 distinct return values, and therefore the caller must assign each one to its own variable.

Multiple Return Values... sometimes

The [] operator returns either one or two values depending upon how it is used:

func main() {
	var quuz = map[string]int{
		"foo": 1,
		"bar": 2,
	}

	// Wow! It can return one OR two values!
	var x = quuz["foo"]
	var y, present = quuz["bar"]

	fmt.Println(x, y, present)
}

Once again, it seems like it would be useful to introduce a similar capability into your own functions; but alas, there does not appear to be a way to do this. 😭

Named Return Values

Return values in Go can be named... sorta.

It seems like this is really just a way to save a few keystrokes when creating variables. But, unlike regular variables, the compiler never actually checks if you use them. So, code such as the following is valid:

// The compiler is happy even though `foo` and `bar` are unused variables.
func quuz() (foo int, bar int) {
	return 1, 2
}

Even weirder, you aren't forced to use the variables for their intended purpose. So, the following code is also valid:

func quuz() (foo int, bar int) {
	foo, bar = 3, 5

	// `bar` is being returned in place of `foo`, and vice versa
	return bar, foo
}

Error Handling

Go does not support the try/catch/... syntax that is common in many languages. Instead, errors are just another return value. For instance:

func divide(x, y int) (int, error) {
  if y == 0 {
    return 0, errors.New("Cannot divide by zero!")
  }

  return x / y, nil
}

I'm not really sure how I feel about this. It basically forces the caller to handle every error explicitly. But, if you've done much programming, then you know that errors frequently need to be handled further up the call stack. So, why not just make this behavior the default? 🤷‍♂️

Unused Variables are Errors, not Warnings

Initially, I was a little annoyed that Unused Variables are treated as Errors instead of Warnings. This can make it difficult to implement your functions incrementally without continually commenting out sections of the code. 😤

But... then I saw a function like the following:

func getFile(name string) (*os.File, func(), error) {
	file, err := os.Open(name)
	if err != nil {
		return nil, nil, err
	}

	cleanup := func() {
		file.Close()
	}

	return file, cleanup, nil
}

Notice that getFile(..) returns a cleanup() function that encapsulates the cleanup for the file resource. Why is this useful? Well, caller will need to assign this to a variable, which forces the caller to use the variable. So, this effectively prevents (!) the developer from forgetting to invoke the cleanup() function. Woohoo! 🥳

(Okay, fine. Once again, you could avoid invoking the cleanup() function if you tried really hard. For example, you could assign the cleanup() function to _, thereby ignoring it. But... why?)

Monadic Contagion

Okay -- I think that's the term for this phenomenon, but I can't remember for sure. Anyway:

If you've ever written an async function in Javascript, then you've probably noticed that almost every function was suddenly forced to be async. So, once this aspect was introduced into a codebase, it had a tendency to "contaminate" a large portion of the codebase.

I'm concerned that a similar phenomenon is present in Go, but it will manifest as functions that have more and more return values. I mean, the getFile(..) function we looked at a moment ago returned:

  • A file resource (Arguably, the actual return value).
  • A cleanup function.
  • An error.

Is this list going to keep growing as we introduce more aspects into the code? 🤔

Do I prefer defer? I'm not sure!

defer statements are an interesting replacement for finally blocks. I just wish there was a way to tie their execution to a block scope rather than a function scope. For example, consider the following:

func main() {
	// Pretend this slice has several hundred elements
	var fileNames = []string {"foo.txt", "bar.txt", ...}

	for _, name := range fileNames {
		fin, err := os.Open(name)
		if err != nil {
			log.Fatal(err)
		}
		defer func(){
			fin.Close()
		}()

		// Do stuff with `fin`
		...

		// Erm... `fin` will remain open even though
		// we're done with it.
	}

	// The deferred functions will finally be called
	// here, which will close all of the opened files.
}

I have a hunch that this behavior has been the cause of a few "resource leaks" over the years! Fortunately, you can work around it by extracting the body of the for loop into a separate function.

In Conclusion...

Go certainly seems to have a lot of quirks so far! Then again, I've probably just grown accustomed to the quirks in the other languages that I use, so I don't even notice them anymore. Besides: a handful of these quirks are necessary for Go to have a Garbage Collector, so I guess I can't complain too much! 🎉

Previous Post

End of an Era