接口隔离与依赖注入的轻量做法是什么?

一句话结论: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 等代码生成工具;不要为了“注入”引入运行时反射容器。
  • 全局变量能少则少:它们让测试顺序互相污染,也掩盖真实的依赖关系。

常见追问 / 记忆点

  • 追问:接口定义在实现包里可以吗?——可以但不推荐;实现包提供具体类型,接口归使用方,避免实现包反向依赖使用方。
  • 追问:什么时候不需要接口?——只有一个真实实现、也没有测试替身需求时,直接用具体类型更简单。
  • 记忆点:小接口 + 消费方定义 + 构造器注入;测试替身是依赖注入最大的红利。
笔记加载中…