Lesson 13 · Errors & idiomatic Go
Idioms: defer, options & named returns
The small patterns that make Go code look like Go.
Your win: read and apply the four idioms you'll meet on every page of
our repo — defer, functional options, named returns, and zero-value design
— so your code reviews (and interview code) read as fluent Go.
1 · defer — cleanup that can't be forgotten
A deferred call runs when the surrounding function returns, in
LIFO order.1 It pairs an
acquire with its release right next to each other, so no early return can skip the
cleanup:
ctx, span := interceptors.StartSpan(ctx, ...); defer span.End()
ctx, cancel := context.WithTimeout(ctx, t); defer cancel()
result := db.SendBatch(...); defer result.Close()
Gotcha: a defer's arguments are evaluated when
deferred, not when it runs. And a defer inside a for loop
doesn't fire until the function returns (a real pattern to watch in long-lived loops —
task_runner.go:161).
2 · Functional options — extensible optional config
Go has no default arguments. The idiom for optional settings is a variadic list of option functions — you can add new options later without breaking any existing call.
internal/golibs/kafka/consumer_option.go:183,203kafka.Consume(topic, group, kafka.Option{
KafkaConsumerOptions: []kafka.KafkaConsumerOption{
kafka.AlwaysCommit(), // each option is a func that mutates config
kafka.Instances(2), // and can validate, returning an error
},
}, handler)
3 · Named returns — clearer signatures, defer-friendly
You can name a function's results. It documents what each return means, allows a
naked return, and lets a deferred function inspect or modify the result.
func (e *Email) FieldMap() (fields []string, values []interface{}) { ... }
func (c *strictConsumer) process(...) (isContinueToNextMsg bool, err error) { ... }
Use sparingly — named returns on long functions with naked
returns can hurt readability. Our repo uses them where the names genuinely
clarify (like (fields, values)).
4 · Zero-value design & "accept interfaces, return structs"
Two idioms you already met, worth naming as idioms: design types so the
zero value works (Lesson 2 — our stateless EmailRepo struct{}),
and accept interfaces, return concrete structs (Lesson 6 —
New...() *T constructors). Together they're why Go code needs so little
ceremony to wire up.
database.ExecInTx(ctx, db, func(ctx, tx) error { ... })
(create_email_handler.go:42). Passing behaviour as a
function value is everywhere in idiomatic Go — options, ExecInTx,
sliceutils.Map's mapper.
Effective Go (defer) + the Uber Go Style Guide
Effective Go and the "Defer, Panic, and Recover" post explain defer
precisely; the Uber guide codifies functional options, named returns, and the rest as
Do/Don't rules close to our repo's style.
Check yourself (from memory)
Q1. A deferred call runs…
defer it.
Q2. Functional options (e.g. AlwaysCommit()) exist to…
Q3. defer cancel() right after context.WithTimeout ensures…
cancel frees the context's resources on
every return path — not calling it leaks. (Context is Part 4.)
defer for cleanup (span.End,
cancel, Close) → released no matter which path returns.
(2) Functional options (AlwaysCommit(),
Instances(n)) → optional, extensible config without breaking signatures.
(3) Named returns ((fields, values), (bool, err))
→ clearer signatures, defer can adjust the result. (Plus: zero-value-useful,
accept-interfaces-return-structs.)1. The Go Blog — Defer, Panic, and Recover; Effective Go — defer.