Code Review in Go: What to Check to Keep Your Code from Blowing Up
\n\nCode review is not just a checkbox in Jira or an excuse to argue with a colleague in pull request comments. It is, perhaps, the most honest way to avoid shooting yourself in the foot in production. And when it comes to Go — a language loved for its simplicity and hated for "where are the generics?" (spoiler: they are here now) — review becomes an art. In this article, we will break down exactly what to look for during a code review in Go so that your code not only works but also pleases the eye. Let's go!
\n\n1. Error Handling: Don't Ignore, Don't Fear
\n\nGo has no exceptions. Everything is straightforward: if a function can fail, it returns an error. The most common problem for beginners is ignoring errors with an underscore. Remember: "Never ignore an error" is not just a mantra, it's the law.
What to check in the code:
\n- \n
- Checking err != nil — it should be everywhere a function returns an error. Even if you are sure "there will be no error here". \n
- Logging or propagating — the error should either be logged with context or returned up the stack. Silencing it is bad form. \n
- Comparison with sentinel errors — use
errors.Is()anderrors.As(), not direct comparison with==. \n
// Bad: ignoring the error\nfile, _ := os.Open("config.yaml")\n\n// Good: checking and logging\nfile, err := os.Open("config.yaml")\nif err != nil {\n log.Printf("failed to open config: %v", err)\n return err\n}\ndefer file.Close()\n\n\n2. Goroutines and Concurrency: Order in Chaos
\n\nGoroutines are Go's superpower. But with great power comes great responsibility. During reviews, we often see goroutine leaks, data races, and deadlocks. Code review in Go without checking concurrency is like driving a sports car without a seatbelt.
\n\nChecklist for goroutines:
\n- \n
- Starting goroutines — always control their lifetime. Use
sync.WaitGroupor contexts. \n - Data races — check that access to shared data is protected by a mutex or channel. Run
go test -race. \n - Panic in a goroutine — if a goroutine panics, the entire program crashes. In production code, always use
recover. \n
// Bad: goroutine without control\ngo func() {\n // can panic and kill the entire process\n doWork()\n}()\n\n// Good: with WaitGroup and recover\nvar wg sync.WaitGroup\nwg.Add(1)\ngo func() {\n defer wg.Done()\n defer func() {\n if r := recover(); r != nil {\n log.Printf("goroutine panicked: %v", r)\n }\n }()\n doWork()\n}()\nwg.Wait()\n\n\n3. Interfaces: Small and to the Point
\n\nIn Go, interfaces are contracts. But, unlike Java, they don't have to be huge. The Go community has a principle: "The smaller the interface, the better". One method is ideal. Two is normal. Three is already suspicious.
\n\nWhat to look for:
\n- \n
- Interface size — if it has more than 3-4 methods, consider splitting it. \n
- Accept interfaces, return structs — classic Go wisdom. It makes code flexible and testable. \n
- Don't define interfaces in advance — let the consumer, not the producer, create them. \n
// Bad: huge interface\ntype Storage interface {\n Save(data []byte) error\n Load(id string) ([]byte, error)\n Delete(id string) error\n List() ([]string, error)\n Connect() error\n Close() error\n}\n\n// Good: small interfaces\ntype Saver interface {\n Save(data []byte) error\n}\ntype Loader interface {\n Load(id string) ([]byte, error)\n}\n
Blogs
Book Recommendations
Read also
How to Add One Vector to Another in C++: What push_back Hides
Python Conditional Statements: From if to match case & Comparisons
Working with Databases in Rust: PostgreSQL and MongoDB
Rust in Real Projects: 5 Cases of Companies That Switched to It
Java in 2025: A Complete Guide for Beginners from Scratch