What are common Go anti-patterns?

5 minadvancedgoanti-patternsbest-practicescode-quality

Quick Answer

Common Go anti-patterns include: overusing interfaces (defining an interface with only one implementation "just in case," adding indirection with no real benefit — Go's guidance is to define interfaces at the point of use, not preemptively); ignoring errors (_ = someCall() or result, _ := someCall() on something that can genuinely fail); goroutine leaks from missing cancellation, covered earlier; overusing interface{}/any where generics or a concrete type would be safer and clearer; global mutable state shared across a package with no synchronization; and large, generic "util" or "common" packages that become a dumping ground with no clear responsibility, making dependencies hard to reason about. Most of these share a root cause: designing for flexibility the code doesn't actually need yet, at the cost of clarity it does need now.

Detailed Answer

Most Go anti-patterns come from applying habits or patterns from other languages that don't fit Go's actual design philosophy.

Premature interface abstraction

// Anti-pattern: an interface with exactly one implementation, defined "just in case"
type UserRepository interface {
    GetUser(id int) (*User, error)
}
type postgresUserRepository struct{ db *sql.DB }
func (r *postgresUserRepository) GetUser(id int) (*User, error) { ... }

If there's genuinely only one implementation and no test double need yet, this interface adds indirection with no real payoff. Go's own guidance, "accept interfaces, return structs," favors defining an interface at the consumer side. Do that when a second implementation or a test double actually becomes necessary, not speculatively ahead of time.

Silently ignoring errors

data, _ := os.ReadFile("config.json")   // if this fails, data is nil, and you'll get a
                                          // confusing downstream error instead of a clear one
json.Unmarshal(data, &cfg)
data, err := os.ReadFile("config.json")
if err != nil {
    return fmt.Errorf("reading config: %w", err)
}

Overusing any where a concrete type or generic would do

func Process(items []any) { ... }   // loses all type safety, forces assertions everywhere
func Process[T any](items []T) { ... }   // type-safe, same flexibility

Unsynchronized global mutable state

var globalCache = map[string]string{}  // no mutex — a data race waiting to happen
                                         // the moment two goroutines touch it concurrently

Sprawling "util" or "common" packages

package util
func FormatDate(...) {}
func ValidateEmail(...) {}
func ParseConfig(...) {}
func SendEmail(...) {}
// dozens of unrelated functions dumped into one package with no clear boundary

A package like this accumulates unrelated responsibilities over time, makes it hard to reason about what actually depends on what, and often becomes a dependency magnet that couples unrelated parts of a codebase together unnecessarily.

The underlying theme

Nearly every one of these traces back to designing for hypothetical future flexibility. Go's simplicity-first philosophy doesn't ask for that, and it costs the clarity and directness the language is actually optimized for.

Related Resources