# Go Package Design Patterns Patterns extracted from the Go standard library source code. --- ## 1. Package-Level Documentation ### Source: `src/io/io.go:5-13`, `src/sync/mutex.go:5-11`, `src/context/context.go:5-57` ```go // src/io/io.go:5-13 // Package io provides basic interfaces to I/O primitives. // Its primary job is to wrap existing implementations of such primitives, // such as those in package os, into shared public interfaces that // abstract the functionality, plus some other related primitives. // // Because these interfaces and primitives wrap lower-level operations with // various implementations, unless otherwise informed clients should not // assume they are safe for parallel execution. package io ``` ```go // src/sync/mutex.go:5-11 // Package sync provides basic synchronization primitives such as mutual // exclusion locks. Other than the Once and WaitGroup types, most are intended // for use by low-level library routines. Higher-level synchronization is // better done via channels and communication. // // Values containing the types defined in this package should not be copied. package sync ``` ### Why The package comment: 1. **States the purpose** in one sentence 2. **Establishes contracts** (not safe for parallel execution, values must not be copied) 3. **Guides users** toward correct usage (prefer channels over mutexes) 4. **Appears before `package` keyword** — becomes `go doc` output ### Convention - First sentence: `"Package X does Y."` or `"Package X provides Y."` - Subsequent paragraphs: contracts, caveats, links to deeper docs - For multi-file packages, put the package comment in `doc.go` or the primary file ### Anti-pattern ```go // DON'T: No package comment package myutil // DON'T: Restate the obvious // Package http provides HTTP stuff. package http // DON'T: Put implementation details in the package comment // Package auth uses bcrypt with cost 12 and stores hashes in PostgreSQL. package auth ``` --- ## 2. Package Naming ### Source: All stdlib packages follow these conventions **Stdlib examples:** - `io` — not `ioutil`, not `ioutils` - `fmt` — not `format`, not `formatting` - `sync` — not `synchronization` - `net/http` — not `net/httpserver` - `encoding/json` — not `encoding/jsonparser` - `context` — not `ctx` or `contexts` - `errors` — not `errs` or `errorhandling` ### Why Go package names are: - **Short** — one word, lowercase, no underscores or mixedCaps - **Clear** — the name is the context for everything inside it - **Singular** (usually) — `context` not `contexts`, `error` exception (`errors` has functions) The package name is part of every qualified identifier: `http.Handler`, `json.Marshal`, `context.Context`. Redundancy in naming is wasted keystrokes: ```go // Good: package name provides context http.Server // not http.HTTPServer json.Encoder // not json.JSONEncoder context.Context // the type IS the context ``` ### Anti-pattern ```go // DON'T: Stutter (repeat package name in exported identifiers) package http type HTTPServer struct{} // http.HTTPServer — redundant func NewHTTPClient() // http.NewHTTPClient — say "http" twice // DON'T: Use utility/helper package names package utils // what does it DO? package helpers // grab bag, no cohesion package common // everything ends up here // DON'T: Use plural when singular works package requests // should be: package request ``` --- ## 3. internal/ Packages — Restricting Visibility ### Source: `src/net/http/internal/`, `src/encoding/json/internal.go` ``` src/net/http/internal/ ├── ascii/ ├── chunked.go ├── common.go ├── http2/ ├── httpcommon/ ├── httpsfv/ ├── sniff.go └── testcert/ ``` ### Why Packages under `internal/` can only be imported by code rooted at the parent of `internal`. For example: - `net/http/internal/ascii` can be imported by `net/http` and `net/http/...` - It **cannot** be imported by `net/url` or any other package This lets you share code between sub-packages without making it part of the public API. ### Usage Guidelines ``` myproject/ ├── internal/ # shared across the project, but not importable externally │ ├── auth/ │ └── metrics/ ├── cmd/ │ └── server/ └── pkg/ # actually public API (if you use this convention) └── client/ ``` ### Anti-pattern ```go // DON'T: Export implementation details that should be internal package mylib func HelperThatOnlyIUse() {} // pollutes API surface // DON'T: Put everything in internal/ (nothing is reusable) // Balance: internal/ for implementation; exported packages for contracts ``` --- ## 4. Export Rules — The Capital Letter Boundary ### Source: Throughout stdlib — the convention is the language itself ```go // src/io/io.go var EOF = errors.New("EOF") // exported: uppercase var errInvalidWrite = errors.New(...) // unexported: lowercase // src/io/io.go:622-625 type teeReader struct { // unexported type r Reader w Writer } // src/io/io.go:618 func TeeReader(r Reader, w Writer) Reader { // exported constructor return &teeReader{r, w} } ``` ### Why The exported/unexported boundary is Go's encapsulation mechanism. `teeReader` is unexported because: 1. Users don't need to know its implementation 2. The return type is `Reader` (the interface) — maximum flexibility 3. The struct's fields can change without breaking anyone ### Pattern: Exported Function, Unexported Type ```go // Export the constructor, not the type func NewParser(r io.Reader) *parser { ... } // WRONG: can't return unexported type // Correct: return via interface or exported type func TeeReader(r Reader, w Writer) Reader { return &teeReader{r, w} } ``` ### Anti-pattern ```go // DON'T: Export everything "just in case" type Parser struct { Input string // should this be settable? probably not buffer []byte // internal state — definitely not pos int } // DON'T: Make internal state accessible type DB struct { Pool []*Conn // callers shouldn't manipulate the pool directly } ``` --- ## 5. init() Functions — Use Sparingly ### Source: `src/net/http/http2.go:37`, `src/net/http/servemux121.go:31` ```go // src/net/http/http2.go:37 func init() { // register HTTP/2 protocol implementation } ``` ### Why `init()` runs automatically at program start, in dependency order. The stdlib uses it for: - **Driver registration** (database drivers register via init) - **Protocol negotiation** (HTTP/2 registers its handler) - **Configuration from build tags** (`servemux121.go` — compatibility shim) ### Rules 1. `init()` should have no side effects beyond registration 2. No errors should be possible (can't return error from init) 3. Keep them short — they block program startup 4. Prefer explicit initialization in `main()` when possible ### Anti-pattern ```go // DON'T: Do heavy work in init func init() { db = connectToDatabase() // fails silently, crashes later cache = loadGigabyteFile() // blocks startup } // DON'T: Use init for configuration func init() { port = os.Getenv("PORT") // harder to test, implicit dependency } // DO: Prefer explicit setup func main() { db, err := connectToDatabase() if err != nil { log.Fatal(err) // clear failure point } } ``` --- ## 6. Functional Options Pattern ### Source: Not directly in stdlib, but `net/http.Server` and `database/sql.DB` demonstrate the problem it solves The stdlib uses struct-based configuration (Server, Transport, DB config via setters). The functional options pattern emerged from the community to solve the "many optional parameters" problem: ```go // The pattern (not in stdlib, but idiom from Rob Pike/Dave Cheney): type Option func(*Server) func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } } func WithLogger(l *log.Logger) Option { return func(s *Server) { s.logger = l } } func NewServer(addr string, opts ...Option) *Server { s := &Server{addr: addr, timeout: 30 * time.Second} for _, opt := range opts { opt(s) } return s } ``` ### What the stdlib uses instead: Config structs ```go // src/net/http/server.go (Server struct acts as config) srv := &http.Server{ Addr: ":8080", ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, Handler: mux, } ``` ### When to use which | Approach | When | |----------|------| | Config struct | Few options, all are data (stdlib preference) | | Functional options | Many options, some involve behavior, public API stability matters | | Builder pattern | Rare in Go — usually overkill | ### Anti-pattern ```go // DON'T: Long parameter lists func NewServer(addr string, timeout time.Duration, maxConns int, logger *log.Logger, tls *tls.Config, handler Handler) *Server // DON'T: Use functional options when a simple struct suffices // (Over-engineering for 2-3 fields) ``` --- ## 7. Constructor Pattern — NewX Functions ### Source: `src/net/http/server.go:2639`, `src/database/sql/sql.go:836` ```go // src/net/http/server.go:2639 func NewServeMux() *ServeMux { return new(ServeMux) } // src/database/sql/sql.go:836-843 func OpenDB(c driver.Connector) *DB { ctx, cancel := context.WithCancel(context.Background()) db := &DB{ connector: c, openerCh: make(chan struct{}, connectionRequestQueueSize), lastPut: make(map[*driverConn]string), stop: cancel, } go db.connectionOpener(ctx) return db } ``` ### Why - `NewX()` when construction is trivial (just allocate) - `OpenX()` or `NewXWithConfig()` when construction involves resources, validation, or can fail - Return `*T` (pointer to concrete type), not an interface The zero value should be usable where possible (`sync.Mutex`, `bytes.Buffer`), making constructors unnecessary. ### Anti-pattern ```go // DON'T: Constructor that returns interface (hides useful methods) func NewWriter() io.Writer { return &myWriter{} } // DON'T: Require constructor when zero value works type Buffer struct { buf []byte // ... } // var b bytes.Buffer ← just works, no New needed ``` --- ## 8. Package Organization — One Concern Per Package ### Source: Standard library structure ``` src/ ├── io/ # I/O interfaces + helpers ├── os/ # OS operations ├── net/ # network primitives │ ├── http/ # HTTP protocol │ └── url/ # URL parsing ├── encoding/ # encoding interfaces │ ├── json/ # JSON codec │ └── xml/ # XML codec ├── database/ │ └── sql/ # SQL database abstraction │ └── driver/ # SPI for database drivers └── context/ # cancellation propagation ``` ### Why Each package has a single, clear responsibility: - `io` defines interfaces; `os` implements them for files - `encoding/json` handles JSON; `encoding/xml` handles XML - `database/sql` is the user-facing API; `database/sql/driver` is the implementor-facing SPI ### Anti-pattern ```go // DON'T: Package per type package user // just has User struct package order // just has Order struct package payment // just has Payment struct // 50 packages with 1 file each — Go prefers fewer, larger packages // DON'T: Circular dependencies package a imports package b package b imports package a // compile error // FIX: Extract shared types into a third package, or merge ``` --- ## 9. API Design — database/sql Separation of Concerns ### Source: `src/database/sql/sql.go` vs `src/database/sql/driver/driver.go` Two distinct APIs in one subsystem: **User-facing (database/sql):** ```go db, _ := sql.Open("postgres", connStr) rows, _ := db.QueryContext(ctx, "SELECT ...") defer rows.Close() for rows.Next() { rows.Scan(&id, &name) } ``` **Driver-facing (database/sql/driver):** ```go type Driver interface { Open(name string) (Conn, error) } type Conn interface { Prepare(query string) (Stmt, error) Close() error Begin() (Tx, error) } ``` ### Why The user never sees `driver.Conn`. The driver never sees `sql.DB`'s pool logic. Clean separation: - Users get a high-level, safe API with pooling and retry - Drivers implement a low-level, minimal interface - The `sql` package mediates between them ### Anti-pattern ```go // DON'T: Expose implementation to users type DB struct { driver driver.Conn // users shouldn't touch this } // DON'T: Mix user and implementor APIs in one interface type Database interface { Query(sql string) Rows // user method Open(dsn string) Conn // driver method — different audiences } ``` --- ## 10. Context Key Pattern — Type-Safe Context Values ### Source: `src/context/context.go:132-164`, `src/net/http/server.go:244-252` ```go // src/context/context.go:132-164 (from doc comment) // Package user defines a User type that's stored in Contexts. // package user // // import "context" // // type key int // // var userKey key // // func NewContext(ctx context.Context, u *User) context.Context { // return context.WithValue(ctx, userKey, u) // } // // func FromContext(ctx context.Context) (*User, bool) { // u, ok := ctx.Value(userKey).(*User) // return u, ok // } ``` ```go // src/net/http/server.go:244-252 var ( ServerContextKey = &contextKey{"http-server"} LocalAddrContextKey = &contextKey{"local-addr"} ) type contextKey struct { name string } ``` ### Why - **Unexported key type** prevents other packages from accessing or overwriting your values - **Type-safe accessors** (`FromContext`) avoid type assertions at every call site - **Pointer-based keys** (`&contextKey{...}`) guarantee uniqueness even with same string names ### Anti-pattern ```go // DON'T: Use string keys (any package can collide) ctx = context.WithValue(ctx, "user", user) // DON'T: Use exported key types (anyone can access) type Key string const UserKey Key = "user" // other packages can use this key // DON'T: Store optional parameters in context ctx = context.WithValue(ctx, "timeout", 5*time.Second) // use function params! ``` --- ## Summary: Package Design Principles | Principle | Rule | |-----------|------| | Package comment | `"Package X does Y."` before `package` keyword | | Naming | Short, lowercase, no stutter (`http.Server` not `http.HTTPServer`) | | Encapsulation | `internal/` for shared-but-private code | | Exports | Minimum viable surface; unexported by default | | init() | Only for registration; keep trivial | | Constructors | `NewX()` → `*T`; prefer usable zero values | | Organization | One concern per package; no circular deps | | API layers | Separate user-facing from implementor-facing (SPI) | | Context values | Unexported key type + typed accessors | | Configuration | Struct literals (stdlib) or functional options (community) |