Back to Notes

Go Proverbs

Overview

A collection of wise, pithy design principles from Rob Pike — one of Go's creators. Presented at Gopherfest 2015, they capture the philosophy behind idiomatic Go. Similar in spirit to the Zen of Python.

Watch: Go Proverbs — Rob Pike, Gopherfest 2015


The Proverbs

Don't communicate by sharing memory, share memory by communicating.

Instead of letting goroutines read/write a shared variable (and reaching for a mutex), pass the data through a channel — transfer ownership of the value itself. Channels make data flow explicit; shared memory makes it invisible.


Concurrency is not parallelism.

Concurrency is structuring a program as independently executing pieces. Parallelism is executing multiple things at the same time on multiple CPUs. A concurrent program may run on one core; a parallel program requires multiple. Go makes concurrency easy — parallelism is a hardware concern.


Channels orchestrate; mutexes serialize.

Channels are for coordinating goroutines — who does what, when, in what order. Mutexes are for protecting a shared resource from simultaneous access. If you find yourself reaching for a mutex to coordinate flow, consider a channel instead.


The bigger the interface, the weaker the abstraction.

A 10-method interface is hard to satisfy and hard to reason about. A 1-method interface (io.Reader, fmt.Stringer) is easy to implement, easy to mock, and composes well. Keep interfaces small — ideally one or two methods.


Make the zero value useful.

A Go type's zero value (what you get from var x T) should be ready to use without explicit initialisation. sync.Mutex works out of the box. bytes.Buffer works out of the box. Design your own types this way — it eliminates a whole class of "forgot to initialise" bugs.


interface{} says nothing.

An empty interface imposes no constraints and conveys no information about what you can do with the value. Prefer a typed interface with a meaningful method. If you find yourself using interface{} (or any) everywhere, you're probably losing the value of Go's type system.


Gofmt's style is no one's favourite, yet gofmt is everyone's favourite.

No one agrees on the perfect Go formatting style. But everyone benefits from having one enforced style. gofmt ends style debates, makes diffs cleaner, and means every Go codebase looks familiar. Stop arguing about tabs vs spaces — gofmt already decided.


A little copying is better than a little dependency.

Adding a dependency for one small utility brings along its entire surface area — versioning, security, maintenance. If you only need 10 lines from a package, copy those 10 lines. Reserve dependencies for substantial, battle-tested libraries.


Syscall must always be guarded with build tags.

Syscalls are OS-specific. Code that calls them directly must be gated with build constraints (//go:build linux) to prevent it from being compiled on the wrong platform. Forgetting this is a common cause of cross-platform build failures.


Cgo must always be guarded with build tags.

Same reasoning as syscalls, but stricter — Cgo crosses the language boundary into C, which brings additional complexity: longer compile times, no goroutine scheduler, CGO_ENABLED requirements. Always isolate it behind build tags.


Cgo is not Go.

When you call C from Go via Cgo, you leave Go's safety guarantees behind — no garbage collector oversight, no goroutine scheduling, manual memory management risks. Use Cgo only when you have no alternative; never treat it as just "another way to write Go."


With the unsafe package there are no guarantees.

unsafe bypasses Go's type system and memory safety. Code using it may break silently across Go versions. The compiler gives you no help. It exists for rare, low-level use cases (interop, performance-critical data structures) — not for everyday use.


Clear is better than clever.

A clever trick that saves 3 lines but takes 10 minutes to understand is a net loss. Go code should read like prose. Optimise for the reader, not the writer. Future-you (and your colleagues) will thank you.


Reflection is never clear.

reflect is powerful but produces code that the compiler can't check, IDEs can't navigate, and humans struggle to read. It's the right tool for serialisation libraries and frameworks — not for application code. When you reach for reflection, ask whether a generic or interface-based approach would be clearer.


Errors are values.

In Go, errors are just values — they can be stored, passed, compared, and wrapped. There's no special exception mechanism. This means error handling is explicit and composable. Don't treat error as a nuisance; treat it as data.


Don't just check errors, handle them gracefully.

if err != nil { return err } checks an error. Handling it means adding context (fmt.Errorf("opening config: %w", err)), logging it at the right level, recovering where possible, or failing fast with a clear message. Bubbling a bare error up 10 layers helps no one.


Design the architecture, name the components, document the details.

Work top-down: first decide the big structure, then name the parts clearly (package names, type names, function names), then document the non-obvious details. Don't skip to implementation before the structure is clear.


Documentation is for users.

Write godoc comments for the people who will use your package — not for the people who will read its source. Explain what something does and when to use it, not how it works internally. Internal implementation details belong in inline comments, not the exported API docs.


Don't panic.

panic is for programmer errors — bugs, invariants that should never be violated, truly unrecoverable states. For expected failure conditions (bad input, network errors, missing files) return an error. A library that panics on bad input forces callers to use recover, which is far harder to work with than an error value.