← Code Like Go

The Non-Negotiables

what Go bakes in so you cannot argue about it

Part 1 was about what Go removed. This part is about what removal left standing. Take away the knobs and you do not get chaos — you get a small set of defaults that win every code review before it starts. These are not style suggestions. They are baked into the toolchain, the type system, and the standard library. You do not vote on them. That is the point: every settled argument is an argument you never have again.

A non-negotiable is a removed debate. Each one deletes a whole class of bug or bikeshed. Remove to accelerate.

1. gofmt — one format, zero bikeshedding

There is exactly one way to format Go, and a tool enforces it. Tabs, not spaces. Braces here, not there. No options, no config file, no .editorconfig war in the PR. You run gofmt and the question is closed.

// you type this, badly
func add(a int,b int)int{return a+b}

// gofmt rewrites it to the one true form
func add(a int, b int) int { return a + b }

What it removes: every "where do the braces go" thread, every diff polluted by reindentation, every reviewer who reviews whitespace instead of logic. The format stopped being a matter of taste, so it stopped being a matter at all. Boring code beats clever code — and identically-boring code is reviewable code.

2. Errors are values — no hidden control flow

Go has no exceptions. An error is an ordinary value you return and check, in line, where it happens. There is no invisible second exit from a function, no stack-unwinding you can't see at the call site.

f, err := os.Open(name)
if err != nil {
    return nil, fmt.Errorf("open %s: %w", name, err)
}
defer f.Close()

What it removes: the entire category of "where did this throw, and who catches it." Control flow is on the page. You cannot accidentally skip an error — the unused variable nags you, the linter nags you, the reviewer sees the bare err sitting there unchecked. The verbosity people complain about is the feature: every failure path is visible and local.

3. Composition over inheritance

No classes, no extends, no superclass to chase up four files. You build big things by embedding small things. A struct holds what it needs; behavior comes from the pieces, not from an ancestor.

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

type Server struct {
    Logger              // embedded — Server now has Log()
    addr string
}

What it removes: the fragile base class, the diamond problem, the "open the parent to understand the child" tax. There is no hidden override resolving at runtime. What a type does is the sum of what it visibly holds. Flat beats deep.

4. Small interfaces — accept interfaces, return structs

The best Go interfaces have one or two methods. io.Reader is one method and half the standard library composes around it. You accept the smallest interface that does the job, and you return a concrete struct so the caller keeps all the type information.

// one method — anything that can Read satisfies it
type Reader interface {
    Read(p []byte) (n int, err error)
}

// accept the interface, return the concrete type
func New(r io.Reader) *Scanner { ... }

What it removes: the speculative mega-interface nobody fully implements, and the tight coupling of demanding a concrete type you don't need. Interfaces are discovered at the consumer, not declared up front by the producer — so a type satisfies an interface just by having the methods, no implements keyword, no import. Define the need where you use it; keep it tiny.

5. The useful zero value

Every type in Go has a zero value, and good types make it work without a constructor. A var declaration alone gives you something usable. No null-then-init dance, no "did you call .Build()" footgun.

var buf bytes.Buffer       // zero value, ready to use
buf.WriteString("hi")      // works, no New() needed

var mu sync.Mutex          // zero value is an unlocked mutex
mu.Lock()

What it removes: a whole tier of "uninitialized" bugs and the ceremony that tries to prevent them. The empty struct is the working struct. Design your types so the zero value is the sensible default and half your constructors disappear.

6. Clear is better than clever

This is a literal Go proverb, and it outranks every other instinct. Given two ways to write something, write the one the next person reads without stopping. No cleverness you'd have to explain in a comment that starts with "basically."

// clever
ok := m[k] != nil && len(m[k]) > 0 && m[k][0].valid

// clear
v, found := m[k]
if !found || len(v) == 0 {
    return errNotFound
}
first := v[0]

What it removes: the maintenance debt of code only its author understood, on the day they wrote it. Clever is a loan against future reading. Clear is paid in full now. The language nudges you here too — no ternary, no operator overloading, no macro magic — because the removed shortcut was always the clever one.

7. CSP concurrency — share by communicating

Go's slogan: do not communicate by sharing memory; share memory by communicating. Goroutines are cheap. Channels pass ownership of data from one to the next, so only one goroutine touches it at a time — and the handoff is the synchronization.

jobs := make(chan int)
results := make(chan int)

go func() {
    for j := range jobs {
        results <- j * 2     // own j here, send it on
    }
}()

What it removes: most of the lock-ordering, race-condition, who-holds- the-mutex misery of shared-state concurrency. You still can reach for a sync.Mutex when it's genuinely simpler — Go isn't dogmatic — but the default path routes data through channels, and go test -race catches you when you stray. The model is small enough to hold in your head, which is the only model worth having.

The pattern under the patterns

Read them together and one shape repeats: each non-negotiable closes a door so you stop walking into it. gofmt closes the formatting argument. Errors-as-values close the hidden-exit argument. Composition closes the inheritance argument. Small interfaces close the coupling argument. Zero values close the init argument. Clear-over-clever closes the taste argument. CSP closes the shared-state argument. Seven settled questions, seven debates you never have, seven classes of bug that can't be born.

That is the whole method, and it generalizes past Go. When you set a default and remove the alternatives, you are not limiting yourself — you are buying back the attention those choices used to cost. If you do one thing: write code the way you'd write Go. Pick the boring default, delete the knob, and move on.

Part 1 — Subtraction  ·  Code Like Go