Top 10 Go Bugs to Catch in Code Reviews
Michael Chang ·
Listen to this article~5 min
Speed up your Go code reviews by learning the 10 most common bugs that slip through. From goroutine leaks to slice pitfalls, know exactly what to look for.
Let's be honest, code reviews can sometimes feel like a chore. You're staring at someone else's work, trying to be helpful while also making sure nothing slips through. It's a balancing act. But when you're reviewing Go code, there are specific pitfalls that pop up again and again. Knowing what to look for can turn a tedious review into a quick, effective one.
I've been through enough pull requests to notice patterns. Certain bugs have a way of hiding in plain sight, especially in Go with its unique quirks. Catching them early saves everyone a headache down the line. So, grab your coffee, and let's walk through the top offenders you should spot fast.
### Common Concurrency Catastrophes
Go is famous for making concurrency accessible with goroutines and channels. That's also where things go wrong most often. It's easy to launch a goroutine and forget about it.
- **Leaked goroutines:** This is the big one. You start a goroutine that runs in a loop or waits on a channel, but you never provide a way to stop it. It just runs forever, consuming memory. Always think about the exit strategy.
- **Unbuffered channel deadlocks:** If you send on an unbuffered channel without a corresponding receiver ready, the whole program grinds to a halt. It's a classic deadlock. Sometimes you need a buffered channel; other times, you need to restructure the logic.
- **Race conditions:** Sharing memory by communicating is the Go mantra, but sometimes old habits die hard. Modifying a map or a slice from multiple goroutines without a mutex is asking for trouble. The `-race` flag is your best friend here.
### Pointer and Nil Problems
Go's zero values are usually helpful, but they can lead to subtle bugs if you're not careful. A nil pointer isn't always an error until you try to use it.
You'll see this a lot with structs and slices. A function returns a nil slice, and you immediately try to `append` to it or range over it. It works, but it's a logic error waiting to happen. Similarly, passing a nil pointer to a function that expects to modify the underlying data does nothing. The function receives a copy of the pointer—which is nil—and any allocation inside the function is lost when it returns.
As one seasoned developer put it, "In Go, nil is a type, not just a value. Understanding the difference saves you from a world of runtime panics."
### Interface Implementation Issues
Go's interfaces are satisfied implicitly, which is elegant but can be sneaky. The bug here is often one of incomplete implementation. Your struct might have most of the methods of an interface, but if it's missing just one, the compiler won't automatically make the connection.
You might pass your struct to a function expecting the interface and get a confusing error about a missing method. The fix is simple—add the method—but spotting it requires knowing exactly what the interface requires. It's worth double-checking the interface definition during a review.
### Error Handling Oversights
Go makes you handle errors explicitly, which is great. The bug is when you handle them *too* quietly. A classic pattern is calling a function that returns an error and just logging it without returning or taking action.
```go
if err != nil {
log.Printf("Something went wrong: %v", err)
// Execution continues as if nothing happened!
}
```
This can leave your program in an inconsistent state. The error was noted, but the operation failed silently. In a review, ask: "If this fails, should we really continue?" Often, the answer is no, and that `log` should be a `return err`.
### The Slice Trap
Slices are references to underlying arrays. This is a powerful feature, but it leads to a very common bug: unintentionally sharing memory. When you pass a slice to a function, and that function modifies the slice's elements, it modifies the original data. That might be what you want. But if you `append` to a slice inside a function and the underlying array needs to reallocate, those changes might not be visible to the caller unless you return the new slice.
It's a subtle distinction between modifying elements and changing the slice header itself. In a review, watch for functions that take a slice, modify it (especially with `append`), and don't return anything. That's a red flag.
Spotting these bugs quickly isn't about being a Go guru. It's about knowing where the common traps are set. Focus on concurrency primitives, pointer semantics, interface satisfaction, error propagation, and slice behavior. If you can flag these areas, you'll make your code reviews faster, more thorough, and genuinely helpful. Your teammates will thank you, and your codebase will be much more robust for it.