Control Flow Best Practices
Readable flow-control habits and when to refactor nested logic.
Search across all documentation pages
Readable flow-control habits and when to refactor nested logic.
These rules keep Go code flat, predictable, and safe under concurrency - from if err != nil discipline to panic boundaries in servers.
if chains or loop-heavy handlers land in a PR.go vet, staticcheck, and golangci-lint to catch suspicious control flow automatically.if. if err := f(); err != nil { return err } beats nested success paths.switch for multi-value discrimination on one expression. Long else if chains on the same variable signal a switch.fallthrough unless you can name the reason in a comment. Shared case bodies should call a helper function instead.panic for expected request or I/O failures. Return error and let callers map to HTTP/gRPC status.for range for slices, maps, channels, and strings. Use index loops when you need neighbors or reverse iteration.s[i] = x updates the collection; the range value is a copy.for range on channels deadlocks when nobody closes.break label when exiting nested search loops. Replace found boolean pyramids with a label or helper return.break out of for { select } loops. Bare break only exits select, not the loop.defer immediately after successful resource acquisition. Readers pair Open/Lock with defer Close/Unlock visually.defer inside tight loops without a nested function. Defer stacks grow until the outer function returns.Recoverer, gin Recovery, echo Recover, or hand-written defer recover.return after writing responses. Never call next after you have finished the response.r.Context() into downstream I/O. Timeout middleware only works when handlers respect cancellation.r.Body.Close() (or equivalent) per request. Leaked bodies exhaust connections under load.Rarely more than two levels in application code.
Extract functions or use guard clauses when deeper.
When a tight double loop would pay for an extra call and the label names the exit target clearly.
If logic grows past that, extract.
Yes for function-scoped locks - defer mu.Unlock() after Lock() survives panics.
For hot paths inside loops, hoist locking outward.
Tagless switch { case cond: is the modern spelling.
Same semantics, clearer intent.
No - one recovery middleware wrapping the tree is enough.
Duplicate layers are redundant, not harmful.
When the body exceeds one screen, needs multiple exits, or repeats in tests.
Named functions improve stack traces and unit testing.
Almost never in application code.
Prefer labels, helpers, or data-driven dispatch tables.
Deleting the current key during iteration is defined.
Keep delete logic simple; extract keys first if unsure.
Prefer errors for callers who can react.
Reserve panic for documented misuse that should fail in development.
staticcheck, govet, gosimple, and revive flag suspicious constructs.
Run via golangci-lint in CI.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 18, 2026