Lately I’ve been doing more work in the Go programming language. Today I thought I would share three “gotchas” that caught me off guard, or otherwise produced results that I would not have expected in my work with Go.
1. The range clause
The range clause is very convenient. It allows you to iterate over a slice or map with two variables which represent the index and the value of each item. For example:
However, something notable is happening under the hood. Let’s see a more complex example:
In this example we are doing a few things.
- We are creating a slice of
Foostructs called list.
- We are defining a second slice of pointers to Foo structs called list2.
- We iterate through each struct in list in order to assign its pointer to the corresponding index in list2.
So therefore you might expect the output of the above code to be the following:
However, this is not what is happening. Let’s take a look at the output of this code:
The first line is as expected. These are the structs we initially created in list, but the second line is unexpected. It looks like we are printing out a pointer to the last struct in the list three times. But why is this happening ?
The culprit is the range clause.
Here’s the problem: Go uses a copy of the value instead of the value itself within a range clause. So when we take the pointer of
value, we’re actually taking the pointer of a copy of the value. This copy gets reused throughout the range clause, which leaves our list2 slice full of three references to the same pointer (the copy pointer).
According to the Go reference manual, “The iteration values are assigned to the respective iteration variables as in an assignment statement.” So effectively you can imagine the above range clause to be the same as writing:
In order to produce the output we expect, we should use the index to take a pointer to the actual value, instead of the copy.
2. The append built-in function
Slices are primitive types in Go. In other languages you might reach for an Array where in Go you would reach for a Slice. Here’s an example of how we might add a value to the end of an integer slice.
On the surface it seems to be similar to a
push() array method, but slices are not quite arrays, and the built-in append function surprised me with its behavior under the hood. Have a look at the example below:
In this example we define a slice
a of bytes with an initial value of
["foo"]. Next we append another slice
["bar"] to our initial slice
a, and again we append another slice
["baz"] to our initial slice
a. The output of the above snippet is:
Whaaat? Shouldn’t it be
foo foobar foobaz ?
In order to understand what’s going on here we have to understand what a slice really is. A slice is a descriptor which consists of three components:
- A pointer to an underlying array - that is, one allocated by Go which you don’t have direct access to.
- The capacity of said underlying array.
- The effective length of the slice.
So what’s really happening ? Go will reuse the same underlying array in
append() if it can do so without resizing the underlying array. So all three of these structs are referencing the exact same array in memory. The only practical difference is their length value which in the case of
a is 3, and in the case of
c is 6.
Keep in mind, Go will only reuse the same underlying array if the length of the newly created slice is less than or equal to the capacity of the initial slice.
It’s important to understand what is happening under the hood when using some of the built-in slice functions. For more info, Robe Pike wrote a very helpful blog post which goes into a lot of useful details around slices.
3. Variable shadowing
Check this out:
This is somewhat of a contrived example, so bear with me because I think it illustrates the point well. The run down of this snippet is as follows:
- We create a slice of strings,
- We enter a for loop.
- The for loop calls a function
repeat()which returns a new slice, and an error.
- We break out of the for loop, and we print the value of
You might expect the output here to be:
but in fact, it is:
Because we use the shorthand variable declaration operator, we are actually redeclaring the
list variable inside of the scope of the for loop. This is awkward, because the second variable
err is a new variable which we want to declare. We can fix this by changing the first few lines within the for block to:
One way to catch these problems early is by taking advantage of the
go vet tool. It has an option
-shadow which can help to detect issues with variable shadowing.
There is a good blog post called The Golang Beartrap that talks more in detail about variable shadowing.
Go is a great language, and once you understand its quirks it can be a real pleasure to work in. I really appreciate the fantastic tooling, and readability of the language. Good luck, and happy gophering!