Go has a strong house style that experienced practitioners converge on. Some of it is enforced by gofmt and golint. The rest is transmitted through code reviews, the standard library, and writing enough Go to feel the natural grain of the language.
After several years of Go, here are the patterns that mark idiomatic code and why they work.
Errors Are Values, Not Exceptions
The single biggest source of unidiomatic Go is developers from exception-based languages writing exception-style Go. The patterns to avoid:
| |
Idiomatic Go handles errors at each call site, with explicit decisions about what to do when things go wrong:
| |
The %w verb wraps the error so callers can use errors.Is and errors.As to inspect the error chain. The format string adds the context that distinguishes this error from other errors in the logs.
Rule: wrap errors with context when returning them. Don’t use the same format at every layer — one meaningful context per layer is enough.
Accept Interfaces, Return Concrete Types
This is one of the most cited Go guidelines and one of the most misunderstood.
| |
Returning interfaces hides the concrete type from callers, preventing them from accessing methods the concrete type has that the interface doesn’t. Returning concrete types maximises what callers can do.
Accepting interfaces (the minimum interface required) keeps functions general without over-specifying the concrete type required.
The corollary: define interfaces at the point of use, not at the point of definition.
| |
Small, consumer-defined interfaces are one of Go’s strongest patterns. They compose naturally and decouple implementations from consumers.
Table-Driven Tests
Go’s standard testing package, combined with subtests, makes table-driven tests the natural pattern:
| |
The benefits: adding test cases is one line, test names are explicit, subtests (t.Run) run independently so one failure doesn’t prevent others from running, and go test -run TestParseAmount/valid lets you run a specific case.
Named Return Values: Sparingly
Named return values (func f() (result int, err error)) are useful in exactly one case: when a deferred function needs to modify the return value.
| |
Everywhere else, named returns add noise without benefit. They encourage return with no arguments (“naked return”), which is confusing in any function longer than a few lines. Don’t use them unless the defer pattern above requires it.
Struct Initialisation: Always Name Fields
| |
Positional struct initialisation breaks silently when fields are reordered or a field is added. Named field initialisation is resilient to these changes and self-documents the code.
Context Is First, Error Is Last
Function signatures in Go have a conventional parameter order:
| |
This is a convention strong enough that linters enforce it. Following it makes code readable without thinking about it.
Don’t Use init() Except for Registration
init() functions run at package load time, in an order that isn’t always predictable. They make programs harder to test and reason about.
The one legitimate use: registering drivers, codecs, or plugins.
| |
Everything else that happens in init() is usually better as explicit initialisation in main() or as lazy initialisation at first use.
Short Variable Names for Small Scopes
Go convention uses short variable names in small scopes:
| |
Longer names for longer scopes:
| |
The Go standard library uses this consistently. Fighting it produces code that looks foreign to Go readers.
Idiomatic Go is the accumulated judgment of a language designed to be read more than it’s written. The patterns above aren’t arbitrary — each reflects a decision about what makes code clear, testable, and maintainable. The fastest way to internalise them is to read the standard library source and accept code review feedback without defending the pattern you brought from another language.