Files
go-patterns/patterns/from-source.md
T
Rodin c7e61565c0 docs: full iterative patterns extraction from golang/go
794 lines, 35+ patterns across 9 topics with hyperlinked sources.
Includes frequency data from the source (281 interfaces, 55 sentinels,
262 constructors, 309 context-accepting functions, 2685 t.Helper calls).

Topics: interfaces, errors, testing, packages, concurrency,
documentation, naming, configuration, extension, performance, smells.

All examples are real code from the Go source, not invented.
2026-04-30 13:45:02 -07:00

20 KiB

Go Patterns (from golang/go Source)

Prescriptive patterns extracted from the Go language source using iterative analysis. Real examples, hyperlinked to source.

Source: golang/go at commit 17bd5ab

Stats: 281 interfaces, 55 sentinel errors, 145 error types, 262 constructors, 309 context-accepting functions, 1,065 examples.


Interface Design

Single-Method Interfaces

Rule: Define interfaces with exactly one method whenever possible.

type Reader interface {
    Read(p []byte) (n int, err error)
}

Why: Any type with that method satisfies the interface implicitly. Smaller interfaces = more types satisfy them = more reusable code.

When to use: Defining abstraction boundaries, function parameters, dependency injection.

When NOT to use: When operations are genuinely inseparable (sort.Interface needs Len+Less+Swap together).

Source: src/io/io.go#L86


Interface Composition

Rule: Build larger interfaces by embedding smaller ones.

type ReadWriter interface {
    Reader
    Writer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

Why: 15 composed interfaces in io/io.go from just 4 primitives (Reader, Writer, Closer, Seeker). Composition prevents interface bloat.

When to use: When callers need multiple capabilities together.

When NOT to use: Don't compose preemptively. Add compositions only when you have a real function that needs both capabilities.

Source: src/io/io.go#L131


Accept Interfaces, Return Structs

Rule: Parameters should be interfaces. Return values should be concrete types.

// Accepts interface:
func Copy(dst Writer, src Reader) (written int64, err error)

// Returns concrete:
func NewReader(rd io.Reader) *Reader

Why: Accepting interfaces maximizes caller flexibility. Returning structs gives callers full access without type assertions. 262 New* constructors in stdlib all return concrete types.

When to use: Every public API function.

When NOT to use: When the return type must be hidden (use an interface to prevent users from depending on internals).

Source: src/io/io.go#L408 (Copy), src/bufio/bufio.go (NewReader)


The Stringer Interface

Rule: Implement String() string for any type that has a human- readable representation.

func (t Time) String() string {
    return t.Format("2006-01-02 15:04:05.999999999 -0700 MST")
}

Why: 379 types in stdlib implement Stringer. fmt.Println uses it automatically. It's the Go equivalent of __str__.

When to use: Any type that might be printed or logged.

When NOT to use: Internal types that are never user-visible.

Source: src/time/time.go (Time.String)


Type Assertion for Optional Interfaces

Rule: Check if a value implements an optional interface using type assertion.

if wt, ok := src.(WriterTo); ok {
    return wt.WriteTo(dst)
}

Why: 104 type assertions in stdlib. This pattern allows fallback behavior — try the fast path, fall back to the generic path.

When to use: Optional optimizations (WriterTo, ReaderFrom), feature detection.

When NOT to use: Required behavior (just accept the interface directly in the signature).

Source: src/io/io.go#L420 (Copy's WriterTo check)


Error Handling

Sentinel Errors for Known Conditions

Rule: Package-level var Err* for errors callers need to check. Include package name in the message.

var ErrBadPattern = errors.New("syntax error in pattern")
var ErrRange = errors.New("value out of range")
var ErrUnsupported = errors.New("unsupported operation")

Why: 55 exported sentinel errors in stdlib. Callers use errors.Is(err, strconv.ErrRange) to handle specific cases.

When to use: Errors that represent documented, expected conditions callers should distinguish.

When NOT to use: Errors that carry dynamic context (use error types). Errors callers never need to identify specifically.

Source: src/strconv/number.go#L246


Error Types for Rich Context

Rule: Define types implementing error when you need structured error information.

type PathError struct {
    Op   string
    Path string
    Err  error
}

func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}

