How Go Thinks

Lesson 1 of 5

Why Go Looks Like This

"Go is a language designed for large software systems, and one of the most important tools for managing complexity in a large system is to be able to read the code." — Rob Pike, "Go at Google: Language Design in the Service of Software Engineering"

Most languages are introduced by what they let you do. Go is better introduced by what it declined to do, because the declining is the design. There is no inheritance. No exceptions. No method overloading, no default arguments, no operator overloading. Until 2022 there were no generics at all, and their eventual arrival was argued over for a decade. The language has twenty-five keywords, a number chosen deliberately and defended stubbornly.

Read that list as a set of omissions and Go looks impoverished — a language for people not trusted with better tools. That reading is common and it is wrong, but it is wrong in an interesting way, and correcting it is the whole purpose of this chapter.

Every one of those absences was a trade, and they were all traded against the same thing: the cost of reading code you did not write. Go came out of Google in 2007, where the problem was not that programs were hard to write but that codebases had become hard to understand — millions of lines, thousands of engineers, build times measured in the tens of minutes, and a constant churn of people inheriting code from people who had left. In that setting, a feature that makes writing 10% more pleasant while making reading 10% harder is not a neutral trade. It is a bad one, because code is read far more often than it is written, by far more people.

So this chapter is about the constraints that produced the language, because once you can see them the rest of Go stops looking arbitrary. Fast compilation is not a nice-to-have; it shaped the type system. One formatting standard is not pedantry; it deleted a category of argument. And the absence of inheritance is not a gap where a feature should be — it is the space that interfaces occupy, which is where the next chapters go.

The big question

If you had to optimise a language for the person who inherits the code rather than the person who writes it, which features would you refuse — and what would you accept in exchange?

Twenty-five keywords

Start with the concrete claim, because it is checkable and the number is smaller than people expect.

Go — Try it Yourself
Loading editor…
Output
Click Run to execute the code.

Five of those keywords — go, chan, select, defer, range — do work that other languages need libraries or frameworks for. That is where the budget went. A small vocabulary is not a small language; it is a language that spent its complexity somewhere specific and can point at where.

The public line is worth pausing on. Go has no visibility keywords because capitalisation is the visibility rule: an identifier starting with an upper-case letter is exported from its package, and a lower-case one is not. That is either elegant or infuriating depending on the day, and it is characteristic — a syntactic decision that removes a keyword, removes a decision, and makes visibility visible at every use site rather than only at the declaration.

Everyday analogy

Consider the difference between a chef's knife and a drawer of specialised gadgets. The gadget drawer is genuinely better for the specific task each gadget was made for — an avocado slicer really does slice avocados more neatly than a knife. But a kitchen staffed by rotating cooks develops a problem the gadget drawer cannot solve: every new cook must learn the drawer before they can work, and half of them will reach for the wrong thing. The knife is worse at avocados and better at kitchens. Go is a language that made that bet institutionally, and it is worth being honest that the bet has a cost — there are absolutely tasks where a Go programmer writes twenty lines that a Python programmer writes in three. The claim is not that the knife wins every cut. It is that a large kitchen with high turnover is a different optimisation problem from a single cook at home, and Go is unapologetically solving the first one.

Compilation speed is a language feature

Here is the constraint that surprises people most, because it sounds like an implementation concern and is actually visible in the type system.

Go was designed so that compilation could be fast, and "fast" meant something specific: fast enough that the edit-build-test loop feels interactive on a codebase of millions of lines. That goal ruled out language features, and the clearest casualty is how dependencies work.

Go — Try it Yourself
Loading editor…
Output
Click Run to execute the code.

The unused-import rule is the single most complained-about thing in Go, and it is downstream of the compilation model. If the compiler is going to trust that reading one package's export data is sufficient, the declared dependencies must be exactly the real ones. An unused import is a lie about the dependency graph, and the graph is load-bearing.

You are allowed to find this annoying. What is not accurate is treating it as arbitrary fussiness — it is the visible edge of the decision that makes go build finish in a second.

A little history

The origin story is unusually well attested and is worth knowing because it explains the priorities. Rob Pike has described Go as beginning during a C++ build at Google that took roughly 45 minutes — the three of them, Pike, Ken Thompson and Robert Griesemer, waiting, and starting to sketch a language on a whiteboard out of irritation. That anecdote gets repeated as a joke about slow builds, but the substantive point is who was waiting: three people with as much language-implementation history as anyone alive. Thompson had co-created Unix and B; Pike had worked on Plan 9 and UTF-8; Griesemer had worked on the Java HotSpot VM and V8. The language they produced looks conservative because they had spent careers watching which clever features aged badly at scale. It is also worth noting what they got wrong by their own later admission: the decade-long resistance to generics ended in 1.18 conceding they were necessary, and the original dependency-management story went through three replacements before modules landed in 1.11. The design was opinionated, not infallible, and both of those reversals are visible in the language today as slightly awkward seams.

One formatting standard, zero arguments

