← Code Like Go

The Non-Negotiables

part 2 · what Go bakes in so the argument never starts

⚠ warning: slop currently. Work in progress — not final.

Part 1 was about what Go removed. This part is what stayed standing after the cut. Remove the knobs and the side paths, and a few defaults remain. They win before review starts. These are not style tips. They live in the toolchain, the type system, and the standard library. You do not vote on them. A settled argument stays dead.

In one line
A non-negotiable is a dead debate. Each one erases a whole class of bug or bikeshed. Remove to accelerate.

1. gofmt — one format, zero bikeshedding

There is one way to format Go, and the tool does it. Tabs, not spaces. Braces here, not there. No options, no config file, no .editorconfig war in the PR. Run gofmt. The matter 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 smeared with reindentation, every reviewer staring at whitespace instead of logic. Format stopped being taste, so it stopped being anything. Boring code beats clever code. Identically boring code is easy to review.

2. Errors are values — no hidden control flow

Go has no exceptions. An error is a value you return and check where it happens. No invisible second exit from a function. No stack unwinding you have to guess at from 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 whole "where did this throw, and who catches it" mess. Control flow stays on the page. You do not quietly skip an error because the unused variable nags you, the linter nags you, and the reviewer sees the bare err sitting there. The verbosity people complain about is the point: failure paths are visible and local.

3. Composition over inheritance

No classes, no extends, no superclass hiding four files up. You build bigger things from smaller things. A struct holds what it needs; behavior comes from the pieces, not an ancestor ghost.

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 mess, the "open the parent to understand the child" tax. No hidden override resolution 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 good Go interfaces are tiny. io.Reader is one method, and half the standard library moves around it. Accept the smallest interface that does the job. Return a concrete struct so the caller keeps the full type.

// 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 really implements, and the coupling of demanding a concrete type you do not need. Interfaces are discovered at the consumer, not declared up front by the producer. A type satisfies one by having the methods. No implements keyword. No import. Define the need where you use it. Keep it small.

5. The useful zero value

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

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 layer of "uninitialized" bugs and the ceremony built to dodge them. The empty struct is the working struct. Make the zero value the sane default and half your constructors vanish.

6. Clear is better than clever

This is a literal Go proverb, and it outranks the rest. Given two ways to write something, write the one the next reader can pass through without stopping. No clever trick that needs a comment beginning 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, briefly, on one afternoon. Clever is a loan against future reading. Clear is paid now. The language pushes you there too: no ternary, no operator overloading, no macro tricks. The removed shortcut was usually 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 place to the next, so one goroutine touches it at a time. 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: a lot of the lock ordering, race condition, who-holds-the-mutex misery of shared state. You still can reach for a sync.Mutex when it is plainly simpler. Go is not trying to be pure. But the default path moves data through channels, and go test -race catches you when you drift. The model fits in your head. If it does not fit there, it is already too big.

The pattern under the patterns

Read them together and the shape repeats: each non-negotiable closes a door. 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 dead debates. Each closed door is one fewer way to go wrong, and one fewer thing to decide.

That is the method, and it works past Go. Set the default. Remove the alternatives. Buy back the attention those choices used to eat. Minimalism starts by adopting the Go principles: pick the boring default, delete the knob, move. Remove to accelerate.

Next → Habits That Travel: the same discipline carried into code that isn't Go.

Part 1 — Subtraction  ·  Code Like Go