Files
go-patterns/patterns/style.md
T
Rodin 484dc7dd07 fix: update drifted file:line citations in Go patterns
Upstream golang/go has shifted several line numbers since citations
were recorded. Updated 6 citations across 3 files:

- documentation.md: server.go:55-57 → 59-62 (ErrWriteAfterFlush)
- documentation.md: transport.go:79-80 → 72-73 (Transports should be reused)
- structs.md: client.go:31-35 → 30-34 (A Client is an HTTP client)
- structs.md: client.go:59-60 → 57-58 (type Client struct)
- style.md: stream.go:280 → 292 (var _ Marshaler)
- style.md: stream.go:280-281 → 292-293 (var _ Marshaler/Unmarshaler)

Verified against golang/go HEAD (depth=1 clone).
2026-05-27 05:52:38 +00:00

21 KiB
Raw Blame History

Code Style Patterns in the Go Standard Library

Source: golang/go at commit 17bd5ab

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#L130 (URL), net/http/server.go#L3040 (TLSConfig), encoding/json/stream.go#L280 (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:3040
TLSConfig *tls.Config

// encoding/json/stream.go:292
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#L645, os/file.go#L747, encoding/json/stream.go#L280

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) }

When NOT to Use

Don't use interface compliance checks when:

  • The type is unexported AND only used locally (the compiler catches it at the call site anyway)
  • You're asserting against an interface you don't own and it changes frequently (creates churn)
  • The interface is implemented implicitly and you're just cargo-culting the pattern for every type

Over-application example:

// Every single struct gets a compliance check, even trivial unexported ones
var _ fmt.Stringer = (*internalHelper)(nil)  // only used in one function in this file
var _ fmt.Stringer = (*anotherHelper)(nil)   // also never passed as Stringer anywhere

Better alternative:

// Skip the check for types that are only used locally — the compiler
// will catch the issue at the point of use:
func printThing(s fmt.Stringer) { fmt.Println(s.String()) }
printThing(&internalHelper{}) // compiler error here if String() is missing

// DO use it for exported types that implement external interfaces:
var _ io.ReadWriteCloser = (*MyConnection)(nil)  // users depend on this contract

Why: The pattern exists for API contracts — types that must satisfy an interface for consumers. For unexported types used only locally, the compiler already catches mismatches at the call site. Adding the check everywhere is noise.

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:292-293
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#L87, 100, 314, 387; os/file.go#L140, 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#L3173, net/http/example_handle_test.go#L21

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:3171-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#L14, os/error.go#L46

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#L70, time/time.go#L936

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

When NOT to Use

Don't use typed constants with iota when:

  • The values have external meaning (HTTP status codes, exit codes, protocol bytes) — use explicit values
  • The set is not exhaustive or will have gaps (iota assigns sequential values; gaps require explicit assignment)
  • You need the constant to interoperate with untyped int APIs without casting everywhere

Over-application example:

type HTTPStatus int

const (
    StatusOK HTTPStatus = iota  // 0?! HTTP 200 is not 0
    StatusNotFound              // 1?! Should be 404
    StatusServerError           // 2?! Should be 500
)

Better alternative:

type HTTPStatus int

const (
    StatusOK           HTTPStatus = 200
    StatusNotFound     HTTPStatus = 404
    StatusServerError  HTTPStatus = 500
)

Why: iota is for sequential enumerations where the actual numeric value doesn't matter (only the distinctness matters). When values have external meaning (wire protocols, HTTP, exit codes), use explicit values.

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:934-942
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#L16

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#L915

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

When NOT to Use

Don't create named types when:

  • The primitive is only used in one place (over-engineering for a single call site)
  • The type would need constant casting back to the primitive for stdlib interop
  • The semantic meaning is already clear from the parameter name (func Sleep(seconds int) in a script is fine)

Over-application example:

type Port uint16
type Host string
type Path string

// Now every function that takes these needs explicit construction:
func Connect(h Host, p Port, path Path) { ... }
Connect(Host("localhost"), Port(8080), Path("/api"))  // ceremony for no safety gain

Better alternative:

// For simple configurations, a struct with named fields provides clarity without type ceremony:
type Endpoint struct {
    Host string
    Port uint16
    Path string
}

func Connect(ep Endpoint) { ... }
Connect(Endpoint{Host: "localhost", Port: 8080, Path: "/api"})

Why: Named types shine when you need methods, when confusion between units causes real bugs (seconds vs milliseconds), or when the type system should prevent mixing semantically different values of the same primitive. If you're just adding type annotations to strings that don't interact, you're adding ceremony without safety.

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#L9

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:9-36
import (
    "bufio"
    "bytes"
    "context"
    "crypto/tls"
    "errors"
    "fmt"
    // ... more stdlib ...
    "time"
    _ "unsafe" // for linkname

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