AnyLearn
All lessons
Programmingadvanced

The Design Philosophy: What Go Left Out

Go is unusual for what it refuses to include. Errors are values, not exceptions. Interfaces are satisfied without declaring it. Formatting is not a choice. This lesson works through the omissions, the reasoning behind each, and the real costs, because every one of them trades expressiveness for something else.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 8

A language optimised for reading

Most language design optimises for what a programmer can express. Go optimises for something else: what a different programmer can understand a year later, in a large codebase, without context.

That objective explains choices that look like omissions and are decisions.

The reasoning is about where time goes on a large team. Code is written once and read many times, by people who did not write it, often while something is broken. A feature that saves the author five minutes and costs each subsequent reader thirty seconds is a bad trade at scale, even though it is a good trade for the author.

So Go systematically prefers explicit and verbose over concise and clever, and the resulting code is often described as boring. That is the intent rather than an accident.

Whether it is the right objective is arguable and depends on the setting. What is worth understanding is that each omission below was a considered trade, and each has a real cost the enthusiasm tends to skip.

Full lesson text

All 8 steps on one page, for reading, reference, and search.

Show

1. A language optimised for reading

Most language design optimises for what a programmer can express. Go optimises for something else: what a different programmer can understand a year later, in a large codebase, without context.

That objective explains choices that look like omissions and are decisions.

The reasoning is about where time goes on a large team. Code is written once and read many times, by people who did not write it, often while something is broken. A feature that saves the author five minutes and costs each subsequent reader thirty seconds is a bad trade at scale, even though it is a good trade for the author.

So Go systematically prefers explicit and verbose over concise and clever, and the resulting code is often described as boring. That is the intent rather than an accident.

Whether it is the right objective is arguable and depends on the setting. What is worth understanding is that each omission below was a considered trade, and each has a real cost the enthusiasm tends to skip.

2. Errors are values

Go has no exceptions. A function that can fail returns an error alongside its result, and the caller checks it.

f, err := os.Open(path)
if err != nil {
    return fmt.Errorf("opening config: %w", err)
}
defer f.Close()

That three-line pattern is the most criticised thing about the language, and the argument for it is specific.

With exceptions, every call is a potential branch out of the function, and the branches are invisible. Reading a function does not tell you where control can leave it, which is a real problem in code that must release resources or maintain invariants.

With error values, control flow is visible. Every place the function can return is written down, and a caller ignoring an error has done so on a line you can see in review.

The cost is real. The pattern is verbose, it dominates the visual weight of the code, and it makes the happy path harder to pick out. The %w verb wrapping errors preserves the chain so callers can inspect the cause, which recovers some of what a stack trace would have given.

The deeper trade: exceptions make the common case concise and the exceptional case invisible. Go makes both explicit, and pays for it in every function.

3. Interfaces are satisfied implicitly

A type implements an interface by having the right methods. There is no declaration, and the type does not know the interface exists.

type Writer interface {
    Write(p []byte) (n int, err error)
}
// Any type with that method is a Writer. No inheritance, no registration.

The consequence is that the consumer defines the interface, not the producer. A package needing only one method of a large type declares a one-method interface locally and accepts that. The original author never anticipated it and does not need to.

This inverts the usual dependency direction. In languages with explicit implementation, using a type as an interface requires the type's author to have declared it, so the abstraction must be designed up front, and adapting third-party types means wrappers.

The cultural rule that follows is that interfaces should be small and defined where used. A one-method interface is idiomatic and common; a fifteen-method interface is a design smell.

The cost is discoverability. Nothing in a type's source says which interfaces it satisfies, so finding every implementation of an interface requires tooling rather than reading. That is a genuine loss, traded for the ability to abstract over code you do not control.

4. Composition instead of inheritance

There are no classes and no inheritance. Types are composed by embedding: placing one type inside another promotes its methods to the outer type.