The next constraint is social rather than technical, and it may be the most successfully exported idea in the language.

Go ships gofmt. It is not a linter with options; it is a formatter with almost none. There is no configuration file, no team style guide to negotiate, no tabs-versus-spaces discussion — the answer is tabs, it was decided in 2009, and the tool will not argue.

Go — Try it Yourself
Loading editor…
Output
Click Run to execute the code.

That last line is the design in miniature. The value of gofmt is not that its choices are correct — several are debatable — but that they are settled. A configurable formatter gives back exactly the thing the tool existed to remove.

This is the idea other ecosystems borrowed most eagerly. Prettier, Black, and rustfmt all descend from the observation that consistency is worth more than preference.

The engineer's notebook

The habit I would build first in Go is reading the standard library, and I say that having ignored the advice myself for a long time. It is unusual among standard libraries in being written to be read: consistent, heavily commented, and mostly free of the cleverness that makes library code hard to follow. When I am unsure how something should look in Go — how to name a thing, how to shape an interface, where to put an error — the answer is almost always available in the source of a package I already use, and it is more reliable than a blog post because it has survived fifteen years of review. The second habit is to stop translating. Almost everyone arrives from another language and writes that language in Go for a while — inheritance hierarchies via embedding, exceptions via panic, class instances via pointer-to-struct-with-constructor. It compiles and it works and it reads as foreign to anyone who knows the language, which is exactly the cost Go was built to avoid. The tell is when you catch yourself asking "how do I do X in Go" where X is a mechanism rather than a goal. The better question is what problem X was solving, because Go often addresses it somewhere else entirely.

What the omissions bought

Now the accounting. Each absence purchased something, and it is worth naming the purchases rather than treating the list as loss.

Go — Try it Yourself
Loading editor…
Output
Click Run to execute the code.

The generics concession matters because it shows the principle is a principle rather than a taste. The argument against generics was never that abstraction is bad; it was that this particular abstraction cost more in readability than it saved in duplication. When enough evidence accumulated that the reverse was true for a real class of code — containers, and functions over collections — the language changed. Slowly, and with a syntax nobody loves, but it changed.

In the real world

A team I know rewrote a Python service in Go expecting a performance win, and got one — but the change they kept talking about a year later was different. Their Python service had accumulated the things a permissive language permits: three ways of constructing the same object, decorators whose behaviour depended on import order, a metaclass nobody remaining understood, and duck-typed interfaces that were documented only by what happened to be passed in. None of that was incompetence; it was the residue of five years and eleven contributors, each reaching for a reasonable tool. The Go rewrite could not accumulate most of it, because the language does not offer the tools. What surprised them was the onboarding time: a new engineer became useful in days rather than weeks, and the reason was not that Go is simpler to learn but that there was less project-specific vocabulary to learn. The honest other side of the story is that the Go service is about 30% more lines for the same behaviour, and the team found the error handling tedious for the first two months. They considered it a good trade. I mention both halves because "we rewrote it in Go and everything improved" is the kind of story that gets told without its costs, and the costs are real.

Reading Go you did not write

The proof of the design is what happens when you meet unfamiliar code. Go's claim is that you can work out a lot from the shape alone, before understanding the domain.

Go — Try it Yourself
Loading editor…
Output
Click Run to execute the code.

That closing list is deliberate. Go's signatures are unusually informative and they are not complete — concurrency safety, error semantics and aliasing are all invisible in a type, and all three are things this volume returns to. A language that made those visible in the signature would be a different and much heavier language, and Go chose documentation and convention instead. Whether that was the right call is a genuinely open question, and Rust's answer to the first one is the strongest counter-argument in modern language design.

Common Mistakes

  1. Reading the omissions as immaturity. Every one was a deliberate trade with a stated purpose. Disagreeing with a trade is reasonable; assuming nobody considered it is not.

  2. Fighting the unused-import error. It exists because the dependency graph is load-bearing for compilation speed. It is the visible edge of the thing that makes builds fast.

  3. Trying to configure gofmt. There is nothing to configure, and that is the feature — a flag would restore the argument the tool was built to end.

  4. Writing your previous language in Go. Inheritance via embedding, exceptions via panic, constructors for everything. It compiles, and it reads as foreign to everyone who knows the language.

  5. Asking "how do I do X in Go" where X is a mechanism. Ask what X was solving; Go often addresses it somewhere else entirely.

  6. Assuming a small language is a simple one. Twenty-five keywords, and five of them are a concurrency model most languages need a library for.

  7. Treating capitalisation as a style choice. It is the visibility rule. Renaming Get to get changes your package's public API.

  8. Expecting the standard library to be a reference and not a text. It is written to be read, and it is the most reliable style guide available.

  9. Believing the generics arrival proves the original design was wrong. It proves the principle was applied, not merely asserted — the evidence changed for one class of code.

  10. Quoting the 30%-more-lines cost as though it settles the argument, or ignoring it as though it does not exist. It is real, and it is the price of the property.

