Files
go-patterns/patterns/style.md
T
aweiker eb9171368b docs: add 'when to use' triggers + examples to all patterns
Added 'When to Use' subsections with concrete decision triggers and
before/after Go code examples to patterns across all directories:

- patterns/error-handling.md (3 patterns: sentinels, wrapping, Join)
- patterns/concurrency.md (4 patterns: Mutex, Once, done channels, pipelines)
- patterns/interfaces.md (4 patterns: small interfaces, accept/return, adapter, optional)
- patterns/structs.md (3 patterns: zero-value, constructors, config structs)
- patterns/package-design.md (3 patterns: internal/, init(), context keys)
- patterns/style.md (3 patterns: interface checks, iota constants, named types)
- patterns/testing-advanced.md (3 patterns: table tests, golden files, httptest)
- patterns/api-conventions.md (3 patterns: Must, layered API, graceful shutdown)
- patterns/documentation.md (2 patterns: examples, deprecated)
- kubernetes/patterns.md (3 patterns: controller, workqueue, leader election)
- kubernetes/production-go.md (2 patterns: codegen, HandleCrash)
- smells/anti-patterns.md (2 anti-patterns: cache mutation, edge-triggered)
2026-04-30 12:08:41 +00:00

16 KiB
Raw Blame History

Code Style Patterns in the Go Standard Library

1. Naming Conventions: mixedCaps (No Underscores)

Pattern name: mixedCaps / MixedCaps

Source citation: All stdlib code (enforced by gofmt convention, documented in Effective Go)

What it does: All identifiers use mixedCaps (unexported) or MixedCaps (exported). Underscores are never used in Go names except for test helpers and generated code.

Why: Consistent casing makes code scannable. The exported/unexported distinction is communicated solely through initial capitalization — no separate public/private keywords needed.

Anti-pattern: snake_case names; ALL_CAPS for constants; Hungarian notation (strName, iCount).

Code examples from source:

// net/http/server.go — exported
type Server struct { ... }
func ListenAndServe(addr string, handler Handler) error

// net/http/server.go — unexported
func (s *Server) shuttingDown() bool
const shutdownPollIntervalMax = 500 * time.Millisecond

2. Acronyms Are All-Caps

Pattern name: Acronym Capitalization

Source citation: net/http/request.go line 130 (URL), net/http/server.go line 3041 (TLSConfig), encoding/json/stream.go line 280 (JSON)

What it does: Acronyms and initialisms (URL, HTTP, ID, JSON, XML, HTML, TLS, TCP) are always fully capitalized when exported, and fully lowercased when unexported.

Why: Consistency. URL not Url, ID not Id, HTTP not Http. This applies even mid-word: ServeHTTP, xmlEncoder, htmlEscape.

Anti-pattern: Url, Http, Json, Id — mixing cases within an acronym.

Code examples from source:

// net/http/request.go:130
URL *url.URL

// net/http/request.go:822
func ParseHTTPVersion(vers string) (major, minor int, ok bool)

// net/http/server.go:3041
TLSConfig *tls.Config

// encoding/json/stream.go:280
var _ Marshaler = (*RawMessage)(nil)

3. File Organization by Responsibility

Pattern name: One Concept Per File

Source citation: net/http/ directory structure

What it does: Large packages split code into files by topic/type: client.go, server.go, transport.go, request.go, response.go, cookie.go, header.go, fs.go, doc.go. Each file is focused.

Why: Navigability. When you want to find client logic, you open client.go. Files stay manageable sizes. Related code lives together.

Anti-pattern: One giant file with everything; splitting by access level (public.go / private.go); splitting by method count rather than concept.

File layout from net/http/:

client.go        — Client type and methods
transport.go     — Transport type (low-level RoundTripper)
server.go        — Server, Handler, ServeMux
request.go       — Request type and parsing
response.go      — Response type and reading
cookie.go        — Cookie parsing and serialization
header.go        — Header type and canonicalization
fs.go            — FileServer, file serving
doc.go           — Package documentation
clone.go         — Clone helpers
method.go        — HTTP method constants
pattern.go       — URL pattern matching (ServeMux routing)