type Logger struct{ prefix string }
func (l Logger) Log(msg string) { fmt.Println(l.prefix, msg) }

type Server struct {
    Logger              // embedded: Server now has Log
    addr string
}

s := Server{Logger{"[srv]"}, ":8080"}
s.Log("started")        // promoted method

This looks like inheritance and differs in one decisive way: there is no subtyping. A Server is not a Logger and cannot be used where one is expected, unless it separately satisfies an interface. The relationship is has-a rather than is-a, even though the call syntax is the same.

What that removes is the fragile base class problem and the deep hierarchies that make behaviour hard to locate. With inheritance, finding what a method does means walking up a chain of overrides. With embedding, the method is on the embedded type, and the chain is one level.

The cost is that genuine polymorphic hierarchies must be expressed through interfaces instead, which is more verbose for cases where inheritance genuinely fits, and there are such cases.

5. What each omission trades

Read the right column as costs rather than benefits, because that is the half usually omitted.

The formatting decision is the most striking. gofmt defines one canonical layout and there is no configuration, so every Go file in the world looks the same. That removes an entire category of team argument and review comment, and it means unfamiliar code has no unfamiliar shape.

The cost is that you do not get to disagree, including when you are right about a particular case. That is the trade accepted deliberately: the value comes from universality, so allowing exceptions would destroy it.

The same reasoning covers the compiler rejecting unused variables and imports as errors rather than warnings. It is infuriating during exploratory work, and it means no Go codebase accumulates the dead imports and abandoned variables that every other language's codebases collect.

The pattern across all four: a small, permanent, individual cost in exchange for a property that holds across an entire ecosystem.

flowchart TD
A["Design objective: readable at scale"] --> B["No exceptions"]
A --> C["No inheritance"]
A --> D["No formatting choices"]
A --> E["Small standard library surface"]
B --> F["Visible control flow, verbose call sites"]
C --> G["Shallow hierarchies, more interfaces"]
D --> H["No style debates, no personal expression"]
E --> I["Fast to learn, more written by hand"]

6. The standard library as a design statement

Go's standard library is unusually capable for infrastructure work: a production-grade HTTP server, TLS, JSON, compression, cryptography, templating and testing, all in the distribution.

The consequence is a dependency graph most ecosystems do not have. A substantial Go service can be written with very few third-party packages, which means fewer supply-chain surfaces, fewer version conflicts, and fewer abandoned libraries.

The design principle visible throughout is small interfaces composed. io.Reader and io.Writer have one method each, and almost everything in the library accepts or returns them, so a file, a network connection, a buffer, a compressor and a hash all interoperate without adapters.

That is a better illustration of the language's philosophy than any list of features. The abstraction is tiny, defined by behaviour rather than hierarchy, and composable precisely because it demands so little.

The honest counterweight: batteries included also means batteries frozen. The compatibility promise that keeps old code working also keeps early design decisions in place, and some standard library APIs show their age with no route to changing them.

7. Where the philosophy costs most

The omissions have edges, and being straightforward about them is more useful than advocacy.

Repetition. Without generics, the language spent a decade copying slice-manipulation code per type or dropping to interface{} and losing type safety. Generics arrived in Go 1.18 and were resisted for years precisely because they conflict with the readability objective: generic code is harder to read, which the design deliberately weights against.

Error handling verbosity. In a function performing eight fallible operations, more than half the lines are error checks. Proposals to shorten it have been debated for years and rejected on the grounds that any shorthand makes the error path less visible, which was the point.

Nil. Nil pointers panic at runtime, and nil interface values have a famously confusing behaviour: an interface holding a nil pointer is not itself nil. The lesson Types as Propositions describes the alternative, where absence is encoded in the type and unhandled cases do not compile. Go did not take it.

Weak enums. Constants and a type give a convention rather than a closed set, so exhaustive matching cannot be checked.

Each is a real cost, and each traces back to the same objective. A language that will not add a feature unless its readability benefit exceeds its complexity cost will end up without features that are genuinely useful.