Interview Corner

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Junior
Why does Go not have classes or inheritance?
Model answer — It has structs for data and methods you attach to types, so it has most of what classes give you — it just has no inheritance. Instead you compose: embed one struct in another to reuse its behaviour, and use interfaces for polymorphism. The reasoning is that deep inheritance hierarchies are hard to read, because understanding one method can mean tracing up several levels to find where it is actually defined. Composition keeps that local.
Mid
Why is an unused import a compile error rather than a warning?
Model answer — Because the dependency graph has to be exact for Go's compilation model to work. Unlike a C-style #include, which textually pastes a header so the compiler transitively reads everything, a Go package's compiled output records everything an importer needs — so importing a package never makes the compiler open that package's own dependencies. That is a large part of why builds are fast, and it relies on declared dependencies being the real ones. An unused import is a false edge in that graph. The same reasoning is why import cycles are forbidden outright: there would be no valid order to compile them in. It reads as fussiness and it is really the visible edge of the compilation model.
Senior
Go resisted generics for thirteen years and then added them. What does that tell you about the language's design process?
Model answer — That the readability principle was an actual criterion rather than a slogan, applied consistently enough to be reversed by evidence. The original objection was never that abstraction is bad — it was that type parameters cost more in comprehension than the duplication they remove, which for most code was true. What accumulated over a decade was evidence that for one specific class of code, containers and functions over collections, the duplication cost more: every project reimplementing the same map/filter, or reaching for interface{} and losing type safety at the boundary. So the principle stayed and the empirical judgement changed. I would also read it as a caution, though. Thirteen years is a long time to be wrong about something, and the syntax they eventually shipped shows the strain of adding a major feature to a settled language. The same story is visible in dependency management, which took three attempts before modules. Go's design process is conservative in a way that avoids a lot of bad features and is genuinely slow to fix real gaps, and both halves of that are worth saying out loud.
Company lens — how the same topic gets asked
micro1Practical and quick: name three things Go does not have and what you use instead.
GoogleDesign reasoning: why does the compilation model forbid import cycles, and what would break if it did not?
MetaTrade-offs: when would you NOT choose Go, and be specific about the workload.
AmazonOperational: what makes a Go codebase cheap to hand to a new team, and where does that break down?
Hands-on

Two functions about the rules this chapter described, both mechanical enough to test exactly. The first implements Go's visibility rule — which is a real rule with real edge cases around non-letters, not just "check for a capital". The second detects import cycles, the thing the compilation model forbids; a correct answer needs to find a cycle anywhere in the graph rather than only a direct mutual import.

Go — Try it Yourself
Loading editor…
Output
Click Run to execute the code.

Summary

  • Go's design is best read as a set of refusals, each traded for something specific — and the thing bought is almost always what a reader can conclude locally.

  • Twenty-five keywords, five of which (go, chan, select, defer, range) do work other languages need libraries for.

  • Capitalisation is the visibility rule, which removes a keyword and makes visibility legible at every use site.

  • Compilation speed shaped the language. A package's compiled output is sufficient for its importers, so the compiler never transitively reads dependencies.

  • Unused imports and import cycles are errors because that dependency graph has to be exact for the model to hold.

  • gofmt has no configuration, and that is the feature. Its answers are debatable; their being settled is the value.

  • The omissions bought local comprehensibility: no invisible control flow, no ambiguous call sites, no operator surprises, no macro indirection.

  • Generics arrived after thirteen years, which shows the readability principle was a real criterion that evidence could move.

  • A Go signature tells you an unusual amount — receiver mutability, visibility, failure modes — and still cannot tell you about concurrency safety, retry semantics or aliasing.

  • The cost is real: roughly more lines for the same behaviour, and error handling that feels tedious before it feels clear.

The most important omission has been mentioned three times without being examined: there is no inheritance, and the space it would occupy belongs to interfaces. Before that makes sense we need to be precise about what a value is in this language, because Go's copying rules are where most newcomers' first real bug lives. The next chapter is Values, Pointers & Memory.

Editorial reflection

What I find most instructive about Go is that it is a language built by people optimising for a situation most language designers do not centre: the engineer who did not write this, does not know the author, and needs to change it safely this afternoon. Almost every language is pitched to the person writing new code — the demos are greenfield, the tutorials build something from scratch, the features that get advertised are the ones that make the first hundred lines shorter. Go was designed around roughly the thousandth day of a codebase's life, and that explains the parts that feel unrewarding early on. Verbose error handling is a tax on writing and a gift to reading. A formatter with no options is an insult to preference and a subsidy for review. What I take from it is less about Go than about how to evaluate any tool: ask which moment in a project's life it was optimised for, and whether that is the moment you are actually in. A team of two building a prototype next week and a team of forty maintaining a service for six years are not solving the same problem, and a tool that is genuinely excellent for one can be genuinely wrong for the other. Go is unusually honest about which one it picked.

Q&A · 0

Enrol to ask questions and join the discussion.

No questions yet — be the first to ask.