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 vetandgolint: 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.