Idiomatic Go
Names scale with scope
The greater the distance between declaration and use, the longer the name. Short names inside short scopes are correct Go, not laziness.
// Good — tight scope, short names
for i, r := range records {
if r.Total > max { max = r.Total }
}
// Good — package-level, long enough to stand alone
const defaultDialTimeout = 30 * time.Second
// Bad — ceremony inside a two-line loop
for recordIndex, currentRecord := range records { ... }
Receivers get one or two letters, consistent across every method on the type: func (s *Server) Start(), never func (this *Server) or func (server *Server).
No stutter
The package name is part of every identifier a caller reads. Do not repeat it.
// Bad — callers write http.HTTPServer, bytes.BytesBuffer
package http
type HTTPServer struct{}
// Good — callers write http.Server, bytes.Buffer
package http
type Server struct{}
Same rule for functions: user.NewUser() should be user.New(). Package names are short, lowercase, single words, no underscores, no plurals: store, not stores or store_utils.
Make the zero value useful
A struct should be usable without a constructor wherever possible.
// Good — var buf bytes.Buffer works immediately
var mu sync.Mutex
var buf bytes.Buffer
// Good — zero value is a ready cache
type Cache struct {
mu sync.Mutex
m map[string][]byte // lazily initialised on first write
}
Reach for New… only when construction has real requirements. If New only sets fields the caller could set, delete it.
Guard clauses, not nesting
Handle the error and return. The happy path stays at minimum indentation, flush left.
// Good
func load(path string) (*Config, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open config: %w", err)
}
defer f.Close()
var c Config
if err := json.NewDecoder(f).Decode(&c); err != nil {
return nil, fmt.Errorf("decode config: %w", err)
}
return &c, nil
}
If you find yourself writing else after a block that returns, delete the else.
defer goes next to the acquisition
Place defer on the line after the resource is acquired and the error is checked — never before the check, never at the end of the function.
f, err := os.Open(path)
if err != nil { return err }
defer f.Close()
defer runs at function exit, not block exit. Inside a loop, either extract the body into a function or close explicitly.
Composition, not inheritance
Go has embedding, not subclassing. Embed to reuse behaviour; embed an interface to satisfy part of it.
type Handler struct {
*log.Logger // Handler gets Printf, Println, …
store Store
}
Prefer a plain field over embedding when you do not want the inner type's methods on your public surface.
Files read top-down
Put the exported entry points first and the helpers they call below, in call order. A reader opening the file meets the package's purpose before its plumbing, and descends one level of abstraction at a time.
// Good — the story first
func Export(orders []Order) string { ... }
func serialiseRow(o Order) string { ... }
func escapeQuotes(s string) string { ... }
Types go above the methods on them, constructors directly under their type. A function mixing orchestration with byte-level detail belongs at two levels at once and cannot be placed — split it.
Accept the standard shapes
- Return
(T, error), error last, always checked. - Take
ctx context.Contextas the first parameter of anything that blocks, does I/O, or spawns work. - Implement
String() stringfor types that get logged;fmt.Stringeris free readability. - Use
anyoverinterface{}, and reach for generics only when the alternative is copy-pasting the same function per type.
Doc comments
Comments on exported identifiers start with the identifier's name and form a full sentence.
// Store persists orders. A Store is safe for concurrent use.
type Store struct{ ... }
// Get returns the order with the given ID, or ErrNotFound if none exists.
func (s *Store) Get(ctx context.Context, id string) (*Order, error)
Comment on why, never on what the line already says. Delete commented-out code — the version history has it.
Duplication is cheaper than the wrong abstraction
A little copying is better than a little dependency. Two similar functions in different packages are fine; do not couple them to share six lines.
Non-negotiables
gofmt decides formatting — never argue with it, never hand-align. Run go vet and golangci-lint before review. Handle every error or explicitly discard with _ and a reason.