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.
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.