Introduction

How well do you know the Go programming language? Have you written something in Go that does not quite does what you want it to do? Then you may find this article interesting.

We explore some subtle ways one can introduce a bug in a Go program. The criteria is that the compiler gives no warning whatsoever and that the fault can be caused by either a typo or some detail that is easy to forget about.

1. Integer sizes may vary

The built-in types int and uint have sizes that depend on the architecture compiled for. So on 32-bit machines it is possible that int is 32-bits wide while on 64-bit machines it will be 64 bit.

The consequence of this is that programs that runs fine on 64-bit machines could behave differently when integers are suddenly 32 bits wide because some number is being truncated or overflowing.

Another thing that had me pulling my hair one time is that when you have foreign interface that expects integers to be some specific size, funny stuff can happen. For example, one time I was trying to render a sphere with OpenGL by using an []int slice to specify 32-bit indices and produced this beautiful train wreck:

Suggestions

Think about what range your values will be and choose the right type.

2. Type assertions of value types vs. pointer types

When performing a type assertion on an interface to get to the underlying type, keep in mind that a pointer to a type is not the same as the type itself. This can cause your program to panic because the type assertion failed, but it can also introduce a subtle bug if you are using the v, ok := iface.(MyType) form where the branch that is should be taken when ok is true will never be hit.

For example, consider the following function, FindThing , with an error, NotFoundError , that is returned if some item of type Thing can not be found in e.g. a database:

type NotFoundError struct { ID string } func ( err NotFoundError ) Error () string { return fmt . Sprintf ( "thing with ID %q was not found" , err . ID ) } func FindThing ( id string ) ( * Thing , error ) { // ... return nil , NotFoundError { id } }

If the calling routine would like to know whether the NotFoundError was returned as opposed to something else, it can perform a type assertion like so:

if _ , ok := err .( * NotFoundError ); ok { // ... }

Can you spot the bug in the code above? It has to do with NotFoundError being returned from FindThing while we are checking for *NotFoundError which will never work because they are not the same type.

Suggestions

Always use pointer receivers for methods. This way, the type can only be behind an interface it implements if it is a pointer. So asserting without a pointer like v, ok := t.(NotFoundError) becomes impossible, the compiler will catch this error for you and force you to use v, ok := t.(*NotFoundError) . Here’s an example of the error that warns you:

./test.go:22:17: impossible type assertion: NotFoundError does not implement error (Error method has pointer receiver)

You can also reduce the risk of making this mistake by creating a new function that checks whether an interface is some type. This also comes with the advantage that it becomes possible to keep the definition of your interface implementation private in your package. Go’s standard library applies this pattern in some cases, like with the os.IsExist(error) and os.IsPermission(error) of functions.

3. Implementing an interface

The following continues on the difference between pointer and value receivers. Where the previous section outlined how this difference works for concrete types, the following continues on how it affects on when a type implements an interface.

It matters a lot whether we are using a pointer or value receiver when implementing interfaces. When you implement some interface using a pointer type and then attempt to store a value of your implementation in an interface, the interface will not be satisfied.

Consider the example below with two types, Foo and Bar that use value and pointer receivers respectively to implement the fmt.Stringer interface:

type Foo struct {} func ( Foo ) String () string { return "I am a Foo" } type Bar struct {} func ( * Bar ) String () string { return "I am a *Bar" } var _ fmt . Stringer = Foo {} var _ fmt . Stringer = & Foo {} var _ fmt . Stringer = Bar {} var _ fmt . Stringer = & Bar {}

Attempting to store a Bar value will not work and the compiler will warn us about our mistake:

./test.go:10:5: cannot use Bar literal (type Bar) as type fmt.Stringer in assignment: Bar does not implement fmt.Stringer (String method has pointer receiver)

Attempting to store &Foo as a fmt.Stringer will work fine, this is probably because pointers are automatically dereferenced by Go.

But what if we store our implementation in an interface{} and attempt to use it as a fmt.Stringer using a type assertion later on? The compiler will not warn us about this is like dynamic typing;it does not know what kind of values will be stored in this interface{} .

So running:

fmt . Println ( Foo {}) fmt . Println ( & Foo {}) fmt . Println ( Bar {}) fmt . Println ( & Bar {})

will print:

I am a Foo I am a Foo {} I am a *Bar

4. Silent copying of mutexes

The example code below shows a structure that is shared between multiple goroutines. One goroutine updates the structure and the other reads the updates. Access to this shared state is protected by a sync.Mutex .

However, it has a bug! Can you spot it?

type Thing struct { Map map [ string ] string mu sync . Mutex } func ( t Thing ) Read () { t . mu . Lock () defer t . mu . Unlock () for k , v := range t . Map { fmt . Println ( "read" , k , v ) } } func ( t * Thing ) Write () { t . mu . Lock () defer t . mu . Unlock () t . Map [ "entry" ] = fmt . Sprintf ( "%v" , time . Now ()) } func main () { t := Thing { Map : map [ string ] string {}} go func () { for { t . Read () } }() for { t . Write () } }

The culprit is the Read() method which does not have a pointer receiver, but a value receiver instead. This causes the mutex, mu , to be copied when Read() is called, introducing a race condition.

The effect is on my machine that it produced no output like one would expect. I suspect that this could be caused by either:

The mutex being copied in a locked state

The reading routine never observing the mutation because the state might be cached due to a lack of proper synchronisation.

Suggestions

Use pointer receivers for methods on concurrent structures.

5. Pointer comparisons

Go has pointers and you can perform comparisons on them using the == and != operators.

This creates a subtle trap when e.g. you intend to compare two strings by writing a == b . But suppose a and b are pointers, the comparison could evaluate to false even if both referenced strings are equal.

And when you are trying to find out why your program misbehaving it’s kind of hard to spot too, there are no extra symbols in the expression that remind you that you are comparing pointers! You have to look at the type declarations of the values which can be made even harder through type inference.

6. Reusing (error) variables

Go syntax that allows an if statement to be combined with another statement that the branch may act upon. This pattern is very common for functions that return an error:

if err := doThing (); err != nil { return err }

The nice thing about this is, is that the lexical scope of any new variables introduced is confined to the block of the if statement so they won’t pollute the scope outside making accidental reuse a lot less likely.

It is also possible to just assign to an already existing variable by using = instead of := .

But what if we assign to an error variable from a different goroutine? Well, that’s allowed, but it can become a real problem if the same variable is also re-used. The example below shows how reusing an err variable could introduce a race condition.

var err error go func () { if err = thing (); err != nil { // ... } } if err = otherThing (); err != nil { // ... }

Suggestions

Don’t reuse variables whenever possible, this also reduces the risk of accidental reuse.

In addition, use Go’s race condition detector, you can enable it with the -race flag when testing and building. It is able to detect cases such shown above.

7. Receiving from closed channels

Receiving from a closed channel is always able to immediately succeed while returning the zero value for the channel’s type.

But how do you intend to process this value? Does it mean something to your program? Will it cause something bad? If you are receiving in a loop, your loop might even never exit. You need to know when you need to stop processing.

Suggestions

You can check whether the value you received from a channel is zero because it was sent that way or originated from a close by receiving using the v, ok form:

value, ok := <-someChannel if !ok { // ... }

If ok is false, the channel has been closed and you can now safely exit without leaving anything in the channel’s buffer.

This trick is especially useful if you can not use a range loop to iterator over a channel because you need to select from other channels as well.

Conclusion

I that you have found this information interesting and that some time in the future when you run into some bug, something from this article pops up in your head and saves you a few hours of hair pulling.

If you have any suggestions for other pitfalls, I’m interested in hearing from you.