跳转至

接口与组合

学习目标

学完本章后,学习者应该能够:

  1. 理解 Go interface 的隐式实现机制。
  2. 掌握小接口设计和依赖反转。
  3. 理解组合优于继承的工程含义。
  4. 能在后端项目中设计可测试、可替换的依赖边界。

interface 基础

Go 中类型只要实现了接口所需方法,就自动满足接口,不需要显式声明。

type UserRepo interface {
    Get(ctx context.Context, id int64) (User, error)
}

任何拥有这个方法的类型都实现了 UserRepo

小接口设计

接口应该由使用方定义,尽量小:

type UserGetter interface {
    Get(ctx context.Context, id int64) (User, error)
}

小接口更容易 mock,更容易复用,也不容易把实现细节泄露给调用方。

空接口与类型断言

anyinterface{} 的别名,可以表示任意类型。

func Print(v any) {
    fmt.Println(v)
}

不要滥用 any。如果函数需要明确类型,就使用明确类型。类型断言失败会 panic,安全写法是:

name, ok := v.(string)
if !ok {
    return fmt.Errorf("name must be string")
}

组合优于继承

Go 没有传统类继承,鼓励通过组合复用能力:

type Service struct {
    repo   UserRepo
    logger *slog.Logger
}

组合让依赖更明确,也减少了复杂继承层级。

Go 后端实际应用例子

例子一:Service 依赖接口而不是具体数据库

type APIKeyRepo interface {
    Create(ctx context.Context, key APIKey) error
    FindByName(ctx context.Context, tenantID, name string) (APIKey, error)
}

type APIKeyService struct {
    repo APIKeyRepo
}

func NewAPIKeyService(repo APIKeyRepo) *APIKeyService {
    return &APIKeyService{repo: repo}
}

这样 service 测试时可以传入 fake repo,不必启动真实数据库。

例子二:中间件组合

type Middleware func(http.Handler) http.Handler

func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        h = middlewares[i](h)
    }
    return h
}

HTTP 中间件就是典型的组合思想。

常见误区

  • 误区一:先在实现方定义大接口。

Go 更推荐在使用方定义刚好需要的方法集合。

  • 误区二:所有结构体都要配一个接口。

如果没有替换、测试或抽象需求,直接使用具体类型更简单。

  • 误区三:any 可以让代码更通用。

滥用 any 会丢失类型安全,让错误从编译期推迟到运行期。

线上问题案例

某项目定义了一个巨大的 Database 接口,包含用户、订单、权限、审计等几十个方法。任何 service 测试都必须 mock 整个接口,维护成本极高。

修复方式是按使用方拆小接口,例如 UserGetterAuditWriterAPIKeyRepo,让每个 service 只依赖自己需要的方法。

实战任务

为“创建 API Key”功能设计接口边界:

  1. Service 需要检查名称是否重复。
  2. Service 需要保存 API Key。
  3. 测试时可以替换存储实现。
  4. 接口不要暴露无关方法。
参考答案

可以定义一个刚好满足 service 需求的接口:

type APIKeyRepository interface {
    FindByName(ctx context.Context, tenantID, name string) (APIKey, error)
    Create(ctx context.Context, key APIKey) error
}

APIKeyService 依赖这个接口。生产环境注入数据库实现,单元测试注入 fake 实现。接口不需要包含删除、分页、统计等暂时不需要的方法。

面试题

1. Go 的接口是如何实现的?

参考答案

Go 接口是隐式实现。一个类型只要拥有接口要求的全部方法,就自动实现该接口,不需要显式声明 implements

这种机制降低了耦合,让使用方可以定义自己需要的小接口,具体类型无需知道所有调用方。

2. 为什么推荐小接口?

参考答案

小接口表达更精确的依赖,调用方只依赖自己需要的方法。它更容易测试、mock 和替换,也减少实现方被迫实现无关方法的成本。

大接口容易变成“上帝接口”,让模块之间强耦合。

3. 什么时候不需要接口?

参考答案

如果没有多实现、替换、测试隔离或抽象边界需求,直接使用具体类型更简单。为了接口而接口会增加理解成本。

Go 中接口通常在使用方出现需求后再抽取,而不是一开始为每个 struct 都定义接口。