8. What the path establishes

DecisionBuysCosts
Goroutines over threadsConcurrency cheap enough to stop countingCorrectness problems unchanged
Channels and CSPTransfer with backpressure and cancellation built inOwnership is convention, not enforced
No goroutine killNo unsafe terminationLeaks are yours to prevent
Errors as valuesVisible control flowVerbosity everywhere
Implicit interfacesAbstract over code you do not controlImplementations are not discoverable by reading
Composition onlyShallow, locatable behaviourReal hierarchies get verbose
One formattingNo style debate anywhereNo exceptions, even good ones

Read down the middle column and a single objective appears: make a large codebase, worked on by many people over years, remain comprehensible. Every row serves it, including the ones that are annoying daily.

The comparison with the previous cursus is instructive. Rust makes the compiler reject anything it cannot prove safe, paying in a steep learning curve for guarantees checked mechanically. Go makes the compiler accept almost anything and relies on convention, tooling and the race detector, paying in bugs the compiler could have caught for a language learnable in a week.

Neither is correct in general. They optimise different variables, and the transferable question is the one both answer explicitly: what are you willing to give up, and what do you get for it? A language design that cannot answer that has not made a choice, only an accumulation.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. What is the stated argument for returning errors as values rather than throwing?
    • Control flow stays visible: every place the function can return is written down
    • It is faster, since no stack unwinding is required
    • It allows errors to be ignored safely
    • It removes the need for resource cleanup
  2. What follows from interfaces being satisfied implicitly?
    • Interfaces must be declared before the types that implement them
    • The consumer defines the interface, so you can abstract over types whose authors never anticipated it
    • Every type automatically satisfies every interface with matching names
    • Interfaces can only be declared in the same package as the type
  3. How does embedding differ decisively from inheritance?
    • Embedded methods cannot be overridden
    • Embedding works only for interfaces, not structs
    • Embedded fields are copied rather than referenced
    • There is no subtyping: the outer type cannot be used where the embedded type is expected
  4. Why does `gofmt` offer no configuration?
    • Because parsing is faster with a fixed layout
    • Because the value comes from universality, which allowing exceptions would destroy
    • Because the formatter runs before type checking
    • Because the language grammar is ambiguous otherwise
  5. Why were generics resisted in Go for so long?
    • Because the runtime could not support them without boxing
    • Because interfaces already provided equivalent expressiveness
    • Because generic code is harder to read, which the design objective weights against
    • Because they would have broken the compatibility promise

Related lessons

Programming
advanced

Refs: A Branch Is a File Containing a Hash

Hashes are unusable by hand, so Git names them. A branch is not a copy of anything, not a directory, and not a container for commits: it is a forty-character file. Once that is clear, checkout, reset, detached HEAD and the reflog stop being separate concepts and become one operation on one kind of file.

8 steps·~12 min
Programming
intermediate

The Object Database: Git Is a Content-Addressed Store

Git is usually taught as a set of commands. Underneath it is a key-value store where the key is a hash of the content, and four object types built on that one idea. This lesson derives the hash by hand, shows what a commit actually contains, and explains why the design makes history tamper-evident.

8 steps·~12 min
Programming
advanced

Undo: Reset, Revert, Restore, and Getting Work Back

Git's undo commands are notoriously confusing because they are usually memorised as recipes. Read against the object model they separate cleanly: each acts on a different one of the three places content lives. This lesson maps them, covers recovery from the accidents that feel unrecoverable, and states what genuinely cannot be undone.

8 steps·~12 min
Programming
advanced

Merge and Rebase: Two Answers to Divergence

Every operation so far moved a pointer. Merging is the first that has to decide something: two branches changed the same project and one result must come out. This lesson covers the three-way merge and the merge base it depends on, what a conflict actually is, and why rebase produces different commits rather than moving them.

8 steps·~12 min