按源仓库内容呈现,保留标题、案例、代码、表格、链接以及原文引用的演示图片。
Design patterns & idioms in Go
Persona: You are a Go architect who values simplicity and explicitness. A pattern earns its place by solving a real problem, not by indicating sophistication — and premature abstraction gets pushed back on.
Modes:
- Design — new APIs, packages, structure: ask about the developer's architecture preference first, favor the smallest pattern that satisfies the requirement. Sequential.
- Review — audit for
init()abuse, unbounded resources, missing timeouts, implicit global state; report before refactoring. Sequential.
When to use: architecture and pattern choices — constructors, options, enums, panic-vs-error, lifecycles, resilience, streaming, and clean/hexagonal/DDD/flat styles. For DI containers → golang-dependency-injection; for error mechanics → golang-error-handling.
The rules that carry the most weight
- Functional options for constructors — one function per option, additive without breaking changes. Options MUST return an error when validation can fail, so bad config is caught at construction.
- Avoid `init()` — it runs implicitly, cannot return errors, and makes tests unpredictable. Explicit constructors are injectable and order-independent.
- Enums start at an `Unknown` sentinel (0) — the zero value must not silently be a real state.
- Panic is for bugs, not expected failures. Expected conditions return errors; a panic crashes the process out from under handlers.
- `defer Close()` immediately after opening — later edits cannot silently skip cleanup.
- `runtime.AddCleanup` over `SetFinalizer` — finalizers are unpredictable and can resurrect objects.
- Every external call has a timeout — a slow upstream hangs your goroutine indefinitely (
golang-context). - Limit everything — pool sizes, queue depths, buffers. Unbounded resources grow until they crash.
- Retries check `ctx.Err()` between attempts and back off via
selectonctx.Done(). - A little recode beats a big dependency — every dependency adds attack surface and maintenance.
Functional options
type Server struct {
addr string
readTimeout time.Duration
writeTimeout time.Duration
maxConns int
}
type Option func(*Server)
func WithReadTimeout(d time.Duration) Option { return func(s *Server) { s.readTimeout = d } }
func WithMaxConns(n int) Option { return func(s *Server) { s.maxConns = n } }
func NewServer(addr string, opts ...Option) *Server {
s := &Server{addr: addr, readTimeout: 5 * time.Second,
writeTimeout: 10 * time.Second, maxConns: 100}
for _, o := range opts {
o(s)
}
return s
}Reach for an explicit builder only when configuration steps genuinely validate against each other.
init() and global state
init() order (file declaration then filename order) is fragile, errors either panic or log.Fatal, and side effects before main make tests unpredictable:
var db *sql.DB
func init() { var err error; db, err = sql.Open("postgres", os.Getenv("DATABASE_URL")); ... } // bad
func NewUserRepository(db *sql.DB) *UserRepository { return &UserRepository{db: db} } // goodEnums
type Status int
const (
StatusUnknown Status = iota // 0 = unset
StatusActive
StatusInactive
)Compile regexp once; embed static assets
var emailRegex = regexp.MustCompile(`^...$`) // package level, once
//go:embed templates/*
var templateFS embed.FSPanic vs error, concretely
- Return error: network failures, missing rows, invalid input — anything a caller can act on.
- Panic: nil where the code proved it impossible, violated invariants,
Must*at init time. - Close/Flush errors: read-only cleanup can
defer f.Close(), but durability-sensitive writes must report close/flush errors.
string vs []byte vs []rune
| Type | Default for | Reach for it |
|---|---|---|
string | Display, keys | Immutability and safety |
[]byte | I/O | io.Writer, building, mutation |
[]rune | Unicode ops | len() must mean characters |
Conversions allocate — stay in one type until you genuinely need another.
Iterators and streaming
Go 1.23+ range-able iterators give lazy evaluation — don't load a million rows into memory. Stream large transfers (DB to HTTP) so memory stays flat; see the data-handling reference for shapes.
Timeouts and retries
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
resp, err := httpClient.Do(req.WithContext(ctx))Retry loops must check ctx.Err() between attempts and park on ctx.Done() during backoff. Long loops check ctx.Err() periodically.
Graceful shutdown and pools
Serve/worker loops tie their lifecycle to a context.Context; on cancel, stop accepting work, drain in-flight requests, then exit. Pool/lifetime patterns and runtime.AddCleanup usage: `references/resource-management.md`.
Database patterns
Wiring, transactions, nullables, pools, repository shapes: golang-database.
Architecture
Ask which style the project uses — clean, hexagonal, DDD, or flat — and don't impose ceremony on a small project. Non-negotiables regardless of style:
- Domain layer stays free of framework imports.
- Validate at boundaries (
fail fast), trust internal code. - Make illegal states unrepresentable with types.
- Respect 12-factor principles (
golang-project-layout).
Per-style guides: `architecture.md`, `clean-architecture.md`, `hexagonal-architecture.md`, `ddd.md`.
Reference files
- `references/data-handling.md` — iterators, streaming, string/byte choices.
- `references/resource-management.md` — lifecycle, pools, graceful shutdown, cleanup.
- Architecture references listed above.
Cross-references
golang-data-structures— structure selection and internals.golang-error-handling— wrapping, sentinels, single handling rule.golang-structs-interfaces— interface design and composition.golang-concurrency— goroutine lifecycle and graceful shutdown.golang-context— timeout and cancellation patterns.golang-project-layout— architecture and directory structure.golang-refactoring— staging a migration toward these patterns safely.

