接口隔离与依赖注入的轻量做法是什么?
一句话结论:Go 的接口是隐式实现,最佳实践是“消费方定义小接口”:需要什么能力就声明什么方法,别把外部大接口照搬进来;依赖注入不必上框架,把依赖通过 NewXxx(...) 构造器传进结构体即可(构造器注入),配合接口替身就能轻松单测。副作用是避免包级单例与隐藏的全局依赖。
三个轻量原则
- 小接口:方法越少越好,甚至可以只有一个方法(如 io.Reader、http.Handler)。
- 消费方定义:接口放在“用它的包”里,而不是“实现它的包”里。
- 构造时注入:依赖以参数/字段进入组件,不靠全局变量或 init 隐式获取。
代码示例
// 使用方定义最小接口,而不是引入重型 SDK 类型
type KeyValueStore interface {
Get(ctx context.Context, key string) (string, error)
Set(ctx context.Context, key string, val string) error
}
type UserService struct {
store KeyValueStore // 依赖的是“能力”而非具体实现
log *slog.Logger
}
func NewUserService(store KeyValueStore, log *slog.Logger) *UserService {
return &UserService{store: store, log: log}
}
func (s *UserService) Greet(ctx context.Context, id string) (string, error) {
name, err := s.store.Get(ctx, "user:"+id)
if err != nil {
s.log.Error("get user failed", "id", id, "err", err)
return "", err
}
return "hi " + name, nil
}
测试时传一个内存假实现即可,不需要起 Redis/DB:
type fakeStore struct{ data map[string]string }
func (f fakeStore) Get(ctx context.Context, k string) (string, error) { return f.data[k], nil }
func (f fakeStore) Set(ctx context.Context, k, v string) error { f.data[k] = v; return nil }
组装在哪做
- 轻量做法:在 main 或独立的 wire.go 里手工组装依赖图,简单直观。
- 依赖图很大时再考虑 wire 等代码生成工具;不要为了“注入”引入运行时反射容器。
- 全局变量能少则少:它们让测试顺序互相污染,也掩盖真实的依赖关系。
常见追问 / 记忆点
- 追问:接口定义在实现包里可以吗?——可以但不推荐;实现包提供具体类型,接口归使用方,避免实现包反向依赖使用方。
- 追问:什么时候不需要接口?——只有一个真实实现、也没有测试替身需求时,直接用具体类型更简单。
- 记忆点:小接口 + 消费方定义 + 构造器注入;测试替身是依赖注入最大的红利。