Idiomatic Go
Go's reputation for simplicity is half real and half discipline. The language IS small. But idiomatic Go has a hundred small conventions — naming, error handling, package layout, interface placement — that take a year to absorb. This page distills the twenty most-frequently-violated ones.
1 · Naming — the short-name rule
Go names get shorter the closer to the use site. Package names are
short (http, not http_client). Local variables are
very short (i, r, err). Exported types
are descriptive (http.Client, os.File).
- Package names: one word, lowercase, singular.
store, notstoresordata_store. - Receivers: 1-2 chars matching the type.
(c *Client),(s *Server). NEVERthisorself. - Errors: end with "Error" or start with "Err".
os.ErrNotExist,type NotFoundError. - Constructors: New + type name.
NewClient,NewServer. Returns the type (or pointer to it). - Don't stutter.
http.HTTPServeris wrong;http.Serveris right. The package name already says HTTP.
2 · Error handling rituals
- Check err first, return early.
if err != nil { return ... }on the line after every fallible call. - Wrap with context.
fmt.Errorf("opening config: %w", err)— never lose information. - Don't
panicin libraries. Return errors. Panic is for "programmer bug" only. - Error messages: lowercase, no trailing punctuation. They compose:
"opening: not found".
3 · Interfaces — accept, return
"Accept interfaces, return structs." A function should accept the smallest interface it needs, but return concrete types. The caller composes; the callee doesn't make decisions about polymorphism.
4 · Concurrency rules
- Every goroutine must have a way to exit. Context cancel, channel close, or a finite task.
- "Start a goroutine when in doubt" is wrong. Start one when you actually need concurrency.
- Channels for communication; mutexes for shared state. Don't channelify a counter.
- The first parameter is usually
ctx context.Context. Every long-running or blocking function.
5 · Documentation comments
// Package store provides a key-value store backed by Redis.
package store
// User represents a logged-in person.
//
// Multiple lines are fine. The first sentence (up to first period)
// is the synopsis shown in package listings.
type User struct {
Name string
Age int
}
// Authenticate verifies the user's credentials against the database.
// It returns ErrInvalidCredentials if the password doesn't match.
//
// This is the function-level doc comment. Goes immediately above the
// declaration. Starts with the function name.
func Authenticate(username, password string) (*User, error) {
// ...
}6 · The twenty patterns at a glance
| # | Rule | Example |
|---|---|---|
| 1 | Use := in functions, var at package scope. | x := 5 vs var pi = 3.14 |
| 2 | Return early; don't nest happy-path inside else. | if err != nil { return err } |
| 3 | Group related fields in structs. | Name, Email string |
| 4 | Method receivers are short (1-2 chars). | (c *Client) |
| 5 | Errors wrap with %w; never reformat with %v. | fmt.Errorf("foo: %w", err) |
| 6 | Test files end with _test.go; same package OR _test suffix. | store_test.go |
| 7 | Use defer for cleanup right after acquisition. | f.Close() deferred |
| 8 | Make zero values useful. | var b bytes.Buffer works |
| 9 | One main() per binary; in cmd/<name>. | cmd/server/main.go |
| 10 | No nil on slice/map zero values for ranging. | var xs []int; for _, x := range xs { ... } |
| 11 | Don't expose sync.Mutex; embed and lock inside methods. | (c *Cache) Get(k) |
| 12 | Channels are bidirectional internally; one-way at API boundaries. | func gen() <-chan int |
| 13 | Context is the first parameter. | func Fetch(ctx context.Context, url string) |
| 14 | Constants over magic numbers. | const MaxRetries = 3 |
| 15 | Prefer slice over map when keys are sequential ints. | []Item not map[int]Item |
| 16 | Tests run independently. | No shared state across TestXxx |
| 17 | Use internal/ aggressively. | Private until proven public |
| 18 | Format errors lowercase, no period. | "not found" not "Not found." |
| 19 | One package per directory. | No multi-package dirs |
| 20 | Run go vet + golangci-lint in CI. | Catches half of beginner bugs |
7 · From the wild — read good Go
- The standard library. The
net/http,io,encoding/json,contextpackages are all under ~3000 LOC each and beautiful examples of idiomatic Go. - etcd. The distributed KV store under Kubernetes. The Raft implementation is the cleanest you'll find anywhere.
- Prometheus. Production monitoring system. Heavy use of interfaces, context, and clean package boundaries.
- grpc-go. Real RPC at scale. Complex but excellent code organisation.
- The Go compiler itself. Written in Go.
src/cmd/compile. Approachable.
8 · The official references
- Google's Go style guide — the most exhaustive set of rules, with rationale.
- Effective Go — the original document from the Go team. Older but still relevant.
- CodeReviewComments — a checklist of issues Go reviewers flag.
- staticcheck — the most thorough static analyser. Integrated into
golangci-lint.
9 · Exercises (~30 min)
- Audit your code. Pick something you've written. Run
gofmt -l .,go vet ./...,golangci-lint run. Fix everything. - Refactor a long function. Find one over 80 lines. Break into helpers. Watch readability climb.
- Replace a global with a struct. Singletons are a smell. Replace
var defaultClient *Clientwith passing it explicitly. - Read 100 lines of grpc-go. Pick any file. Note one idiom you'd not seen. Copy it for your own use.
10 · You've completed the week
Twenty concepts. From go run hello.go to writing production-grade
HTTP services with idiomatic concurrency. You should now be able to:
- Read any Go program and understand it on first scan.
- Write a small HTTP service in 30 lines from memory.
- Spot common bugs (nil maps, slice aliasing, typed-nil errors) in code review.
- Reach for the right concurrency primitive (channel, mutex, atomic) for the situation.
- Test idiomatically with table-driven structure.
- Know where to look in the standard library before reaching for a third-party package.
The week is over; the practice begins. Build something. Ship it. Profile it. Read the standard library. The language gets out of your way; the rest is engineering.