4. Blank Identifier for Interface Compliance

Pattern name: var _ Interface = (*Type)(nil)

Source citation: io/io.go line 645, os/file.go lines 747750, encoding/json/stream.go lines 280281

What it does: A package-level var _ InterfaceName = (*ConcreteType)(nil) declares that the concrete type must satisfy the interface. The compiler verifies this at build time.

Why: Catches interface drift at compile time without creating an instance. The blank identifier discards the value — this is purely a static assertion.

When to Use

Triggers:

  • You've defined a type that MUST satisfy an interface (implements io.Reader, http.Handler, etc.)
  • You want a compile-time guarantee that catches drift when you add/remove methods
  • You're writing a package with multiple types implementing the same interface

Example — before:

type MyWriter struct{ buf bytes.Buffer }

func (w *MyWriter) Write(p []byte) (int, error) { return w.buf.Write(p) }

// Months later, someone renames Write to WriteBuf...
// No compile error — only discovered at runtime when passed as io.Writer

Example — after:

// Compile-time check: if MyWriter stops implementing io.Writer, this fails to build
var _ io.Writer = (*MyWriter)(nil)

type MyWriter struct{ buf bytes.Buffer }

func (w *MyWriter) Write(p []byte) (int, error) { return w.buf.Write(p) }

Anti-pattern: Relying on tests to catch interface conformance; skipping the check and discovering the mismatch at runtime; using reflection.

Code examples from source:

// io/io.go:645
var _ ReaderFrom = discard{}

// os/file.go:747-750
var _ fs.StatFS = dirFS("")
var _ fs.ReadFileFS = dirFS("")
var _ fs.ReadDirFS = dirFS("")
var _ fs.ReadLinkFS = dirFS("")

// encoding/json/stream.go:280-281
var _ Marshaler = (*RawMessage)(nil)
var _ Unmarshaler = (*RawMessage)(nil)

// net/http/server.go:4071
var _ Pusher = (*timeoutWriter)(nil)

5. Named Return Values

Pattern name: Named Returns for Documentation (and Defer)

Source citation: io/io.go lines 87, 100, 314, 387; os/file.go lines 140, 175

What it does: Return values are given names when the names add documentary value (clarifying which int is what) or when defer needs to modify the return value.

Why: (n int, err error) is immediately understandable — n is the byte count. Named returns also enable defer func() { err = wrap(err) }() patterns.

Anti-pattern: Naming returns for trivial functions where the types are self-explanatory; using named returns as implicit variables throughout the function body (confusing naked returns); always using naked return statements.

Code examples from source:

// io/io.go:87 — Interface documentation
type Reader interface {
    Read(p []byte) (n int, err error)
}

// io/io.go:100
type Writer interface {
    Write(p []byte) (n int, err error)
}

// io/io.go:387 — Named return used with defer-style logic
func Copy(dst Writer, src Reader) (written int64, err error) {
    return copyBuffer(dst, src, nil)
}

// os/file.go:140 — Named return for readability
func (f *File) Read(b []byte) (n int, err error) {
    if err := f.checkValid("read"); err != nil {
        return 0, err
    }
    n, e := f.read(b)
    return n, f.wrapErr("read", e)
}

6. Defer for Resource Cleanup

Pattern name: defer mu.Unlock() / defer f.Close()

Source citation: net/http/server.go lines 31733174, net/http/example_handle_test.go lines 2122

What it does: Resources acquired at the top of a scope are immediately deferred for cleanup. Mutexes are locked then immediately defer Unlock()'d.

Why: Guarantees cleanup regardless of return path (early returns, panics). Keeps the acquire/release pair visually adjacent. Reduces bugs from forgotten unlocks.

Anti-pattern: Manual unlock at each return point; deferring in a loop (deferred calls accumulate until function exit); deferring expensive operations that should run earlier.

Code examples from source:

// net/http/server.go:3173-3174
func (s *Server) Close() error {
    s.inShutdown.Store(true)
    s.mu.Lock()
    defer s.mu.Unlock()
    // ...
}

