Contents

golang gotchas

Go is a small language…

1
2
3
4
5
6
7
8
+----------+---------------+
| LANGUAGE | NUM KEYWORDS  |
+----------+---------------+
| golang   |            25 |
| C        |            32 |
| python   |            33 |
| java     |            50 |
+----------+---------------+

… it has only a handful of keywords, and a small set of language features. This makes it incredibly easy to learn, but it’s not without hiccups. The following is a list of gotchas to be aware of…

variables in a for loop

Due to the lexical scope rules in golang, there are some for loops where you might end up doing work on the same variable multiple times. See this code block:

1
2
3
4
5
6
7
8
9
var printers []func()
strings := []string{"a", "b", "c"}
for _, str := range strings {
	printers = append(printers, func() { print(str) })
}

for _, print := range printers {
	print()
}

Runnable example: https://play.golang.org/p/1Us0qXCp0U

The output is:

1
2
3
c
c
c

This is due to how the variables in the foor loop are initialised, the str variable is a memory location that each of the values in strings are copied into. That means that when you get to calling print() func, all the printers use the reference to the same value.

print(str) inside the anonymous function is executed after the last iteration of the for loop and so uses the value of the variable from the last iteration!

This can be fixed by adding a variable assignment inside the loop:

1
2
3
4
for _, str := range strings {
    str := str
    printers = append(printers, func() { print(str) })
}

You can use the same name, or a different one.

Now str used in print(str) is the one that’s initialised to the value of the for loop at that moment, and not at the end of the iterations.

don’t use goroutines just because you can.

One of the most powerful features of go is it’s built in concurreny support. The concept of gorountines that are based on coroutines and are mutliplexed onto threads by the go runtime. You can easily run thousands of goroutines in a program without a serious cost. goroutines are incredibly lightweight, they take about 2KB heap memory and more is dynamically provisioined by the go runtime when needed (comparison: threads take about 1MB).

This significantly reduces the activation energy required to write an algorithm that exploits concurrency, but should you? The answer is not always yes.

benchmarking to the rescue…

Micro benchmarking is powerful, but with great power comes great responsibility.

Benchmarks are part of the golang testing package and allow you to find out the running time of a function.

Benchmarks take the format func BenchmarkHelloWorld(b *testing.B), b has a field N (b.N) that is automatically adjusted by the go test package until it deeps that the function has run for long enough to be timed reliably. The benchmark:

1
2
3
4
5
func BenchmarkHelloWorld(b *testing.B) {
    for i := 0; i < b.N; i++ {
        fmt.Sprintf("hello world")
    }
}

may have the output:

1
BenchmarkHelloWorld    1000000    182 ns/op

Benchmarks can help to reliably test your assumptions around which algorithm implementation is faster. So you can use them to test if your concurrent implementation really is faster.

Note of Warning / the responsibility bit: Like with any testing, benchmarks are only as good as the test that they are executing. Make sure you benchmark with real production workloads and sample data that is as close as possible to the data that is going to be processed.

think carefully about closing channels.

Channels are one of the best and most versatile primitives in go, that is until you have writtin and benchmarked your concurrent algorithm and you get an error like: panic: send on closed channel!

While you do not need to close a channel, as the go gc will clean up any channel that is no longer used, irrelevant of it’s open / closed status, sometimes it’s a useful control mechanism. Make sure that you have clearly defined responsibilties around who owns that channel and who is responsible for closing it. Plus what the effects of closing it are.

Rule of thumb: let the sender close the channel.

This logically makes sense as it means that when all the data has been sent, close the channel to indicate that no more data will be sent in that channel. But, reading from a closed channel yeilds the channel type’s default value. So given the channel done := make(chan bool) a read <-done will yeild false. This becomes a problem in situations where you are merging or collating the results from multiple channels. Here’s an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// items := []A
// otherItems := []B

// createItemsChan := make(chan AResponse)
// logItemsChan := make(chan BResponse)

go createItems(items, createItemsChan)
go logItems(items, logItemsChan)

for i := 0; i < len(items); i++ {
    select {
        case createResponse := <- createItemsChan:
            log.Info(createResponse)
        case logResponse := <- logItemsChan:
            log.Info(logResponse)
    }
}

If the senders now control when the channels should be closed then this is now a race and a memory leak. If go createItems finishes before go logItems and closes it’s channel, then the select statment will continue to always read the default value from the createItemsChan (nil for the struct type). The go rountine performing logItems will be blocked and cause a memory leak, it will be waiting for ever trying to send on logItemsChan.

deferred func calls

The defer statement pushes a function call onto a list, defer calls are executed in a LIFO manner. But beware…

“A deferred function’s arguments are evaluated when the defer statement is evaluated.” - https://blog.golang.org/defer-panic-and-recover

The following code evaluates a at the point at which fmt.Println(a) is called…

1
2
3
a := 0
defer fmt.Println(a)
a++

output : 0

The fix - create an anonymous function, and use a inside it. Now the anonymous function call is evaulated when the defer statement is called, instead of the call to fmt.Println(a):

1
2
3
4
5
a := 0
defer func(int x) { 
        fmt.Println(x) 
}(a)
a++

output : 1

Runnable example: https://play.golang.org/p/V9z8eOQKSi

wrap up

There are so few concepts in golang, and it is such a small language it’s worth taking the time to learn the core concepts. Knowing how each of the language features works will save you from the most common gotchas and won’t take a large time investment.