Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
244: Code Review Checklist for Idiomatic Go
When you start reviewing code for other Go developers, you'll notice that "working code" isn't the goal—"idiomatic code" is. Go is a language of opinions. It prizes clarity over cleverness and simplicity over abstraction. If you're coming from Java or Python, your instinct will be to build hierarchies and complex abstractions. In Go, that's usually a red flag.
Let's look at this through a real-world scenario. Imagine a teammate submitted a PR for a simple UserRepository. It works, it passes the tests, but it feels... heavy. Let's walk through how I'd review this and refactor it into something that looks like it was written by a seasoned Gopher.
// The original PR submission
type User struct {
ID int
Name string
}
type UserStore interface {
FetchUser(id int) (*User, error)
}
type MySQLStore struct {
connection string
}
func (m *MySQLStore) FetchUser(id int) (*User, error) {
if id != 0 {
user, err := m.doLookup(id)
if err == nil {
return user, nil
} else {
return nil, err
}
} else {
return nil, errors.New("id cannot be zero")
}
}
Flattening the Happy Path
The first thing that jumps out at me is the "Pyramid of Doom" in FetchUser. The logic is nested inside if/else blocks, pushing the "happy path" (the successful result) deep into the right side of the screen. In Go, we do the opposite: we handle errors first and return early. This keeps the successful flow aligned to the left margin.
func (m *MySQLStore) FetchUser(id int) (*User, error) {
if id == 0 {
return nil, errors.New("id cannot be zero")
}
return m.doLookup(id) // No need for the extra if/else wrapper
}
Avoiding Interface Pollution
Now look at the UserStore interface. A common mistake for newcomers is to define an interface before they have more than one implementation. This is "Interface Pollution." In Go, interfaces are satisfied implicitly. You don't need to declare that MySQLStore implements UserStore; it just does.
My rule of thumb during reviews: Accept interfaces, return structs. The MySQLStore should be returned as a concrete pointer, and the consumer of the store should define the interface they actually need. I'd tell the developer to delete the UserStore interface entirely unless we're already implementing a PostgresStore or a MockStore.
Wait, I messed up the slice
Let's add a method to fetch multiple users. I'll write this quickly to show you a mistake I actually made in a PR last week. I wanted to pre-allocate a slice for performance because I knew I was expecting about 10 users.
func (m *MySQLStore) FetchActiveUsers() ([]*User, error) {
users := make([]*User, 10) // My mistake here
// Imagine a loop that appends results
for i := 0; i < 10; i++ {
u, _ := m.doLookup(i)
users = append(users, u)
}
return users, nil
}
I caught this during my own self-review. By calling make([]*User, 10), I created a slice that already contains 10 nil elements. When I call append, Go adds the new users after those 10 nils. The resulting slice would have a length of 20. To fix this, I need to set the length to 0 and the capacity to 10.
// The correction
users := make([]*User, 0, 10)
Cleaning up the Nomenclature
Finally, I'd look at the naming. Go prefers short, punchy names when the context is clear. FetchUser is okay, but inside a UserRepository, Get is often sufficient. Also, if a variable's type is User, naming the variable userObj or userStruct is redundant. Just use u or user.
Here is the final, idiomatic version of our little service:
type User struct {
ID int
Name string
}
type MySQLStore struct {
conn string
}
func NewMySQLStore(conn string) *MySQLStore {
return &MySQLStore{conn: conn}
}
func (s *MySQLStore) Get(id int) (*User, error) {
if id == 0 {
return nil, errors.New("id cannot be zero")
}
return s.doLookup(id)
}📋 Practical Task
Refactoring the Legacy Order Processor
You are reviewing a PR for an order processing system. The current code is functional but non-idiomatic. Refactor the following ProcessOrder function to follow Go idioms:
- Eliminate the nested "Pyramid of Doom" by using early returns.
- Remove redundant
elseblocks. - Fix the slice initialization mistake (it currently creates unnecessary empty elements).
- Simplify the naming.
func ProcessOrder(orderID int, items []string) ([]string, error) {
var processedItems []string
if orderID != 0 {
if len(items) > 0 {
// Mistake: This pre-allocates length instead of capacity
processedItems = make([]string, len(items))
for _, item := range items {
upperItem := strings.ToUpper(item)
processedItems = append(processedItems, upperItem)
}
return processedItems, nil
} else {
return nil, errors.New("no items provided")
}
} else {
return nil, errors.New("invalid order id")
}
}There are no comments for now.