// net/http/example_handle_test.go:21-22
func (h *countHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    h.mu.Lock()
    defer h.mu.Unlock()
    h.n++
    fmt.Fprintf(w, "count is %d\n", h.n)
}

7. Error Wrapping and Sentinel Errors

Pattern name: Sentinel Errors + Structured Error Types

Source citation: os/error.go lines 1427, os/error.go lines 4667

What it does: Package-level sentinel errors (ErrNotExist, ErrPermission) are declared as var for use with errors.Is(). Structured error types (*PathError, *SyscallError) carry context and implement Unwrap() for the errors chain.

Why: Enables programmatic error handling without string matching. errors.Is(err, os.ErrNotExist) works regardless of wrapping depth. Structured types let callers extract the operation, path, or underlying syscall error.

Anti-pattern: Comparing error strings; creating unique error types for every possible failure; not implementing Unwrap; sentinel errors as const (breaks errors.Is for wrapped errors — use var).

Code examples from source:

// os/error.go:14-27
var (
    ErrInvalid    = fs.ErrInvalid    // "invalid argument"
    ErrPermission = fs.ErrPermission // "permission denied"
    ErrExist      = fs.ErrExist      // "file already exists"
    ErrNotExist   = fs.ErrNotExist   // "file does not exist"
    ErrClosed     = fs.ErrClosed     // "file already closed"
)

// os/error.go:46
type PathError = fs.PathError

// os/error.go:49-57
type SyscallError struct {
    Syscall string
    Err     error
}

func (e *SyscallError) Error() string { return e.Syscall + ": " + e.Err.Error() }
func (e *SyscallError) Unwrap() error { return e.Err }

8. Receiver Naming: Short, Consistent, Never this/self

Pattern name: Single-Letter or Short Receiver Names

Source citation: All stdlib code; net/http/server.go uses s for Server, bufio/scan.go uses s for Scanner

What it does: Method receivers use 12 letter abbreviations of the type name, consistent across all methods of that type: s for *Server, b for *Builder, f for *File, t for *Timer.

Why: Receivers appear on every method. Short names reduce visual noise. Consistency within a type avoids confusion. this/self are alien to Go's conventions.

Anti-pattern: this, self, me; long receiver names like server, scanner; inconsistent receivers across methods of the same type.

Code examples from source:

// net/http/server.go
func (s *Server) ListenAndServe() error { ... }
func (s *Server) Serve(l net.Listener) error { ... }
func (s *Server) Shutdown(ctx context.Context) error { ... }

// strings/builder.go
func (b *Builder) WriteString(s string) (int, error) { ... }
func (b *Builder) String() string { ... }
func (b *Builder) Grow(n int) { ... }

// os/file.go
func (f *File) Read(b []byte) (n int, err error) { ... }
func (f *File) Name() string { ... }

9. Constants: Typed, Grouped, with iota

Pattern name: Typed Constants with iota

Source citation: crypto/crypto.go lines 7085, time/time.go lines 936943

What it does: Related constants are grouped in a const ( ... ) block using a named type and iota for sequential values. Constants of the same type are exhaustively listed together.

