--- name: go-interface-design description: > Go interface design patterns: implicit interfaces, consumer-side definition, interface compliance verification, composition, the accept-interfaces-return-structs principle, and common pitfalls. Use when designing interfaces, decoupling packages, defining contracts, reviewing interface usage, or refactoring for testability. Trigger examples: "design interface", "accept interfaces return structs", "interface compliance", "consumer-side interface", "interface composition". Not for: HTTP handler patterns (go-api-design), general review (go-code-review). user-invocable: true license: MIT compatibility: Designed for Claude Code or similar AI coding agents working on Go projects. Requires the Go toolchain. allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(gofmt:*) metadata: author: eduardo-sl version: "1.2.1" --- # Go Interface Design Go interfaces are implicit. This is the single most important design feature of the language, and most people coming from Java or C# get it wrong at first. > Community default. A project skill or agent-config rule that explicitly > supersedes `go-interface-design` takes precedence over this one. Detailed reference material, loaded on demand: - `references/embedding-and-assertions.md` — embedding vs named fields, interface-embedding test fakes, optional capabilities, sealed interfaces. Read a reference file only when the summary below is not enough. ## 1. The Cardinal Rule: Define Interfaces at the Consumer The consumer of a behavior defines the interface, NOT the provider: ```go // ❌ Wrong — producer defines interface (Java thinking) // package store type UserStore interface { // defined alongside implementation GetByID(ctx context.Context, id string) (*User, error) Create(ctx context.Context, user *User) error // ... 15 more methods } type PostgresStore struct { ... } func (s *PostgresStore) GetByID(...) { ... } func (s *PostgresStore) Create(...) { ... } // ✅ Right — consumer defines what it needs // package service type UserReader interface { // only what THIS service needs GetByID(ctx context.Context, id string) (*domain.User, error) } type UserService struct { store UserReader // depends on narrow interface } // package store (no interface defined here) type PostgresStore struct { db *sql.DB } func (s *PostgresStore) GetByID(ctx context.Context, id string) (*domain.User, error) { ... } func (s *PostgresStore) Create(ctx context.Context, user *domain.User) error { ... } // PostgresStore satisfies service.UserReader implicitly — no declaration needed ``` Why this matters: - Consumer depends only on what it uses (Interface Segregation Principle). - Producer can add methods without breaking consumers. - Testing requires only the methods the consumer calls. - No import cycle: consumer doesn't import producer's package. ## 2. Keep Interfaces Small The bigger the interface, the weaker the abstraction. ```go // ✅ Good — focused, composable type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } type ReadWriter interface { Reader Writer } // ❌ Bad — kitchen sink interface type FileManager interface { Read(path string) ([]byte, error) Write(path string, data []byte) error Delete(path string) error List(dir string) ([]string, error) Move(src, dst string) error Copy(src, dst string) error Stat(path string) (os.FileInfo, error) Watch(path string) (<-chan Event, error) } ``` Guideline: 1-3 methods is ideal. If you need more, compose smaller interfaces. ## 3. Accept Interfaces, Return Structs ```go // ✅ Good — accepts interface, returns concrete type func NewUserService(store UserReader, logger Logger) *UserService { return &UserService{store: store, logger: logger} } // ❌ Bad — returns interface (hides the concrete type for no reason) func NewUserService(store UserReader) UserServiceInterface { return &UserService{store: store} } ``` Return a concrete type so callers get full access to the type's methods. Returning an interface only makes sense when the function genuinely returns different concrete types based on input (factory pattern). ## 4. Verify Interface Compliance at Compile Time Use the blank identifier assignment to catch broken contracts early: ```go // Verify *PostgresStore implements service.UserReader at compile time var _ service.UserReader = (*PostgresStore)(nil) // Verify LogHandler implements http.Handler var _ http.Handler = (*LogHandler)(nil) // For value receivers: var _ fmt.Stringer = Status(0) ``` Place these immediately after the type declaration. They cost nothing at runtime and prevent silent contract breakage. ## 5. Don't Use Pointers to Interfaces ```go // ❌ Bad — pointer to interface is almost never correct func process(r *io.Reader) { ... } // ✅ Good — interface is already a pointer internally func process(r io.Reader) { ... } ``` An interface value is internally two pointers (type + data). A pointer to an interface is a pointer to a pointer — needless indirection. The only exception: when you need to replace the interface value itself (swap the implementation at runtime), which is extremely rare. ## 6. The Empty Interface `interface{}` (or `any` in Go 1.18+) means you've given up on type safety. Use it sparingly: ```go // ✅ Acceptable — generic container before generics / stdlib compatibility func Marshal(v any) ([]byte, error) // ✅ Better (Go 1.18+) — use generics instead of any func Map[T, U any](slice []T, fn func(T) U) []U { ... } // ❌ Bad — lazy interface design func Process(data any) any { ... } // what does this even do? ``` ## 7. Optional Configuration Is Not an Interface Problem Optional constructor settings are not a reason to introduce an interface. Use functional options or a config struct — the go-design-patterns skill owns both and the trade-off between them. ## 8. Common Interface Anti-Patterns ### Premature interfaces: ```go // ❌ Bad — interface defined before second implementation exists type Processor interface { Process(ctx context.Context, data []byte) error } type processor struct { ... } // only one implementation ever // ✅ Good — use concrete type until you need the abstraction type Processor struct { ... } // Add interface when you have 2+ implementations or need testing seam ``` "Don't design with interfaces, discover them." — Rob Pike ### Interface pollution: ```go // ❌ Bad — wrapping every struct in an interface "for testability" type UserServiceInterface interface { ... } type OrderServiceInterface interface { ... } type PaymentServiceInterface interface { ... } // 50 more interfaces with exactly one implementation each // ✅ Good — define interfaces where they're consumed // Each consumer declares only the methods IT needs ``` ### Sealed interfaces vs enums: A sealed interface — an unexported marker method, so only this package can add variants — is the right tool for a closed set of variants that carry different data, consumed with a type switch. When the variants carry no data, use a typed constant enum instead. Example: `references/embedding-and-assertions.md`. ## 9. Embedding and Type Assertions - Embedding promotes the whole method set. Embedding `sync.Mutex` in an exported struct publishes `Lock`/`Unlock`; use a named field unless the outer type truly *is* the inner one, extended. - An interface embedded in a struct satisfies it with methods that panic when called — fine for test fakes, a hidden runtime failure elsewhere. - Assert to an interface for optional capabilities (`w.(http.Flusher)`), always with comma-ok. - Generic methods (Go 1.27+) never satisfy an interface method; keep the methods of anything you define as an interface non-generic. ## Decision Checklist 1. **Do I need an interface here?** — Only if you have 2+ implementations, need a testing seam, or are crossing a package boundary. 2. **Where should it be defined?** — At the consumer, not the producer. 3. **How many methods?** — Fewer is better. 1-3 is ideal. 4. **Am I returning an interface?** — Probably shouldn't. Return concrete. 5. **Have I verified compliance?** — `var _ Interface = (*Type)(nil)` 6. **Am I embedding for convenience?** — named field unless every promoted method belongs on the outer type.