← Code Like Go

Subtraction as Pedagogy

Part 1 of Code Like Go · why removing features teaches more than adding them

Most languages teach you by accretion. Every release adds a keyword, a syntax sugar, a clever new way to do the thing you already had three ways to do. You learn the language by learning the pile. Go did the opposite. Go taught a generation of programmers by refusing — by looking at the standard bag of language features and saying no to most of them, on purpose, and leaving the gap exposed so you had to walk through it.

That refusal is the whole lesson. This is rm/acc — remove to accelerate — applied to language design. Strip the thing out, and what's left forces clarity.

The decade without generics

Go shipped in 2009. Generics landed in Go 1.18 in March 2022. That's roughly thirteen years where the single most-requested feature in the language — the one every C++ and Java refugee screamed about — was simply not there.

The conventional read is that the team was slow or stubborn. The real read is that they were waiting for a design that didn't poison the rest of the language. And in the meantime, an entire ecosystem learned to write useful, fast, readable code without reaching for type parameters. People discovered that interface{} plus a type switch covered more cases than they expected, that copy-paste of a forty-line function is often cheaper than a generic abstraction nobody can read, that sort.Slice with a closure beats a generic Sort<T> for the 95% case.

The absence taught restraint. By the time generics arrived, the community had already internalized that you reach for abstraction last, not first.

When generics finally shipped they were narrow, boring, constraint-based — not the Turing-complete template metaprogramming horror of C++. Subtraction first; addition only when it earned its keep.

No inheritance

There is no class in Go. No extends. No superclass, no method override, no diamond problem, no abstract base nobody can instantiate. The thing every OO course spends six weeks on — 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 forced you to think in terms of behavior (what can this thing do?) instead of lineage (what is this thing descended from?). The single-method interface — io.Reader, io.Writer, fmt.Stringer, error — is the most copied design idea in the language, and it only exists because the heavyweight alternative was taken off the table.

No exceptions

Errors are values. There is no try, no catch, no invisible non-local jump that teleports control flow up the stack to some handler you forgot you wrote. 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 is visible, in line, in the same column you read everything else. With exceptions, the failure path is invisible — you cannot tell by reading a function which lines can blow up. Go made the cost of failure explicit and put it where your eyes already are. Removing the magic added the clarity.

One loop, one formatter, no implicit conversions

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 to memorize. You never argue about which loop construct to use 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 non-negotiable. The entire category of "code style" arguments that consume Java and JavaScript code review just does not exist in Go. Every Go file on earth looks the same. That uniformity isn't aesthetic; it's cognitive 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'd never think about in C are exactly the bugs that ship; Go made you write them down, and writing them down made you notice them.

Each removal is a forced clarity

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 could no longer make a decision for you, so you had to make it yourself, 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 be clever. What's left is the boring code — and boring code beats clever code, because boring code is the code you can still read at 3am during the incident.

rm/acc as language design

The broader krons thesis is that you accelerate by removing, not adding — in systems, in process, in code. Go is the cleanest existing proof. It is a language whose defining decisions are subtractions, and it became one of the most productive languages on the planet precisely because of them. The constraints aren't a limitation to apologize for. The constraints are the pedagogy.

If you take one thing from this whole series: write code the way you'd write Go. 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 in the series → the mechanics of those removals: errors as values, small interfaces, and the rest of the toolbox that's left after you put the clever toys away.