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.
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.
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.
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.
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.
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.
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.
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.
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.