func (e *PathError) Unwrap() error { return e.Err }

Why: 145 error type implementations in stdlib. Callers use errors.As(err, &pathErr) to extract structured data.

When to use: When the error needs to carry structured fields (path, operation, underlying error).

When NOT to use: Simple conditions (use sentinel errors). One-off errors (use fmt.Errorf).

Source: src/os/error.go (PathError)


Wrap with %w

Rule: Add context when propagating errors. Use %w to preserve the chain.

return fmt.Errorf("cannot parse %q as JSON number: %w", val, strconv.ErrSyntax)

Why: 115 %w wrappings in stdlib. Creates a chain that errors.Is and errors.As can traverse.

When to use: Every time you add context to an error from a lower layer.

When NOT to use: When the original error's identity should be hidden from callers (use %v to break the chain).

Source: src/encoding/json/v2_decode.go#L219


io.EOF as Termination Signal

Rule: Use io.EOF to signal normal end-of-stream, not an error.

n, err := r.Read(buf)
if err == io.EOF {
    break // Normal termination
}
if err != nil {
    return err // Actual error
}

Why: 316 io.EOF references in stdlib. EOF is expected, not exceptional. Readers return io.EOF when there's no more data.

When to use: Implementing Reader, iterators, stream processors.

When NOT to use: Errors that indicate failure (use a real error).

Source: src/io/io.go#L44


Testing

Table-Driven Tests

Rule: Use []struct{} with named cases and t.Run.

tests := []struct {
    name string
    input string
    want  string
}{
    {"empty", "", ""},
    {"hello", "hello", "HELLO"},
}
for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
        got := Transform(tt.input)
        if got != tt.want {
            t.Errorf("got %q, want %q", got, tt.want)
        }
    })
}

Why: 1,926 t.Run calls in the Go source. Named subtests make failure output clear. Adding cases is one struct literal.

When to use: Any function with 3+ input variations.

When NOT to use: Tests where setup varies significantly between cases (separate test functions).

Source: src/testing/testing_test.go (TestSetenv)


t.Helper() for Test Utilities

Rule: Call t.Helper() as the first line of any test helper.

func assertEqual(t *testing.T, got, want string) {
    t.Helper()
    if got != want {
        t.Errorf("got %q, want %q", got, want)
    }
}

Why: 2,685 t.Helper() calls. Without it, error messages report the helper's line number instead of the caller's.

When to use: Every function that calls t.Error, t.Fatal, or other testing.T methods on behalf of the caller.

When NOT to use: Functions that ARE the test (not helpers).


Example Functions as Living Docs

Rule: Write Example* functions in _test.go with // Output: comments.

func ExampleSprintf() {
    fmt.Println(fmt.Sprintf("Hello, %s", "world"))
    // Output: Hello, world
}

Why: 1,065 Example functions in stdlib. They compile, run, and appear in docs. They can't go stale.

When to use: Every exported function that would benefit from usage demonstration.

When NOT to use: Internal APIs. Functions with non-deterministic output.


testdata/ for Fixtures

Rule: Put test fixtures in testdata/ directories.

Why: 111 testdata/ dirs in stdlib. The go tool ignores them during compilation. Golden files, certificates, sample inputs live here.

When to use: Files your tests read but never modify at runtime.

When NOT to use: Generated test data (create in TestMain).


Benchmarks

Rule: Prefix benchmark functions with Benchmark and use b.N.

func BenchmarkSprintf(b *testing.B) {
    for b.Loop() {
        fmt.Sprintf("hello, %s", "world")
    }
}

Why: 1,974 benchmark functions in stdlib. Performance is tested, not assumed.

When to use: Any code on a hot path. Any code you're optimizing.

When NOT to use: Code that's not performance-sensitive.


Package Organization

Flat Packages

Rule: No pkg/ wrapper. Import path = directory path.

myproject/
├── server/
├── client/
├── internal/
└── cmd/
    └── myapp/

Why: The Go stdlib has zero nesting beyond 2 levels (e.g., net/http). Import paths should be short and predictable.

When to use: Always.

When NOT to use: Never. pkg/ is a community anti-pattern the Go team never endorsed.


internal/ for Shared Private Code

