Part 1 of Code Like Go · why removing features teaches more than adding them
Most languages teach by accretion. Every release brings a keyword, some sugar, another clever way to do what already had three ways. You learn the language by learning the pile. Go went the other way. It taught a generation by refusing: looking at the usual bag of features and saying no to most of them, on purpose, then leaving the gap so you had to cross it yourself.
That refusal is the lesson. This is rm/acc, remove to accelerate, applied to language design. Cut the thing out and what's left has to be clear.
Go shipped in 2009. Generics landed in Go 1.18 in March 2022. That's about thirteen years where the most requested feature in the language, the one every C++ and Java refugee yelled about, simply did not exist.
The usual read is that the team was slow or stubborn. The real read is that they waited for a design that wouldn't rot the rest of the language. Meanwhile a whole ecosystem learned to write useful, fast, readable code without type parameters. People found that interface{} plus a type switch covered more than expected, that copying a forty-line function is often cheaper than a generic abstraction nobody wants to read, that sort.Slice with a closure beats a generic Sort<T> in the 95% case.
The absence taught restraint. By the time generics arrived, the community already knew to reach for abstraction last, not first.
When generics finally shipped they were narrow, boring, constraint-based, not the C++ template nightmare. Subtraction first. Addition only after it proved itself.
There is no class in Go. No extends. No superclass, no override chain, no diamond problem, no abstract base nobody can instantiate. The thing OO courses spend weeks on is gone.
What's left is composition. You embed a struct in a struct and its methods get promoted. You satisfy small interfaces. The famous Go proverb is accept interfaces, return structs, and the equally famous one is the bigger the interface, the weaker the abstraction.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// Composition, not inheritance: stack the small pieces.
type ReadWriter interface {
Reader
Writer
}
Removing inheritance forces you to think in behavior (what can this thing do?) instead of lineage (what did this thing descend from?). The single-method interface, io.Reader, io.Writer, fmt.Stringer, error, became one of the most copied ideas in the language because the heavy alternative was never allowed in.
Errors are values. There is no try, no catch, no hidden non-local jump teleporting control flow up the stack to a handler you forgot existed. A function that can fail returns an error as its last value, and you check it.
f, err := os.Open(name)
if err != nil {
return fmt.Errorf("open %s: %w", name, err)
}
defer f.Close()
People mock the if err != nil repetition. They're wrong. The repetition is the point: the error path stays visible, inline, in the same column as everything else. With exceptions, failure is hidden. You can't tell by reading a function which lines might blow up. Go made failure explicit and put it where your eyes already are. Remove the magic, keep the clarity.
Three smaller subtractions. Same shape.
One loop keyword. There is no while, no do, no foreach. There is for. for with three clauses, for with one (a while), for with none (a loop), for range. One keyword, four shapes, nothing extra to learn. You don't argue about loop style because there is only one.
for i := 0; i < n; i++ { } // C-style
for x < limit { } // while
for { break } // infinite
for i, v := range items { } // range
One formatter. gofmt has no options. None. Tabs, brace placement, alignment, all decided, all final. The whole category of "code style" arguments that eats Java and JavaScript review just does not exist in Go. Every Go file on earth looks the same. That sameness isn't aesthetic; it's load removed. You read structure, not style.
No implicit conversions. You cannot add an int32 to an int64 without saying so. No silent widening, no truncation you didn't ask for, no "3" + 4 guessing game. The conversions you stop seeing in C are exactly the bugs that ship. Go makes you write them down, and writing them down makes you notice them.
Look at the pattern. Inheritance gone → you think in behavior. Exceptions gone → the failure path is visible. Style options gone → you read structure. Generics absent → you reach for abstraction last. Every gap Go left in the spec is a place where the language stopped deciding for you, so you had to decide in the open, where the next reader can see it.
A feature you don't have is a class of bug you can't write and a debate you don't have to have.
This is why clear is better than clever is the load-bearing Go proverb. The language was built by removing the tools you'd use to get clever. What's left is boring code, and boring code beats clever code, because boring code is what you can still read at 3am during the incident.
The broader krons thesis is that you accelerate by removing, not adding: in systems, in process, in code. Go is the cleanest proof lying around. Its defining decisions are subtractions, and it became one of the most productive languages on the planet because of them. The constraints are not something to apologize for. The constraints are the teaching.
Minimalism starts, practically, by adopting the Go principles, not in Go necessarily, in any language. Remove the clever escape hatch. Make the error path visible. Pick one way to do the thing. Let the boring version ship.
Next → The Non-Negotiables: errors as values, small interfaces, and the rest of the toolbox left after you put the clever toys away.