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:

internal/spike/.../repositories/email.go:20 · consumer.go:112 · email.go:113
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,203
kafka.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.

email.go:29 · consumer.go:192
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.

Bonus idiom — the tx closure Transactions are run through a closure so commit/rollback is handled for you: 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.
Read this next

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.

go.dev/blog/defer-panic-and-recover
Uber Go Style Guide

Check yourself (from memory)

Q1. A deferred call runs…

At return, last-deferred-first. Its args, though, are evaluated at the moment you defer it.

Q2. Functional options (e.g. AlwaysCommit()) exist to…

Go has no default args; option funcs give optional, validatable config that stays backward-compatible as you add more.

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.)
Name three everyday Go idioms our repo uses and what each buys you.
recall, then click to reveal
(1) 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.)
🎓 That's Part 3 You can now handle errors as values, wrap and inspect them, map them to gRPC codes at the right layer, and write the everyday idioms fluently. Part 4 — Concurrency & context — is the biggest interview pillar, and where our Kafka engine finally makes complete sense.
Ready for Part 4 (concurrency & context), or a mixed quiz across Lessons 10–13 first? Ask me.

1. The Go Blog — Defer, Panic, and Recover; Effective Go — defer.