Rule: Code shared between packages but not part of public API goes in internal/.

Why: 61 internal packages in stdlib. Compiler-enforced — external code cannot import them. Stronger than unexported identifiers.

When to use: Utility code that multiple packages need but users shouldn't depend on.

When NOT to use: Code only one package uses (keep it unexported in that package). Code stable enough for public API (promote it).


One Package, One Responsibility

Rule: A package does one thing. Name it with a singular noun.

fmt, io, net, os, sync, time, bytes, errors

Why: Package names prefix all exported identifiers. Short names compose well: bytes.Buffer, sync.Mutex, time.Duration.

When to use: Every package.

When NOT to use: Never name packages utils, helpers, common, models, or types.


Concurrency

context.Context as First Parameter

Rule: Functions that do I/O take ctx context.Context first.

func (c *Client) Do(ctx context.Context, req *Request) (*Response, error)

Why: 309 functions take Context in stdlib. First-parameter position is universal. Context carries cancellation, deadlines, values.

When to use: Any function that blocks, does I/O, or might be cancelled.

When NOT to use: Pure computation. Init functions. Functions that complete instantly.


sync.Mutex for Shared State

Rule: Protect shared state with a mutex. Comment what it guards.

type Group struct {
    mu sync.Mutex       // protects m
    m  map[string]*call // lazily initialized
}

Why: 148 Mutex/RWMutex fields in stdlib. The comment-what-it- guards pattern appears throughout.

When to use: Shared mutable state accessed by multiple goroutines.

When NOT to use: Channel-based coordination. Single-goroutine ownership.

Source: src/internal/singleflight/singleflight.go#L30


sync.Once for Lazy Initialization

Rule: Use sync.Once for thread-safe lazy init.

var defaultLogger struct {
    once sync.Once
    val  *Logger
}

func getLogger() *Logger {
    defaultLogger.once.Do(func() {
        defaultLogger.val = newLogger()
    })
    return defaultLogger.val
}

Why: 58 sync.Once usages in stdlib. Guarantees exactly-once execution regardless of concurrent callers.

When to use: Expensive initialization that should happen at most once (DB connections, compiled regexps, parsed configs).

When NOT to use: Init that should happen at package load (use init() or package-level var).


sync.Pool for Reusable Buffers

Rule: Use sync.Pool for frequently allocated/freed objects.

var encodeStatePool sync.Pool

func newEncodeState() *encodeState {
    if v := encodeStatePool.Get(); v != nil {
        e := v.(*encodeState)
        e.Reset()
        return e
    }
    return new(encodeState)
}

Why: Used in encoding/json, fmt, and other hot-path code. Reduces GC pressure by reusing allocations.

When to use: Objects allocated per-request that are expensive to create and safe to reuse.

When NOT to use: Small objects. Objects with complex cleanup. Objects that shouldn't be shared between goroutines.

Source: src/encoding/json/encode.go#L312


defer for Cleanup

Rule: Use defer immediately after acquiring a resource.

mu.Lock()
defer mu.Unlock()

f, err := os.Open(path)
if err != nil { return err }
defer f.Close()

Why: 329 defer Close()/defer Unlock() in stdlib. Guarantees cleanup even on panic. Pairs acquisition with release visually.

When to use: Every Lock/Close/Release/Done pattern.

When NOT to use: Hot loops where defer overhead matters (rare, profile first).


Documentation

Package-Level doc.go

Rule: Complex packages get a doc.go with overview documentation.

// Source: src/fmt/doc.go
/*
Package fmt implements formatted I/O with functions analogous
to C's printf and scanf.
*/
package fmt

Why: 25 doc.go files in stdlib. Separates overview from code. # headings create sections in pkg.go.dev.

When to use: Any package with non-trivial API surface.

When NOT to use: Small packages where the comment fits in the main file.


Deprecated: Comment Convention

Rule: Mark deprecated items with // Deprecated: use X instead.

Why: 203 Deprecated: comments in stdlib. Tools (editors, linters) recognize this pattern and show warnings.

When to use: Any public API you want to discourage but can't remove.

When NOT to use: Internal code (just delete it).


Naming

Short Package Names

Rule: 3-7 characters, lowercase, singular noun.