Why: Type safety (can't accidentally pass an os.Flag where a crypto.Hash is expected). iota eliminates magic numbers. Grouping makes the full set visible.

When to Use

Triggers:

  • You have a set of related values that represent distinct states or options (status codes, modes, categories)
  • Raw integers would be meaningless to readers (SetMode(3) vs SetMode(ModeAsync))
  • You want the type system to prevent passing a "color" where a "direction" is expected

Example — before:

func SetLogLevel(level int) { ... }

// Caller:
SetLogLevel(3) // what does 3 mean?!
SetLogLevel(-1) // valid? who knows

Example — after:

type LogLevel int

const (
    LevelDebug LogLevel = iota
    LevelInfo
    LevelWarn
    LevelError
)

func SetLogLevel(level LogLevel) { ... }

// Caller:
SetLogLevel(LevelWarn) // self-documenting

Anti-pattern: Untyped numeric constants; separate const declarations for related values; using raw integers in function signatures.

Code examples from source:

// crypto/crypto.go:70-85
const (
    MD4         Hash = 1 + iota
    MD5
    SHA1
    SHA224
    SHA256
    // ...
)

// time/time.go:936-943
const (
    Nanosecond  Duration = 1
    Microsecond          = 1000 * Nanosecond
    Millisecond          = 1000 * Microsecond
    Second               = 1000 * Millisecond
    Minute               = 60 * Second
    Hour                 = 60 * Minute
)

10. Comments: Guard Clauses Over Conditions

Pattern name: // guards x Field Comments

Source citation: net/http/example_handle_test.go line 16

What it does: When a sync primitive (mutex) protects specific fields, a brief comment documents what it guards: mu sync.Mutex // guards n.

Why: Concurrency bugs come from unclear ownership. A one-line comment makes the lock's scope obvious to every reader.

Anti-pattern: No documentation of what a lock protects; locks that protect "everything" (unclear scope); comments that restate the type.

Code example from source:

// net/http/example_handle_test.go:16-17
type countHandler struct {
    mu sync.Mutex // guards n
    n  int
}

11. Duration Type Pattern

Pattern name: Named Type for Semantic Units

Source citation: time/time.go lines 915943

What it does: Duration is type Duration int64 — a named type over a primitive. This gives it its own method set (String(), Hours(), Truncate()) and prevents accidental mixing with raw int64 values.

Why: Semantic meaning through the type system. You can't accidentally pass nanoseconds where seconds are expected. Methods provide conversion and formatting. Constants like time.Second make intent clear.

When to Use

Triggers:

  • A primitive type (int, string, float64) has a specific semantic meaning in your domain
  • You want to attach methods (formatting, validation, arithmetic) to the value
  • You've seen bugs from accidentally mixing units (int could be seconds, milliseconds, or nanoseconds)

Example — before:

func SetTimeout(ms int) { ... }    // is this milliseconds? seconds?
func SetRetries(n int) { ... }     // can't accidentally swap these... or CAN you?

SetTimeout(5)   // 5 what?
SetRetries(500) // oops, swapped arguments — compiles fine!

Example — after:

type Timeout time.Duration
type RetryCount int

func SetTimeout(t Timeout) { ... }
func SetRetries(n RetryCount) { ... }

SetTimeout(Timeout(5 * time.Second))   // explicit units
SetRetries(RetryCount(3))              // can't swap — different types

Anti-pattern: Using raw int64 for durations; accepting int parameters for time intervals; mixing units (milliseconds in one place, seconds in another).

Code example from source:

// time/time.go:915
type Duration int64

// time/time.go:947-949
func (d Duration) String() string {
    var arr [32]byte
    n := d.format(&arr)
    return string(arr[n:])
}

12. gofmt: Non-Negotiable Formatting

Pattern name: Canonical Formatting via gofmt

Source citation: Every file in the Go standard library

What it does: All Go code is formatted with gofmt. Tabs for indentation, spaces for alignment. No style debates — the tool decides.

Why: Eliminates formatting bikesheds. All Go code looks the same regardless of author. Diffs show only semantic changes, never style changes. Tooling can parse and emit canonical code.

Anti-pattern: Manual formatting; spaces for indentation; custom alignment rules; checking in code that gofmt would modify.

Key rules enforced by gofmt:

  • Tabs for indentation
  • Opening brace on the same line (if x {)
  • No optional parentheses (if x, not if (x))
  • Aligned struct field tags
  • One blank line between top-level declarations
  • No trailing whitespace

13. Import Organization

Pattern name: Grouped Imports (stdlib / external / internal)

Source citation: net/http/server.go lines 836

What it does: Imports are organized in groups separated by blank lines:

  1. Standard library
  2. External packages (golang.org/x, third-party)
  3. Internal packages

The goimports tool enforces this automatically.

Why: Scannable at a glance. Makes dependency provenance clear (stdlib vs. external). Reduces merge conflicts.

Code example from source:

// net/http/server.go:8-36
import (
    "bufio"
    "bytes"
    "context"
    "crypto/tls"
    "errors"
    "fmt"
    // ... more stdlib ...
    "time"
    _ "unsafe" // for linkname

    "golang.org/x/net/http/httpguts"
)