When writing or modifying Go code, follow these rules based on the Uber Go Style Guide.
Guidelines
Interfaces
- Never use pointers to interfaces; pass interfaces as values
- Verify interface compliance at compile time:
var _ http.Handler = (*Handler)(nil)
Receivers
- Value receivers can be called on pointers and values; pointer receivers only on pointers or addressable values
Mutexes
- Zero-value
sync.Mutexandsync.RWMutexare valid; don't usenew(sync.Mutex) - Don't embed mutexes in structs; use a named field
mu sync.Mutex
Slices and Maps at Boundaries
- Copy slices and maps received as arguments if you store them (prevent caller mutation)
- Copy slices and maps before returning them if they expose internal state
Defer
- Use
deferto clean up resources (files, locks). The overhead is negligible
Channels
- Channel size should be one or unbuffered (zero). Any other size requires strong justification
Enums
- Start enums at one with
iota + 1unless zero value is a meaningful default
Time
- Use
time.Timefor instants,time.Durationfor periods - Include unit in field names when
time.Durationcan't be used:IntervalMillis - Use RFC 3339 for timestamp strings
Errors
- Use
errors.Newfor static errors,fmt.Errorffor dynamic - Export error vars (
ErrFoo) for caller matching viaerrors.Is; use custom types withErrorsuffix forerrors.As - Wrap errors with
%w(caller can match) or%v(opaque); avoid "failed to" prefixes: use"new store: %w"not"failed to create new store: %w" - Prefix exported error vars with
Err, unexported witherr - Handle errors once: either wrap and return, or log and degrade gracefully; never log AND return
Type Assertions
- Always use the "comma ok" form:
t, ok := i.(string)
Panics
- Don't panic in production code; return errors instead
- In tests, use
t.Fatalnotpanic - Only acceptable for program initialization:
template.Must(...)
Atomics
- Prefer
go.uber.org/atomictypes (atomic.Bool, etc.) over rawsync/atomicfor type safety
Mutable Globals
- Avoid mutable globals; use dependency injection instead
Embedding Types
- Don't embed types in public structs; delegate methods explicitly
- Embedding leaks implementation details and inhibits type evolution
Built-In Names
- Never shadow predeclared identifiers (
error,string,len,cap, etc.)
init()
- Avoid
init(). Make it deterministic, no I/O, no global state mutation - Prefer
var _defaultFoo = defaultFoo()or initialization inmain()
Exit
- Call
os.Exitorlog.Fatalonly inmain(); all other functions return errors - Prefer a single
run() errorfunction called frommain()for testability
Field Tags
- Always use field tags in marshaled structs:
json:"price"
Goroutines
- Don't fire-and-forget goroutines; every goroutine must have a way to stop and be waited on
- Use
sync.WaitGroupfor multiple goroutines, adonechannel for single ones - No goroutines in
init(); expose objects withShutdown()/Close()methods
Performance
- Use
strconvoverfmtfor primitive-to-string conversion - Don't repeatedly convert fixed strings to
[]byte; do it once - Specify capacity for slices:
make([]T, 0, size)and maps:make(map[K]V, size)
Style
Formatting
- Soft line length limit of 99 characters
- Be consistent above all else
Declarations
- Group similar declarations (
const,var,type) but only group related items - Two import groups: standard library, then everything else (separated by blank line)
Naming
- Package names: lowercase, no underscores, short, not plural, not "common/util/shared/lib"
- Function names: MixedCaps; test functions may use underscores for grouping
- Import aliases only when package name doesn't match last path element or on conflict
- Prefix unexported globals with
_(except error vars which useerrprefix)
Functions
- Sort functions by rough call order; group by receiver
- Exported functions first, after struct/const/var;
NewXYZ()right after type definition - Utility functions at end of file
Control Flow
- Reduce nesting: handle errors/special cases first, return early
- Eliminate unnecessary else: use default value + conditional override
- Reduce variable scope:
if err := doThing(); err != nil
Variables
- Use
:=for local variables with explicit values - Use
varfor zero-value declarations and empty slices nilis a valid slice; returnnilnot[]int{}; check emptiness withlen(s) == 0
Parameters
- Avoid naked bool parameters; use C-style comments or custom types for clarity
Strings
- Use raw string literals to avoid escaping:
`unknown error:"test"`
Structs
- Use field names in struct initialization (enforced by
go vet) - Omit zero-value fields unless they provide meaningful context
- Use
var s MyStructfor zero-value structs, nots := MyStruct{} - Use
&T{Name: "bar"}notnew(T)for struct references - Place embedded types at top of field list, separated by blank line
Maps
- Use
make(map[K]V)for empty/programmatic maps; use literals for fixed sets - Provide capacity hints when size is known
Printf
- Declare format strings as
constforgo vetanalysis - Name Printf-style functions with
fsuffix:Wrapf, notWrap
Patterns
Test Tables
- Use table-driven tests with subtests for repetitive test logic
- Name the slice
tests, each casett, usegive/wantprefixes - Avoid complex conditional logic in table tests; split into separate test functions instead
- For parallel table tests, ensure loop variables are scoped correctly
Functional Options
- Use the functional options pattern for constructors with 3+ parameters
- Implement with an
Optioninterface and unexportedoptionsstruct - Prefer concrete types over closures for debuggability and testability
Linting
- Run at minimum: errcheck, goimports, revive, govet, staticcheck
- Use golangci-lint as the lint runner
- Lint consistently across the entire codebase