go-interface-design

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go Interface Design

Go 接口设计

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.
Go 的接口是隐式的。这是该语言最重要的设计特性,大多数从 Java 或 C# 转来的开发者最初都会理解错误。

1. The Cardinal Rule: Define Interfaces at the Consumer

1. 核心原则:在消费者端定义接口

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.
行为的接口应由消费者定义,而非提供者:
go
// ❌ 错误示例——生产者定义接口(Java思维)
// package store
type UserStore interface {      // 与实现代码一同定义
    GetByID(ctx context.Context, id string) (*User, error)
    Create(ctx context.Context, user *User) error
    // ... 还有15个其他方法
}

type PostgresStore struct { ... }
func (s *PostgresStore) GetByID(...) { ... }
func (s *PostgresStore) Create(...) { ... }

// ✅ 正确示例——消费者定义自身所需的接口
// package service
type UserReader interface {     // 仅包含当前服务需要的方法
    GetByID(ctx context.Context, id string) (*domain.User, error)
}

type UserService struct {
    store UserReader  // 依赖窄接口
}

// package store(此处无需定义接口)
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 会隐式满足 service.UserReader 的要求——无需额外声明
为什么这一点很重要:
  • 消费者仅依赖自身实际使用的方法(接口隔离原则)。
  • 生产者新增方法不会破坏消费者的代码。
  • 测试仅需实现消费者调用的方法。
  • 避免导入循环:消费者无需导入生产者的包。

2. Keep Interfaces Small

2. 保持接口精简

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.
接口越大,抽象性越弱。
go
// ✅ 良好示例——聚焦、可组合
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
}

// ❌ 糟糕示例——大而全的接口
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)
}
指导原则:1-3个方法是理想状态。如果需要更多方法,可通过组合更小的接口实现。

3. Accept Interfaces, Return Structs

3. 接受接口,返回结构体

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).
go
// ✅ 良好示例——接受接口,返回具体类型
func NewUserService(store UserReader, logger Logger) *UserService {
    return &UserService{store: store, logger: logger}
}

// ❌ 糟糕示例——返回接口(毫无必要地隐藏具体类型)
func NewUserService(store UserReader) UserServiceInterface {
    return &UserService{store: store}
}
返回具体类型,以便调用者能访问该类型的全部方法。只有当函数确实需要根据输入返回不同具体类型时(工厂模式),返回接口才有意义。

4. Verify Interface Compliance at Compile Time

4. 在编译期验证接口合规性

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.
使用空白标识符赋值来尽早发现契约破坏问题:
go
// 在编译期验证 *PostgresStore 是否实现了 service.UserReader
var _ service.UserReader = (*PostgresStore)(nil)

// 验证 LogHandler 是否实现了 http.Handler
var _ http.Handler = (*LogHandler)(nil)

// 对于值接收者:
var _ fmt.Stringer = Status(0)
将这些代码放在类型声明之后。它们在运行时不会产生任何开销,还能防止契约被静默破坏。

5. Don't Use Pointers to Interfaces

5. 不要使用接口指针

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.
go
// ❌ 糟糕示例——接口指针几乎永远是错误的用法
func process(r *io.Reader) { ... }

// ✅ 良好示例——接口内部已包含指针
func process(r io.Reader) { ... }
接口值在内部包含两个指针(类型指针 + 数据指针)。指向接口的指针是指针的指针,属于不必要的间接引用。
唯一的例外场景:你需要在运行时替换接口值本身(切换实现),这种情况极为罕见。

6. The Empty Interface

6. 空接口

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?
interface{}
(Go 1.18+ 中可写为
any
)意味着你放弃了类型安全。请谨慎使用:
go
// ✅ 可接受场景——泛型出现前的通用容器/标准库兼容性
func Marshal(v any) ([]byte, error)

// ✅ 更优方案(Go 1.18+)——使用泛型替代 any
func Map[T, U any](slice []T, fn func(T) U) []U { ... }

// ❌ 糟糕示例——偷懒的接口设计
func Process(data any) any { ... } // 这个函数到底做什么?

7. Functional Options Pattern

7. 函数式选项模式

When a constructor needs optional configuration, use functional options instead of a config struct with an interface:
go
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),
)
当构造函数需要可选配置时,使用函数式选项而非带接口的配置结构体:
go
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,  // 合理的默认值
        logger:  zap.NewNop(),      // 默认空操作日志器
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

// 使用示例
srv := NewServer(":8080",
    WithTimeout(60 * time.Second),
    WithLogger(logger),
)

8. Common Interface Anti-Patterns

8. 常见接口反模式

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
go
// ❌ 糟糕示例——在第二个实现出现前就定义接口
type Processor interface {
    Process(ctx context.Context, data []byte) error
}

type processor struct { ... }  // 永远只有一个实现

// ✅ 良好示例——在需要抽象前使用具体类型
type Processor struct { ... }
// 当有2个及以上实现或需要测试接缝时再添加接口
“不要为接口而设计,要在实践中发现接口。”——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
go
// ❌ 糟糕示例——为每个结构体都套一层接口“为了可测试性”
type UserServiceInterface interface { ... }
type OrderServiceInterface interface { ... }
type PaymentServiceInterface interface { ... }
// 还有50个接口,每个都只有一个实现

// ✅ 良好示例——在消费端定义接口
// 每个消费者仅声明自身需要的方法

Misusing interfaces for enums:

误用接口实现枚举:

go
// ❌ 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
)
go
// ❌ 糟糕示例——接口被用作枚举/求和类型
type Shape interface {
    isShape()
}
type Circle struct{}
func (Circle) isShape() {}

// ✅ 更优方案——密封接口模式(如果确实需要)
// 或者直接使用带类型的常量
type ShapeKind int
const (
    ShapeCircle ShapeKind = iota
    ShapeRectangle
)

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)
  1. 我真的需要在这里定义接口吗? —— 只有当你有2个及以上实现、需要测试接缝,或者正在跨越包边界时才需要。
  2. 接口应该定义在哪里? —— 在消费者端,而非生产者端。
  3. 接口应该包含多少个方法? —— 越少越好,1-3个是理想状态。
  4. 我应该返回接口吗? —— 大概率不应该,返回具体类型。
  5. 我验证过接口合规性了吗? —— 使用
    var _ Interface = (*Type)(nil)
    进行验证