API Design Rules for Go Libraries
Naming, error returns, context first param, and breaking changes.
Dense reference for authors publishing Go libraries - scan before exporting new symbols or tagging a release.
How to Use This Cheatsheet
- Review before opening a PR that adds exported funcs, types, or constants.
- Pair with
go vet, staticcheck API checks, and semver tags. - Mark Block items as merge gates; Discuss items need ADR or issue link.
- Consumers read this when evaluating third-party module quality.
Naming and packages
| Rule | Do | Avoid | Rationale |
|---|---|---|---|
| Package name | Short, lowercase, no underscores (httpclient) | common, util, misc | Import path readability |
| Exported name | MixedCaps, no stutter (user.UserID) | user.UserUserID | pkg.go.dev clarity |
| Getter | Name() not GetName() | Java-style Get* on simple fields | Go convention |
| Interface name | -er suffix when one method (Reader) | IReader | Idiomatic interfaces |
| Acronyms | Consistent casing (HTTP, ID, URL) | Http, Id | golint/revive alignment |
| Error vars | Err prefix exported sentinels | ErrorNotFound mixed styles | errors.Is ergonomics |
Signatures and types
| Rule | Do | Avoid | Rationale |
|---|---|---|---|
| Context | func F(ctx context.Context, ...) first | context mid-params | Cancellation propagation |
| Options | Functional options or config struct | Long positional param lists | Forward-compatible APIs |
| Returns | Concrete structs/pointers | Unexported-interface returns | Caller type clarity |
| Parameters | Small interfaces at call site | Export wide Interface in producer pkg | Interface segregation |
| Nil | Document nil behavior | Panic on nil without doc | Predictable libraries |
| Time | time.Time / time.Duration in APIs | Raw int64 without units | Ambiguity in public APIs |
Errors and observability
| Rule | Do | Avoid | Rationale |
|---|---|---|---|
| Errors | Return error last | Panic for expected failures | Caller control |
| Wrapping | fmt.Errorf("op: %w", err) | Opaque errors.New chains | Inspection with Is/As |
| Sentinels | Stable var ErrX = errors.New(...) | String compare on Error() | Fragile clients |
| Logging | Let application log | Library log.Printf on errors | Import coupling |
| Metrics | Optional hooks/interfaces | Hard-coded Prometheus | Dependency weight |
Versioning and compatibility
| Rule | Do | Avoid | Rationale |
|---|---|---|---|
| Semver tag | v1.2.3 on module tags | Ad-hoc git tags | go get resolution |
| Breaking change | Major bump / new module path | Rename exports in patch | Consumer breakage |
| Deprecated | // Deprecated: use X + issue | Silent removal | Migration time |
| Experimental | v0 or internal/ until stable | v1 with churn | Expectation setting |
| go.mod | go directive matches CI | Drift across modules | Toolchain consistency |
Documentation and testing
| Rule | Do | Avoid | Rationale |
|---|---|---|---|
| Godoc | Full sentences on exports | Empty or wrong-name comments | pkg.go.dev quality |
| Examples | Example* in example_test.go | README-only samples | Compile-checked docs |
| Tests | package foo_test for consumer view | Only white-box tests | API ergonomics signal |
| Build tags | Document integration build tags | Surprise //go:build in core API | Consumer go test |
Constructor patterns
// Preferred: concrete return, optional functional options
type Client struct { /* ... */ }
type Option func(*Client)
func WithTimeout(d time.Duration) Option { /* ... */ }
func New(opts ...Option) (*Client, error) {
c := &Client{timeout: 30 * time.Second}
for _, o := range opts {
o(c)
}
return c, nil
}// Accept interfaces at integration boundaries
func RegisterHandler(mux ServeMux, h Handler) { /* ... */ }Breaking change decision table
| Change | Semver impact | Example |
|---|---|---|
| Unexported field added | Patch | Safe |
| New exported func | Minor | func ParseConfig |
| Exported func signature change | Major | Parameter type change |
| Remove exported symbol | Major | Delete LegacyDial |
| Behavior fix breaking callers | Major or feature flag | Stricter validation |
FAQs
When should a constructor return an interface?
Rarely.
Return concrete types unless you intentionally hide implementation behind a small interface type you define.
Should libraries use context.Background internally?
No.
Accept ctx from callers; only top-level main or tests create roots.
How small should exported interfaces be?
Often one method.
Consumers define them; producers return structs.
Can I export errors as strings?
Use sentinel var ErrFoo values.
Stable inspection beats substring matching on messages.
What about functional options versus config structs?
Options scale for libraries with many defaults.
Config structs work when fields are mostly required.
How do I mark experimental APIs?
Use internal/ until stable, or stay on v0 module tags with clear README warnings.
Should I export testing helpers?
Put them in package foo_test or internal/testutil.
Avoid widening semver surface for test-only helpers.
How do generics affect API design?
Export clear type parameters with constraints.
Avoid exporting overly generic helpers that obscure types.
When is stutter acceptable?
Almost never on exported names.
Package name already provides context (user.New not user.NewUser).
How do I deprecate without breaking v1?
Add Deprecated godoc, keep symbol, ship replacement, remove in next major.
Should context carry values across library boundaries?
Libraries should use ctx for cancellation/deadlines.
Avoid requiring private context keys unless documented as optional extension points.
What linters help API design?
staticcheck SA1019 (deprecated), exported comment checks (revive), and vet struct tags.
Pair with human review for shape.
Related
- Effective Go Rules Checklist - core idioms
- Module & Dependency Rules - semver and internal/
- Custom Error Types and Error Interfaces - error API shape
- Packages and Modules: Go's Unit of Distribution - module mechanics
- Go Rules & Best Practices Summary - section index
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).