fmt · io · net · os · sync · time · bytes · errors

When to use: Every package.

When NOT to use: NEVER: utils, helpers, common, base, models, types, shared.


New* Constructors

Rule: Constructor functions are named New or New<Type>.

func NewReader(rd io.Reader) *Reader
func New(text string) error

Why: 262 New* functions in stdlib. Universal convention. No Create*, no Make* (except make builtin), no Build*.

When to use: Any function that allocates and initializes.

When NOT to use: Functions that transform or convert (name them by what they do: Parse, Open, Dial).


No Get Prefix

Rule: Getters don't say "Get". Setters DO say "Set".

// Wrong:
func (u *User) GetName() string

// Right:
func (u *User) Name() string
func (u *User) SetName(s string)

Why: Go convention. Only 58 Get* methods in all of stdlib (mostly in legacy APIs like net/http).

When to use: All accessor methods.

When NOT to use: RPC/protobuf generated code (follows its own convention).


MixedCaps Only

Rule: ExportedName and unexportedName. Never underscores.

Why: Capitalization IS the visibility system. Underscores are reserved for test files and generated code.


Configuration

Functional Options (With* Pattern)

Rule: Options as functions returning an opaque Options type.

func NewEncoder(w io.Writer, opts ...Options) *Encoder

// Option constructors:
func WithIndent(indent string) Options { ... }
func WithByteLimit(n int64) Options { ... }

Why: Growing in stdlib (encoding/json/v2, context). Allows adding options without breaking existing callers.

When to use: APIs with many optional parameters that grow over time.

When NOT to use: Simple APIs with 1-2 options (just use parameters or a config struct).

Source: src/encoding/json/jsontext/options.go#L232


Config Structs for Complex Setup

Rule: Group related options into a named struct.

type Config struct {
    Certificates []Certificate
    RootCAs      *x509.CertPool
    ServerName   string
    MinVersion   uint16
}

Why: crypto/tls.Config is the canonical example. Zero value is usable with sensible defaults.

When to use: APIs with many related settings that configure a long-lived object.

When NOT to use: Per-call options (use functional options).

Source: src/crypto/tls/common.go#L566


Extension

Register Pattern (Plugin Discovery)

Rule: Provide a Register* function for plugin architectures.

func Register(name string, driver driver.Driver) {
    // ...
}

Why: Used in database/sql, encoding/gob, image, archive/zip, crypto. The pattern: init-time registration + runtime lookup.

When to use: When users provide implementations you discover at runtime (drivers, codecs, formats).

When NOT to use: When you know all implementations at compile time (use interfaces directly).

Source: src/database/sql/sql.go#L53


Performance

Append* for Zero-Alloc Formatting

Rule: Provide Append* variants that write to caller's buffer.

func (t Time) AppendFormat(b []byte, layout string) []byte
func AppendEncode(dst, src []byte) []byte
func AppendQuote[Bytes ~[]byte | ~string](dst []byte, src Bytes) ([]byte, error)

Why: Growing pattern in stdlib. Avoids allocation by letting the caller own the buffer. The encoding package now defines BinaryAppender and TextAppender interfaces.

When to use: Hot-path formatting functions where allocation cost matters.

When NOT to use: Convenience APIs where readability > performance.

Source: src/time/format.go#L655


Preallocate Slices

Rule: Use make([]T, 0, expectedCap) when you know the size.

Why: 326 make([]T, len, cap) calls in stdlib. Avoids repeated reallocation during append.

When to use: Loops where the output size is known or estimable.

When NOT to use: Unknown sizes. Small slices (<8 elements).


Smells

go:linkname Abuse

1,711 uses in Go's own source — but actively being removed. If you need go:linkname, your API boundary is wrong.

TODO Without Owner

// TODO: fix this — unaccountable. Go's 3,428 TODOs ALL have owners.

Get* Methods

Only 58 in stdlib, mostly legacy. Modern Go drops the prefix.

Huge Single Files

proc.go is 8,156 lines. Don't copy this. The scheduler stays in one file because splitting breaks the mental model. Your CRUD handler has no such excuse.

Generated Code Without Generator

If you check in generated code, also check in the generator or clearly document regeneration steps.