← GophersCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Gophers
Snapshot Sep 30, 2026 · 23:14 UTC · version 0.1.0
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "Use when defining or implementing Go interfaces, composing types through embedding, designing dependency-injection seams, or deciding between pointer and value receivers. Apply proactively whenever a new abstraction is introduced or a constructor returns an abstract type, even if the user has not asked about interfaces. Does not cover generics (see go-generics).",
"included_files": [
{
"relative_path": "agents/openai.yaml",
"size_in_bytes": 220
},
{
"relative_path": "references/consumer-owned-interfaces.md",
"size_in_bytes": 3920
},
{
"relative_path": "references/embedding-and-receivers.md",
"size_in_bytes": 4709
},
{
"relative_path": "references/std-interfaces-cheatsheet.md",
"size_in_bytes": 4331
}
],
"name": "go-interfaces",
"skill_md_contents": "---\nname: go-interfaces\ndescription: \"Use when defining or implementing Go interfaces, composing types through embedding, designing dependency-injection seams, or deciding between pointer and value receivers. Apply proactively whenever a new abstraction is introduced or a constructor returns an abstract type, even if the user has not asked about interfaces. Does not cover generics (see go-generics).\"\nlicense: MIT\ncompatibility: \"Designed for Claude Code or similar AI coding agents. Targets Go 1.21+. Generics guidance is delegated to go-generics.\"\nallowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)\n---\n\n# Go Interfaces and Composition\n\nInterfaces in Go are *consumer contracts*, not implementation hierarchies. They should be **small**, **discovered late**, and **owned by the package that uses them** — not the package that satisfies them.\n\n## Core Rules\n\n1. **Accept interfaces, return concrete types.** Consumers state what they need; producers expose what they have.\n2. **Interfaces belong in the consumer package.** Defining an interface next to its sole implementation is almost always wrong.\n3. **Don't design with interfaces — discover them.** Wait for a second implementation or a test mock to demand one.\n4. **The bigger the interface, the weaker the abstraction.** Aim for 1–3 methods; compose larger contracts from smaller ones.\n5. **Receiver consistency:** if any method needs a pointer receiver, give *every* method a pointer receiver.\n6. **Verify satisfaction at compile time** with `var _ I = (*T)(nil)` when the relationship must not break silently.\n7. **Use the comma-ok idiom for every type assertion.** A bare assertion panics on mismatch.\n\n## Decision: Should I Introduce an Interface?\n\n| Situation | Verdict |\n|---|---|\n| Single implementation, no tests need to swap it | No interface. Use the concrete type. |\n| Second implementation appears (or is imminent) | Extract an interface in the consumer package. |\n| Test needs to fake an external dependency | Define a small interface in the consumer; pass a fake. |\n| You want to expose optional behaviour (`Flusher`, `ReaderFrom`) | Define a tiny interface; check with `_, ok := v.(Iface)`. |\n| You want a stable plugin/SPI boundary | Yes, but keep it minimal and version it explicitly. |\n\n> Read [references/consumer-owned-interfaces.md](references/consumer-owned-interfaces.md) when migrating a producer-defined interface back to the consumer, or when designing a new package boundary.\n\n## Accept Interfaces, Return Concrete Types\n\n```go\n// Good — consumer defines what it needs\npackage notify\ntype Sender interface { Send(to, body string) error }\ntype Service struct{ s Sender }\nfunc NewService(s Sender) *Service { return &Service{s: s} }\n\n// Good — producer returns a concrete type\npackage email\ntype Client struct{ /* ... */ }\nfunc New(cfg Config) *Client { /* ... */ }\nfunc (c *Client) Send(to, body string) error { /* ... */ }\n```\n\n```go\n// Bad — producer defines and returns its own interface,\n// forcing every consumer to depend on email.Sender.\nfunc New(cfg Config) Sender { return &client{...} }\n```\n\nThe exception is \"expose an interface, hide the implementation\": when a type has no exported methods beyond what the interface promises, returning the interface (`func NewHash() hash.Hash32`) is fine.\n\n## Keep Interfaces Small\n\nStandard library interfaces are the model: `io.Reader`, `io.Writer`, `io.Closer`, `fmt.Stringer`, `error` — one or two methods each. Compose larger contracts:\n\n```go\ntype ReadWriteCloser interface { io.Reader; io.Writer; io.Closer }\n```\n\nIf you find yourself writing a five-method interface, split it until each piece has a single reason to exist.\n\n## Compile-Time Satisfaction Check\n\n```go\nvar _ io.ReadWriter = (*MyBuffer)(nil)\n```\n\nUse when the type must satisfy an interface for correctness (custom JSON marshalling, `http.Handler`) and no other static use already enforces it. Don't add one for every interface.\n\n## Type Assertions and Type Switches\n\nAlways use the comma-ok form. Type switches re-declare the variable; cases with multiple types fall back to the interface type. Use optional-behaviour assertions to *enhance* a path without requiring the capability:\n\n```go\ns, ok := v.(string) // comma-ok\nswitch x := v.(type) { case string: /* ... */ } // type switch\nif f, ok := w.(http.Flusher); ok { f.Flush() } // optional behaviour\n```\n\n## Embedding: Composition, Not Inheritance\n\nStruct embedding promotes the inner type's methods and fields to the outer type. Use it deliberately — every promoted method becomes part of your public API.\n\n```go\ntype Server struct {\n *slog.Logger // exposes Info/Warn/Error on Server\n addr string\n}\n```\n\n| Use embedding when | Use a named field when |\n|---|---|\n| You want the outer type to *be* an enhanced version of the inner | You only need the inner type internally |\n| The full inner API should be promoted | You want to delegate explicitly to a subset |\n\nAvoid embedding in exported types unless the promotion is the whole point. The inner type's method set is locked in once published.\n\n> Read [references/embedding-and-receivers.md](references/embedding-and-receivers.md) when designing struct embedding, overriding promoted methods, resolving name conflicts, or choosing between pointer and value receivers.\n\n## Dependency Injection via Interfaces\n\nConstructors take interfaces; tests pass fakes. No DI container required.\n\n```go\ntype UserStore interface {\n FindByID(ctx context.Context, id string) (*User, error)\n}\n\ntype UserService struct{ store UserStore }\nfunc NewUserService(s UserStore) *UserService { return &UserService{store: s} }\n```\n\nThe `UserStore` interface lives in the package that defines `UserService`. The concrete `*pgUserStore` lives in a database package and doesn't know `UserService` exists.\n\n## Preventing Accidental Copies\n\nStructs that must not be copied (those holding a mutex, internal pointers, or a `sync.WaitGroup`) should embed a `noCopy` sentinel so `go vet` catches the mistake:\n\n```go\ntype noCopy struct{}\nfunc (*noCopy) Lock() {}\nfunc (*noCopy) Unlock() {}\n\ntype ConnPool struct {\n _ noCopy\n mu sync.Mutex\n /* ... */\n}\n```\n\nPass these by pointer (`func process(p *ConnPool)`), never by value.\n\n**Don't reach for `noCopy` reflexively.** Plain value types (config structs, request DTOs, immutable snapshots) *should* be copyable — adding `noCopy` to them locks consumers into pointer-only APIs for no gain. The rule of thumb: if the struct owns a `sync.Mutex`, `sync.WaitGroup`, `sync.Pool`, internal `chan`, or a pointer that *must* stay unique (file handle, OS resource), embed `noCopy`. Otherwise leave it copyable.\n\n## Anti-Patterns\n\n| Anti-pattern | Why it hurts | Do this instead |\n|---|---|---|\n| Producer-defined interface returned from constructor | Couples every consumer to the producer's package | Return the concrete type; let consumers define interfaces |\n| Five-plus method interface | Hard to implement, hard to mock | Split into small interfaces; compose |\n| Premature interface with one implementation | Indirection without value | Start concrete; extract when a second consumer appears |\n| `v := x.(T)` without `ok` | Panics on mismatch | `v, ok := x.(T)` |\n| Embedding a concrete type into an exported struct | Inner API leaks into your public surface | Use a named, unexported field |\n| Mixing pointer and value receivers on one type | `(*T)` and `T` have different method sets — confusing satisfaction errors | Pick one receiver style for the whole type |\n| `ToString()` / `ReadData()` instead of canonical names | Breaks `fmt.Stringer` / `io.Reader` discovery | Honour `String()` / `Read(p []byte) (int, error)` |\n| Returning `*MyErr` instead of `error` | Typed-nil trap; `err != nil` is true even when \"no error\" | Return the interface type |\n\n## Verification Checklist\n\nBefore finishing an interface change:\n\n- [ ] Interfaces are defined in the package that consumes them\n- [ ] Constructors return concrete types (or hide a single unexported implementation behind a small interface)\n- [ ] No interface has more than ~3 methods unless it composes named smaller ones\n- [ ] Every type assertion uses the comma-ok form\n- [ ] Pointer vs value receivers are consistent across all methods on a type\n- [ ] Compile-time `var _ I = (*T)(nil)` exists where silent regressions would hurt\n- [ ] Exported structs don't accidentally promote inner-type APIs through embedding\n\n## References\n\n- [references/consumer-owned-interfaces.md](references/consumer-owned-interfaces.md) — where interfaces live and how to migrate\n- [references/embedding-and-receivers.md](references/embedding-and-receivers.md) — embedding, overrides, pointer vs value receivers\n- [references/std-interfaces-cheatsheet.md](references/std-interfaces-cheatsheet.md) — canonical signatures from the standard library\n"
}SHA-256 of public snapshot: 47d1d944f0f23e8e8ad111d282215e8e20aeca74dacf07abfabec3e4eceb89e2