Loading...
Loading...
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". Do NOT use for HTTP handler patterns (use go-api-design) or general code review (use go-code-review).
npx skill4agent add eduardo-sl/go-agent-skills go-interface-design// ❌ 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// ✅ 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)
}// ✅ 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}
}// 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)// ❌ 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) { ... }interface{}any// ✅ 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?type Option func(*Server)
func WithTimeout(d time.Duration) Option {
return func(s *Server) { s.timeout = d }
}
func WithLogger(l Logger) Option {
return func(s *Server) { s.logger = l }
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{
addr: addr,
timeout: 30 * time.Second, // sensible default
logger: zap.NewNop(), // default no-op logger
}
for _, opt := range opts {
opt(s)
}
return s
}
// Usage
srv := NewServer(":8080",
WithTimeout(60 * time.Second),
WithLogger(logger),
)// ❌ 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// ❌ 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// ❌ Bad — interface used as enum/sum type
type Shape interface {
isShape()
}
type Circle struct{}
func (Circle) isShape() {}
// ✅ Better — sealed interface pattern (if you need it)
// Or just use constants with a type
type ShapeKind int
const (
ShapeCircle ShapeKind = iota
ShapeRectangle
)var _ Interface = (*Type)(nil)