go-api-ergonomics-patterns
Use this for the Go-specific realization of the api-ergonomics-audit checklist — `(*Cache).Close()` shape, `errors.Is(err, ErrX)` discoverability, `Example_*` runnable godoc with `// Output:`, ctx-as-
SKILL.mdskills/go-api-ergonomics-patterns/SKILL.md
GitHubgo-api-ergonomics-patterns (v1.0.0)
Scope
Go realization of the 8-point checklist in shared-core api-ergonomics-audit. The audit defines what to check; this skill defines what "good" looks like in Go.
5-line quickstart
The first ten minutes of a new integrator's life decide whether the SDK ships. "Hello world" is construct → use → close:
cache, err := dragonfly.New(dragonfly.WithAddr("localhost:6379"))
if err != nil { return err }
defer cache.Close()
if err := cache.Set(ctx, "k", "v", 0); err != nil { return err }
v, err := cache.Get(ctx, "k")
If the quickstart exceeds 5 lines, either the Config has too many required fields (consider sensible defaults) or New has the wrong shape.
Runnable Example with // Output: block
Example_* functions live in *_example_test.go. Two wins simultaneously: go test runs them (they catch silent breakage), and pkg.go.dev renders them as the intro:
// Example demonstrates Set/Get round-trip and miss classification via errors.Is.
func Example() {
mr, _ := miniredis.Run()
defer mr.Close()
cache, _ := dragonfly.New(dragonfly.WithAddr(mr.Addr()), dragonfly.WithProtocol(2))
defer cache.Close()
ctx := context.Background()
_ = cache.Set(ctx, "greeting", "hello", 0)
v, _ := cache.Get(ctx, "greeting")
fmt.Println(v)
_, err := cache.Get(ctx, "absent")
if errors.Is(err, dragonfly.ErrNil) {
fmt.Println("absent: miss")
}
// Output:
// hello
// absent: miss
}
Source: core/l2cache/dragonfly/example_test.go lines 20-57.
Naming convention: package-level Example() for the intro; ExampleType_Method() for each significant method (renders as a section under the type's godoc on pkg.go.dev).
Sentinel-error discoverability
Every failure mode is a package-level var ErrX = errors.New(...), with godoc starting with the symbol name and a [traces-to:] provenance marker:
// ErrNil is the sentinel for "key not found" (mirrors redis.Nil).
// [traces-to: TPRD-§7-ErrNil]
var ErrNil = errors.New("dragonfly: key not found")
// ErrTimeout wraps context.DeadlineExceeded and net.Error timeouts.
// [traces-to: TPRD-§7-ErrTimeout]
var ErrTimeout = errors.New("dragonfly: timeout")
Caller pattern:
v, err := cache.Get(ctx, "k")
if errors.Is(err, dragonfly.ErrNil) {
return cacheMiss // discriminated, not string-matched
}
The sentinel set is semver-public — adding is minor, removing/renaming is major. Document the set in package godoc.
ctx-first parameter ordering
Every I/O method takes ctx context.Context as the FIRST parameter:
// GOOD
func (c *Cache) Get(ctx context.Context, key string) (string, error)
// BAD
func (c *Cache) Get(key string, ctx context.Context) (string, error)
staticcheck enforces this; reviewers flag deviations. Code generators that wrap your API also assume ctx-first.
godoc starts with the symbol name
// Cache is a Dragonfly L2 client... ← good: starts with type name
type Cache struct { ... }
// Get fetches the value at key... ← good
func (c *Cache) Get(...) (...)
// fetches the value at key ← bad: doesn't start with name
func (c *Cache) Get(...) (...)
staticcheck's ST1020 and gofmt flag missing first-word-is-name. CLAUDE.md rule 6 requires godoc on every exported symbol.
Compile-time interface assertion
When implementing a port:
var _ ports.Cache = (*Cache)(nil) // compiles only if *Cache satisfies ports.Cache
Catches "I forgot to implement the new method" at build time, before tests.
Anti-patterns
1. String-matching error discrimination. if strings.Contains(err.Error(), "not found") — fragile to godoc rewords. Fix: export ErrNotFound; consumer uses errors.Is.
2. Two-error returns (T, error1, error2) — always wrong. Pick a single error chain; use errors.Is/errors.As to discriminate.
3. Surprising zero-value defaults. cache, _ := dragonfly.New() returning a Cache that panics on first use. Either reject the zero-value config (return ErrInvalidConfig) OR provide an explicit Default() constructor.
4. Missing Example_*. pkg.go.dev shows "Examples" section with nothing in it. First-time users reverse-engineer construction from tests. Author at minimum a package-level Example().
Severity ladder
- BLOCKER — ctx not first, two-error returns, panic on documented valid input, forged zero-value
- HIGH — missing
Example_*, missing sentinel for documented failure mode, quickstart >5 lines - MEDIUM — sibling-package inconsistency (Shutdown vs Close, int-seconds vs Duration)
- LOW — godoc phrasing, ordering of options functions, package-doc thinness
Cross-references
- shared-core
api-ergonomics-audit— the 8-point checklist this realizes go-sdk-config-struct-pattern— constructor shape (drives quickstart length)go-example-function-patterns— Example function structure,// Output:disciplinego-error-handling-patterns— sentinel taxonomy,errors.Is/errors.Asdisciplinesdk-marker-protocol—[traces-to:]marker on every sentinelsdk-semver-governance— ergonomics-driven API rewrite triggers semver bumps