Golang Pitfalls

Go is a powerful language, but it has some common pitfalls that can trip up even experienced developers. Understanding these can help you write more robust code. This guide covers frequent mistakes, with examples and explanations.

Caveat: Capturing Iteration Variables

One of the most common pitfalls in Go is related to closures capturing loop variables. In a for loop, the loop variable is reused, so closures may capture the final value instead of the intended one.

Example 1: Creating a New Func in a Loop

From Section 5.6.1 in “The Go Programming Language”

var rmdirs []func()

for _, d := range tempDirs() {
    dir := d // NOTE: necessary! create a new local variable to capture the value
    os.MkdirAll(dir, 0755)
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dir)
    })
}

// do some work ...

for _, rmdir := range rmdirs {
    rmdir() // clean up
}

If we don’t have dir := d, all funcs in rmdirs will capture the last value of d from tempDirs(), leading to incorrect cleanup.

Example 2: Thumbnail Image, Passing Value as an Explicit Argument to Anonymous Func

From Page 235 of “The Go Programming Language”

// correct!
func makeThumbnails3(filenames []string) {
    ch := make(chan struct{}, len(filenames))
    for _, f := range filenames {
        go func(f string) {
            thumbnail.ImageFile(f)
            ch <- struct{}{}
        }(f) // here is important
    }

    // wait for goroutines to complete
    for range filenames {
        <-ch
    }
}

Notice that we passed the value of f as an explicit argument to the literal function instead of using the declaration of f from the enclosing for loop:

// incorrect!
for _, f := range filenames {
    go func() {
        thumbnail.ImageFile(f) // NOTE: incorrect!
    }()
}

Without passing f as an argument, all goroutines would use the final value of f.

Other Common Pitfalls

1. Nil Pointer Dereference

Attempting to access methods or fields on a nil pointer causes a panic.

var p *int
*p = 5 // Panic: runtime error: invalid memory address or nil pointer dereference

Fix: Check for nil before dereferencing.

if p != nil {
    *p = 5
}

2. Slice Append Issues

Appending to a slice can cause unexpected behavior if not handled correctly.

s := []int{1, 2, 3}
s = append(s, 4) // Correct
append(s, 5)     // Incorrect: doesn't modify s

Fix: Always assign the result of append back to the slice.

3. Map Iteration Order

Maps do not guarantee order; iteration order is random.

m := map[string]int{"a": 1, "b": 2}
for k, v := range m {
    fmt.Println(k, v) // Order may vary
}

Fix: Use slices for ordered data.

4. Shadowing Variables

Variable shadowing can lead to bugs.

x := 10
if x > 5 {
    x := 5 // Shadows outer x
    fmt.Println(x) // Prints 5
}
fmt.Println(x) // Prints 10

Fix: Use different variable names or be aware of scope.

5. Defer in Loops

Defer statements execute at function end, not loop end.

for i := 0; i < 3; i++ {
    defer fmt.Println(i) // Prints 2, 1, 0 at function end
}

Fix: Use anonymous functions if needed.

Best Practices to Avoid Pitfalls

  • Use go vet and golint: These tools catch common issues.
  • Write Tests: Unit tests can reveal pitfalls.
  • Read Effective Go: Covers many best practices.
  • Code Reviews: Peer reviews help spot mistakes.

Conclusion

Awareness of these pitfalls can save debugging time. Go’s simplicity hides some subtleties, so always test and review your code carefully.

Resources