接口与组合¶
学习目标¶
学完本章后,学习者应该能够:
- 理解 Go interface 的隐式实现机制。
- 掌握小接口设计和依赖反转。
- 理解组合优于继承的工程含义。
- 能在后端项目中设计可测试、可替换的依赖边界。
interface 基础¶
Go 中类型只要实现了接口所需方法,就自动满足接口,不需要显式声明。
任何拥有这个方法的类型都实现了 UserRepo。
小接口设计¶
接口应该由使用方定义,尽量小:
小接口更容易 mock,更容易复用,也不容易把实现细节泄露给调用方。
空接口与类型断言¶
any 是 interface{} 的别名,可以表示任意类型。
不要滥用 any。如果函数需要明确类型,就使用明确类型。类型断言失败会 panic,安全写法是:
组合优于继承¶
Go 没有传统类继承,鼓励通过组合复用能力:
组合让依赖更明确,也减少了复杂继承层级。
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 整个接口,维护成本极高。
修复方式是按使用方拆小接口,例如 UserGetter、AuditWriter、APIKeyRepo,让每个 service 只依赖自己需要的方法。
实战任务¶
为“创建 API Key”功能设计接口边界:
- Service 需要检查名称是否重复。
- Service 需要保存 API Key。
- 测试时可以替换存储实现。
- 接口不要暴露无关方法。
参考答案
可以定义一个刚好满足 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 都定义接口。