c8ed244a07
Every source reference now links to the exact line in the golang/go repo at commit 17bd5ab. Added PATTERN_COMPLETE sentinels. Total: 154 hyperlinks across 10 topic files.
650 lines
18 KiB
Markdown
650 lines
18 KiB
Markdown
# Go Package Design Patterns
|
|
|
|
Patterns extracted from the Go standard library source code.
|
|
|
|
---
|
|
|
|
## 1. Package-Level Documentation
|
|
|
|
### Source: [src/io/io.go#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L5), [src/sync/mutex.go#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L5), [src/context/context.go#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L5)
|
|
|
|
```go
|
|
// [src/io/io.go#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L5)
|
|
// 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#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L5)
|
|
// 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."`
|
|
- 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
|
|
```
|
|
|
|
---
|
|
|
|
## 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`
|
|
|
|
### Why
|
|
|
|
Go package names are **short, lowercase, no underscores or mixedCaps**. The package name is part of every qualified identifier:
|
|
|
|
```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
|
|
package http
|
|
type HTTPServer struct{} // http.HTTPServer — redundant
|
|
|
|
// DON'T: Utility package names
|
|
package utils // what does it DO?
|
|
package helpers // grab bag, no cohesion
|
|
package common // everything ends up here
|
|
```
|
|
|
|
---
|
|
|
|
## 3. internal/ Packages — Restricting Visibility
|
|
|
|
### Source: `src/net/http/internal/`, `src/encoding/json/internal.go`
|
|
|
|
```
|
|
src/net/http/internal/
|
|
├── ascii/
|
|
├── chunked.go
|
|
├── http2/
|
|
├── httpcommon/
|
|
├── sniff.go
|
|
└── testcert/
|
|
```
|
|
|
|
### Why
|
|
|
|
Packages under `internal/` can only be imported by code rooted at the parent of `internal`. This lets you share code between sub-packages without making it public API.
|
|
|
|
- `net/http/internal/ascii` → importable by `net/http` and children
|
|
- NOT importable by `net/url` or any other package
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- You have helper code shared between sub-packages but NOT part of your public API
|
|
- You're tempted to export a function "just for testing" — put it in `internal/` instead
|
|
- Your package has grown and you want to split it without committing to new public APIs
|
|
|
|
**Example — before:**
|
|
```go
|
|
// pkg/mylib/helpers.go — exported just so pkg/mylib/sub can use it
|
|
package mylib
|
|
|
|
func ParseInternalFormat(s string) Thing { ... } // now anyone can depend on this!
|
|
```
|
|
|
|
**Example — after:**
|
|
```go
|
|
// pkg/mylib/internal/parse/parse.go
|
|
package parse
|
|
|
|
func InternalFormat(s string) Thing { ... } // only importable by pkg/mylib and children
|
|
|
|
// pkg/mylib/sub/handler.go
|
|
import "pkg/mylib/internal/parse" // ✓ allowed
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use `internal/` when:**
|
|
- The code is only used by a single package (just keep it unexported in that package)
|
|
- You're hiding code that *should* be public API — `internal/` isn't a staging area for "maybe later"
|
|
- You have a flat package structure with no sub-packages (no one to share with)
|
|
|
|
**Over-application example:**
|
|
```go
|
|
// pkg/mylib/internal/config/config.go
|
|
package config
|
|
|
|
// Only used by pkg/mylib itself — no sub-packages import this
|
|
func DefaultTimeout() time.Duration { return 30 * time.Second }
|
|
```
|
|
|
|
**Better alternative:**
|
|
```go
|
|
// pkg/mylib/config.go — just make it unexported in the parent package
|
|
package mylib
|
|
|
|
func defaultTimeout() time.Duration { return 30 * time.Second }
|
|
```
|
|
|
|
**Why:** `internal/` adds directory structure complexity. If you have no sub-packages sharing the code, an unexported function in the parent package is simpler and achieves the same encapsulation.
|
|
|
|
### Anti-pattern
|
|
|
|
```go
|
|
// DON'T: Export implementation details
|
|
package mylib
|
|
func HelperThatOnlyIUse() {} // pollutes API surface
|
|
|
|
// DO: Move to internal/
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Export Rules — The Capital Letter Boundary
|
|
|
|
### Source: `src/io/io.go` — exported vs unexported
|
|
|
|
```go
|
|
// src/io/io.go
|
|
var EOF = errors.New("EOF") // exported: uppercase
|
|
var errInvalidWrite = errors.New(...) // unexported: lowercase
|
|
|
|
type teeReader struct { // unexported type
|
|
r Reader
|
|
w Writer
|
|
}
|
|
|
|
func TeeReader(r Reader, w Writer) Reader { // exported constructor
|
|
return &teeReader{r, w}
|
|
}
|
|
```
|
|
|
|
### Why
|
|
|
|
`teeReader` is unexported because:
|
|
1. Users don't need to know its implementation
|
|
2. The return type is `Reader` (interface) — maximum flexibility
|
|
3. The struct's fields can change without breaking anyone
|
|
|
|
### Anti-pattern
|
|
|
|
```go
|
|
// DON'T: Export everything "just in case"
|
|
type Parser struct {
|
|
Input string // should this be settable?
|
|
buffer []byte // internal state
|
|
pos int
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. init() Functions — Use Sparingly
|
|
|
|
### Source: [src/net/http/http2.go#L37](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/http2.go#L37)
|
|
|
|
```go
|
|
// src/net/http/http2.go:37
|
|
func init() {
|
|
// register HTTP/2 protocol implementation
|
|
}
|
|
```
|
|
|
|
### Why
|
|
|
|
The stdlib uses `init()` for:
|
|
- **Driver registration** (database drivers register via init)
|
|
- **Protocol negotiation** (HTTP/2 registers its handler)
|
|
|
|
### Rules
|
|
|
|
1. Should have no side effects beyond registration
|
|
2. No errors possible (can't return error from init)
|
|
3. Keep them short
|
|
4. Prefer explicit initialization in `main()` when possible
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- You're writing a driver or plugin that needs to register itself with a central registry on import
|
|
- The registration is side-effect-only (no return value, can't fail)
|
|
- You want `import _ "mydb/driver"` to make the driver available without explicit setup
|
|
|
|
**Example — before:**
|
|
```go
|
|
// main.go — user must manually register every driver
|
|
func main() {
|
|
postgres.Register() // easy to forget
|
|
mysql.Register() // order matters?
|
|
sqlite.Register()
|
|
}
|
|
```
|
|
|
|
**Example — after:**
|
|
```go
|
|
// postgres/driver.go
|
|
func init() {
|
|
sql.Register("postgres", &Driver{}) // auto-registers on import
|
|
}
|
|
|
|
// main.go — import for side-effect
|
|
import _ "github.com/lib/pq" // driver registers itself
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use `init()` when:**
|
|
- The initialization can fail (you can't return errors from `init()`)
|
|
- The setup requires configuration or parameters (init takes no args)
|
|
- You need to control initialization order across packages
|
|
- It's a one-off application (not a library/driver) — just call setup in `main()`
|
|
|
|
**Over-application example:**
|
|
```go
|
|
// internal/metrics/metrics.go
|
|
func init() {
|
|
// Bad: init() hides this dependency, makes testing impossible,
|
|
// and panics if prometheus isn't reachable
|
|
prometheus.MustRegister(requestCounter)
|
|
prometheus.MustRegister(errorCounter)
|
|
prometheus.MustRegister(latencyHistogram)
|
|
}
|
|
```
|
|
|
|
**Better alternative:**
|
|
```go
|
|
// internal/metrics/metrics.go
|
|
func Register(reg prometheus.Registerer) error {
|
|
if err := reg.Register(requestCounter); err != nil {
|
|
return fmt.Errorf("registering request counter: %w", err)
|
|
}
|
|
// ...
|
|
return nil
|
|
}
|
|
|
|
// main.go
|
|
func main() {
|
|
if err := metrics.Register(prometheus.DefaultRegisterer); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
```
|
|
|
|
**Why:** `init()` is invisible, untestable, and can't fail gracefully. Use it only when the registration pattern demands it (database/sql drivers, codec registration) and failure is impossible.
|
|
|
|
### Anti-pattern
|
|
|
|
```go
|
|
// DON'T: Do heavy work in init
|
|
func init() {
|
|
db = connectToDatabase() // fails silently, crashes later
|
|
cache = loadGigabyteFile() // blocks startup
|
|
}
|
|
|
|
// DO: Prefer explicit setup in main()
|
|
func main() {
|
|
db, err := connectToDatabase()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Functional Options Pattern
|
|
|
|
The stdlib uses struct-based configuration (`http.Server`, `tls.Config`). The functional options pattern emerged from the community for APIs with many optional parameters:
|
|
|
|
```go
|
|
// The pattern (idiom from Rob Pike/Dave Cheney):
|
|
type Option func(*Server)
|
|
|
|
func WithTimeout(d time.Duration) Option {
|
|
return func(s *Server) {
|
|
s.timeout = d
|
|
}
|
|
}
|
|
|
|
func NewServer(addr string, opts ...Option) *Server {
|
|
s := &Server{addr: addr, timeout: 30 * time.Second}
|
|
for _, opt := range opts {
|
|
opt(s)
|
|
}
|
|
return s
|
|
}
|
|
```
|
|
|
|
### What stdlib uses: Config structs
|
|
|
|
```go
|
|
// net/http — struct literal configuration
|
|
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 data (stdlib preference) |
|
|
| Functional options | Many options, some involve behavior, public API stability |
|
|
|
|
---
|
|
|
|
## 7. Constructor Pattern — NewX Functions
|
|
|
|
### Source: [src/net/http/server.go#L2639](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L2639), [src/database/sql/sql.go#L836](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/sql.go#L836)
|
|
|
|
```go
|
|
// src/net/http/server.go:2639
|
|
func NewServeMux() *ServeMux {
|
|
return new(ServeMux)
|
|
}
|
|
|
|
// [src/database/sql/sql.go#L836](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/sql.go#L836)
|
|
func OpenDB(c driver.Connector) *DB {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
db := &DB{
|
|
connector: c,
|
|
openerCh: make(chan struct{}, connectionRequestQueueSize),
|
|
stop: cancel,
|
|
}
|
|
go db.connectionOpener(ctx)
|
|
return db
|
|
}
|
|
```
|
|
|
|
### Why
|
|
|
|
- `NewX()` when construction is trivial
|
|
- `OpenX()` when construction involves resources or can fail
|
|
- Return `*T` (concrete), not an interface
|
|
- Zero value should be usable where possible (`sync.Mutex`, `bytes.Buffer`)
|
|
|
|
### Anti-pattern
|
|
|
|
```go
|
|
// DON'T: Constructor that returns interface
|
|
func NewWriter() io.Writer { return &myWriter{} } // hides methods
|
|
|
|
// DON'T: Require constructor when zero value works
|
|
// var b bytes.Buffer ← just works
|
|
```
|
|
|
|
---
|
|
|
|
## 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/
|
|
│ ├── json/ # JSON codec
|
|
│ └── xml/ # XML codec
|
|
├── database/
|
|
│ └── sql/ # SQL abstraction
|
|
│ └── driver/ # SPI for drivers
|
|
└── context/ # cancellation propagation
|
|
```
|
|
|
|
### Why
|
|
|
|
Each package has a single, clear responsibility. Packages communicate through interfaces, not shared state.
|
|
|
|
### Anti-pattern
|
|
|
|
```go
|
|
// DON'T: Package per type (50 packages with 1 file each)
|
|
package user
|
|
package order
|
|
package payment
|
|
|
|
// DON'T: Circular dependencies
|
|
package a imports package b
|
|
package b imports package a // compile error
|
|
```
|
|
|
|
---
|
|
|
|
## 9. API Layering — User vs Implementor (database/sql)
|
|
|
|
### Source: `src/database/sql/sql.go` vs `src/database/sql/driver/driver.go`
|
|
|
|
**User-facing (database/sql):**
|
|
```go
|
|
db, _ := sql.Open("postgres", connStr)
|
|
rows, _ := db.QueryContext(ctx, "SELECT ...")
|
|
```
|
|
|
|
**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 high-level safe API; drivers implement minimal interface.
|
|
|
|
---
|
|
|
|
## 10. Context Key Pattern — Type-Safe Context Values
|
|
|
|
### Source: [src/context/context.go#L132](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L132), [src/net/http/server.go#L244](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L244)
|
|
|
|
```go
|
|
// [src/context/context.go#L132](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L132) (from doc)
|
|
// package user
|
|
//
|
|
// 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#L244](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L244)
|
|
var (
|
|
ServerContextKey = &contextKey{"http-server"}
|
|
LocalAddrContextKey = &contextKey{"local-addr"}
|
|
)
|
|
|
|
type contextKey struct {
|
|
name string
|
|
}
|
|
```
|
|
|
|
### Why
|
|
|
|
- **Unexported key type** prevents other packages from accessing your values
|
|
- **Type-safe accessors** avoid repeated type assertions
|
|
- **Pointer-based keys** guarantee uniqueness
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- You need to pass request-scoped metadata through a call chain (user ID, trace ID, auth token)
|
|
- The data crosses package boundaries and isn't appropriate as a function parameter
|
|
- You want type safety — only your package should read/write its context values
|
|
|
|
**Example — before:**
|
|
```go
|
|
// String keys — any package can collide or access your values
|
|
ctx = context.WithValue(ctx, "userID", 42)
|
|
uid := ctx.Value("userID").(int) // panics if wrong type or missing
|
|
```
|
|
|
|
**Example — after:**
|
|
```go
|
|
type ctxKey struct{}
|
|
|
|
func WithUserID(ctx context.Context, id int) context.Context {
|
|
return context.WithValue(ctx, ctxKey{}, id)
|
|
}
|
|
|
|
func UserID(ctx context.Context) (int, bool) {
|
|
id, ok := ctx.Value(ctxKey{}).(int)
|
|
return id, ok
|
|
}
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use context values when:**
|
|
- The data is a required function parameter (pass it explicitly)
|
|
- The data controls behavior/logic (timeouts, retry counts) — use function args or config structs
|
|
- You're using it to avoid refactoring function signatures
|
|
- The value is large or expensive to retrieve (context isn't a cache)
|
|
|
|
**Over-application example:**
|
|
```go
|
|
// Passing database connection through context — it's required everywhere!
|
|
func HandleRequest(ctx context.Context) {
|
|
db := DatabaseFromContext(ctx) // nil if forgotten — runtime panic
|
|
users, err := db.Query(ctx, "SELECT ...")
|
|
}
|
|
```
|
|
|
|
**Better alternative:**
|
|
```go
|
|
// Make the dependency explicit
|
|
type Handler struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func (h *Handler) HandleRequest(ctx context.Context) {
|
|
users, err := h.db.QueryContext(ctx, "SELECT ...")
|
|
}
|
|
```
|
|
|
|
**Why:** Context values are untyped, invisible in function signatures, and can silently be nil. They're meant for *request-scoped metadata* that crosses API boundaries (trace IDs, auth tokens), not for dependency injection or configuration.
|
|
|
|
### Anti-pattern
|
|
|
|
```go
|
|
// DON'T: Use string keys (collision risk)
|
|
ctx = context.WithValue(ctx, "user", user)
|
|
|
|
// DON'T: Store optional parameters in context
|
|
ctx = context.WithValue(ctx, "timeout", 5*time.Second) // use function params!
|
|
```
|
|
|
|
---
|
|
|
|
## 11. Struct Tags for Codec Configuration
|
|
|
|
### Source: [src/encoding/json/tags.go#L17](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/tags.go#L17), [src/encoding/json/encode.go#L101](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/encode.go#L101)
|
|
|
|
```go
|
|
// [src/encoding/json/tags.go#L17](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/tags.go#L17)
|
|
func parseTag(tag string) (string, tagOptions) {
|
|
tag, opt, _ := strings.Cut(tag, ",")
|
|
return tag, tagOptions(opt)
|
|
}
|
|
```
|
|
|
|
Usage in struct definitions:
|
|
```go
|
|
type Person struct {
|
|
Name string `json:"name"`
|
|
Age int `json:"age,omitempty"`
|
|
Secret string `json:"-"` // always omitted
|
|
Address string `json:"addr,omitempty"`
|
|
}
|
|
```
|
|
|
|
### Why
|
|
|
|
Struct tags are metadata for codecs. The `json` package reads `json:"..."` tags via reflection to control field names and behavior. The format is `key:"value"` with comma-separated options.
|
|
|
|
### Convention (from encode.go docs, line 101-181)
|
|
|
|
- `json:"fieldname"` — override JSON key name
|
|
- `json:",omitempty"` — omit if zero value
|
|
- `json:"-"` — never include
|
|
- `json:"-,"` — use literal `-` as name
|
|
|
|
---
|
|
|
|
## Summary: Package Design Principles
|
|
|
|
| Principle | Rule |
|
|
|-----------|------|
|
|
| Package comment | `"Package X does Y."` before `package` keyword |
|
|
| Naming | Short, lowercase, no stutter |
|
|
| Encapsulation | `internal/` for private shared code |
|
|
| Exports | Minimum surface; unexported by default |
|
|
| init() | Only for registration; prefer explicit setup |
|
|
| Constructors | `NewX()` → `*T`; prefer usable zero values |
|
|
| Organization | One concern per package |
|
|
| API layers | Separate user from implementor (SPI) |
|
|
| Context values | Unexported key type + typed accessors |
|
|
| Configuration | Struct literals or functional options |
|
|
|
|
<!-- PATTERN_COMPLETE -->
|