Every Go design decision that looks like a missing feature is actually a deliberate choice to remove cognitive overhead. No method overloading. No implicit conversions. One canonical formatter. Generics that arrived late and deliberately. Go’s simplicity is not an accident — it’s a feature, and one that pays compounding dividends as a codebase and team grow.
The Problem
Languages that offer maximum expressiveness also offer maximum inconsistency. In Java or C++, method overloading sounds convenient:
// Java — seems convenient until you have to trace what gets called
public class Printer {
void print(String s) { ... }
void print(int n) { ... }
void print(String s, boolean newline) { ... }
void print(Object o, String format, boolean newline) { ... }
}
When you read a call to print(value), you don’t know which version runs without understanding the type of value. Overloading adds a dispatch layer in your head for every call site. That tax is small per instance, but it accumulates across a large codebase.
Implicit type conversions create a similar trap. In C, integer arithmetic can silently overflow. In JavaScript, "5" + 3 === "53". In Python 2, integer division truncated silently. These aren’t exotic edge cases — they’re common bugs that the language made invisible.
And style debates. Every language community eventually produces a style guide. Then a competing style guide. Then a linting tool. Then endless pull request comments about brace placement and import ordering. The cognitive cost is real and completely avoidable.
The Idiomatic Way
Go doesn’t allow method overloading. Functions have specific, distinct names:
// WRONG attempt — won't compile
type Printer struct{}
func (p Printer) Print(s string) { ... }
func (p Printer) Print(n int) { ... } // compile error: method redeclared
// RIGHT — explicit, distinct names
type Printer struct{}
func (p Printer) PrintString(s string) { fmt.Print(s) }
func (p Printer) PrintInt(n int) { fmt.Print(n) }
func (p Printer) PrintLine(s string) { fmt.Println(s) }
func (p Printer) Printf(format string, args ...any) { fmt.Printf(format, args...) }
When you call p.PrintInt(42), it is unambiguous. There’s no dispatch logic to understand. The name tells you exactly what’s happening.
Type conversions are explicit — no automatic widening, no silent coercion:
// WRONG — won't compile in Go
var x int32 = 100
var y int64 = x // cannot use x (type int32) as type int64
// RIGHT — conversions are explicit
var x int32 = 100
var y int64 = int64(x) // clear, intentional
This looks pedantic until you hit the classic truncation bug:
func averageRequests(counts []int) float64 {
total := 0
for _, c := range counts {
total += c
}
// WRONG — integer division, truncates toward zero
// return float64(total / len(counts))
// RIGHT — convert before dividing
return float64(total) / float64(len(counts))
}
The difference between those two lines is a subtle, silent bug. Go’s explicitness means you make this choice consciously, at the site where it matters.
gofmt ends style debates entirely:
// Before gofmt — however you happened to write it:
func add(x int,y int) (int) {
return x+y
}
var m = map[string]int{"one": 1,"two": 2,"three":3}
// After gofmt — always this, for everyone:
func add(x int, y int) int {
return x + y
}
var m = map[string]int{"one": 1, "two": 2, "three": 3}
The value isn’t in which style is chosen — it’s that the choice is made once, universally, and never revisited. Code reviews in Go don’t contain comments about brace placement. Every Go file you open, anywhere, looks the same.
On generics: Go added them in 1.18 (2022). Before that, Go codebases were perfectly functional. The lesson is that generics are genuinely useful for a narrow category of problems — data structures, algorithms that operate on multiple types, library code that can’t know its types at compile time. They are not useful as a pattern for general business logic:
// WRONG — reaching for generics when a concrete function is clearer
func Filter[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
activeUsers := Filter(users, func(u User) bool { return u.Active })
// RIGHT — a concrete function with an obvious name
func activeUsers(users []User) []User {
var result []User
for _, u := range users {
if u.Active {
result = append(result, u)
}
}
return result
}
The generic version is more general. The concrete version is more readable, more searchable (grep for activeUsers), and requires no knowledge of Go’s type constraint system to understand.
Generics do shine for reusable data structures:
// WHERE generics make sense — a typed stack that works for any element type
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
Without generics, you’d need a StringStack, IntStack, and UserStack. With them, one implementation handles all cases safely. Use generics here.
In The Wild
Go has 25 keywords:
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
No class. No extends. No implements. No try, catch, finally, throw. No abstract, virtual, override. No public, private, protected — just exported (capitalized) and unexported.
When you learn these 25 keywords, you’ve learned the entire control flow vocabulary of the language. A Go programmer from 2012 can read Go code written in 2024 without consulting documentation. Contrast that with languages where every senior engineer has developed strong opinions about how the language should be used: you join the team and have to learn not just the language but the local dialect, the preferred libraries, the architectural patterns that were fashionable when the codebase started. Each of those is onboarding friction.
Go’s constraints compress that friction. The language is small enough that there aren’t many ways to express the same thing, so code across a large codebase tends to look similar even when written by different people at different times.
The Gotchas
Fighting the formatter. Some engineers spend energy trying to structure code to “look better” before gofmt runs. Stop. Let gofmt have it. Run it on save. The goal is to never think about formatting again.
Misapplying generics to business logic. The fact that you can write ProcessItems[T Processable](items []T) doesn’t mean you should. Generic business logic is harder to read, harder to debug (error messages get verbose), and provides flexibility that your application almost certainly doesn’t need. Write the concrete version first.
Treating the language’s “missing features” as problems to solve. No method overloading means you write explicit function names. No implicit conversion means you write explicit casts. These feel like extra work until you’re reading code written by someone else — and then you appreciate that there’s only one interpretation possible.
Key Takeaway
Go’s simplicity is load-bearing. The language was designed to be written by large teams over long time horizons, and every constraint — 25 keywords, no overloading, explicit conversions, one formatter, late and deliberate generics — is there to make the codebase maintainable two years after it was written, by people who weren’t there when it started. You give up some expressive power. You get back code that junior developers can read on their first day, that compiles in seconds, that produces predictable behavior, and that your team can refactor confidently. Simplicity in Go isn’t about doing less. It’s about doing exactly what’s needed, in the most direct way possible, and trusting that clarity compounds over time.
← Lesson 24: Prefer Plain Structs | Course Index | 🎓 Course Complete!