3. 接口定义规范:使用 Go interface 进行 API 契约设计

在微服务与分层架构中,接口(Interface)是连接调用方与实现方的唯一契约。Go 语言的 interface{} 机制天然支持隐式实现,这为 API 契约设计提供了极大的灵活性,但也带来了滥用风险。本章将围绕“如何用 Go interface 定义稳定、可 Mock、可治理的 API 契约”展开,给出工程级规范与案例。

3.1 为什么需要接口契约?

没有显式接口的代码,通常表现为:

  • 直接依赖具体类型:业务逻辑层直接调用 mysql.UserRepo,导致单元测试必须连接数据库。
  • 隐式耦合:修改底层实现时,上层代码被迫重新编译甚至修改。
  • 无法 Mock:无法在测试中替换为内存实现或桩对象。

引入接口契约后,上述问题得到系统性解决:

维度 无接口契约 有接口契约
依赖方向 上层依赖下层具体实现 上下层均依赖抽象接口
测试能力 需启动真实依赖(DB/RPC) 可注入 Mock 实现,单测秒级
变更影响 修改实现波及所有调用方 仅需替换实现,调用方无感
复用性 实现与业务逻辑强绑定 同一接口可对接多种实现
核心原则:依赖倒置(DIP)——高层模块不应依赖低层模块,二者都应依赖抽象。Go 的 interface 正是实现 DIP 的最佳语法载体。

3.2 接口定义规范:从命名到粒度

一个可治理的接口契约,必须满足以下规范:

3.2.1 命名规范

  • 接口名:以 “er” 结尾(如 UserRepository、Notifier),或直接描述行为(如 Storage、Cache)。
  • 方法名:动词 + 名词,清晰表达业务意图(如 SaveUser、GetOrderByID)。
  • 包名:接口定义在消费方(调用方)所在的包,而非实现方。例如 service 包定义 UserRepository,repository/mysql 包实现它。

3.2.2 粒度规范

  • 单一职责:一个接口只描述一组高度相关的操作。避免“万能接口”(如 UserService 包含 CRUD + 发邮件 + 生成报表)。
  • 方法数量:建议不超过 5 个方法。超过时考虑拆分为多个小接口。
  • 参数与返回值:使用具名结构体作为参数/返回值,避免超过 3 个的平铺参数。
反例(胖接口):
type UserService interface {
    Create(ctx context.Context, u *User) error
    Get(ctx context.Context, id int64) (*User, error)
    Update(ctx context.Context, u *User) error
    Delete(ctx context.Context, id int64) error
    SendWelcomeEmail(ctx context.Context, userID int64) error
    GenerateReport(ctx context.Context, start, end time.Time) ([]byte, error)
}
正例(拆分后):
type UserRepository interface {
    Create(ctx context.Context, u *User) error
    Get(ctx context.Context, id int64) (*User, error)
    Update(ctx context.Context, u *User) error
    Delete(ctx context.Context, id int64) error
}

type WelcomeEmailSender interface {
    Send(ctx context.Context, userID int64) error
}

type ReportGenerator interface {
    Generate(ctx context.Context, start, end time.Time) ([]byte, error)
}

3.3 接口契约的层级结构

在典型的三层架构(Handler → Service → Repository)中,接口契约位于 Service 层和 Repository 层的边界:

接口契约在分层架构中的位置 Handler / Transport Service Interface Service Impl Repository Interface 契约定义位置 Service Interface 定义在 service 包(消费方) Repository Interface 定义在 service 包(消费方)

关键规则:

  • 接口定义在消费方:Service 层定义它需要的 Repository 接口,而不是由 Repository 层定义。
  • 实现方隐式实现:MySQL、Mock、Cache 等 Repository 实现只需拥有对应方法签名,无需显式声明 implements。
  • 依赖注入:在 main 函数或 wire 中,将具体实现注入到消费方。

3.4 接口与 Mock 的协同

接口契约的最大工程收益之一,是让 Mock 依赖隔离成为可能。以下是一个典型的 Mock 实现模式:

// service/user_repository.go —— 接口定义(消费方)
package service

import "context"

type UserRepository interface {
    GetByID(ctx context.Context, id int64) (*User, error)
    Save(ctx context.Context, u *User) error
}

// service/user_service.go —— 业务逻辑
type UserService struct {
    repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) GetUser(ctx context.Context, id int64) (*User, error) {
    return s.repo.GetByID(ctx, id)
}
// service/mock_user_repository.go —— Mock 实现(测试用)
package service

import "context"

type MockUserRepository struct {
    GetByIDFunc func(ctx context.Context, id int64) (*User, error)
    SaveFunc    func(ctx context.Context, u *User) error
}

func (m *MockUserRepository) GetByID(ctx context.Context, id int64) (*User, error) {
    return m.GetByIDFunc(ctx, id)
}

func (m *MockUserRepository) Save(ctx context.Context, u *User) error {
    return m.SaveFunc(ctx, u)
}
// service/user_service_test.go —— 单元测试
func TestGetUser(t *testing.T) {
    mock := &MockUserRepository{
        GetByIDFunc: func(ctx context.Context, id int64) (*User, error) {
            return &User{ID: id, Name: "Alice"}, nil
        },
    }
    svc := NewUserService(mock)
    user, err := svc.GetUser(context.Background(), 1)
    // 断言 user.Name == "Alice" 且 err == nil
}
工程要点:Mock 实现与接口定义放在同一个包(service),避免循环导入。Mock 结构体使用函数字段(GetByIDFunc),让每个测试用例可以自定义行为,无需定义多个 Mock 子类。

3.5 接口治理清单

在 API 设计治理中,接口契约需要纳入以下检查项:

检查项 标准 违规示例
接口位置 定义在消费方包内 定义在实现方包(如 repository/mysql)
方法数量 ≤ 5 个 一个接口包含 10+ 方法
参数结构 使用结构体参数(参数 ≥ 3 时) func Create(name, email, phone string, age int) error
返回值 优先返回 error,业务数据通过返回值 通过 out 参数或全局变量传递结果
Mock 可测性 每个接口至少有一个 Mock 实现 接口无 Mock,测试依赖真实 DB
文档注释 每个方法必须有注释说明行为 无注释,或注释与实现不符

3.6 常见陷阱与避坑

  • 陷阱1:接口定义在实现方。导致消费方被迫 import 实现包,破坏依赖方向。
  • 陷阱2:接口过于抽象。如 type Repository interface { Execute(ctx context.Context, query string, args ...interface{}) (interface{}, error) },丧失类型安全。
  • 陷阱3:在接口中使用具体类型。如 GetByID(ctx context.Context, id int64) (*sql.Row, error),将 SQL 细节泄露到契约中。
  • 陷阱4:为每个结构体都定义接口。只有存在多个实现或需要 Mock 时,才引入接口。否则过度设计。
治理红线:禁止在接口方法中使用 interface{} 作为参数或返回值,除非是序列化/反序列化等通用场景。否则将导致运行时类型断言泛滥,丧失编译期类型检查。

3.7 小结

Go interface 是 API 契约设计的核心语法工具。通过将接口定义在消费方、控制粒度、配合 Mock 实现,可以实现:

  • 依赖倒置:高层不依赖低层,两者依赖抽象。
  • 测试隔离:任意依赖均可替换为 Mock,单测秒级运行。
  • 架构演进:替换底层实现(如 MySQL → Redis + MySQL)时,业务层零改动。