DI 的价值

依赖注入(DI)将“对象的创建”与“对象的使用”解耦,带来:可测试性提升(可注入 Mock)、启动流程清晰(构造器组织)、生命周期统一(资源启停)、模块边界明确。Uber Fx 是 Go 生态中工程化程度较高的 DI 框架,内置生命周期与日志/metrics 接口。

基本概念

  • Provide:向容器注册构造器(constructor)
  • Invoke:在容器完成图构建后调用入口函数
  • Lifecycle:统一资源启动/停止(连接池、后台协程等)
  • Module:组合多个 Provide/Invoke 形成可复用单元
  • Annotate/As/Name:标注、接口绑定、命名实例,解决多实现与歧义

快速上手

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Config struct{ DSN string }
type DB struct{ DSN string }

func NewConfig() *Config { return &Config{DSN: "mysql://user:pwd@tcp(localhost:3306)/app"} }
func NewDB(c *Config) (*DB, error) { return &DB{DSN: c.DSN}, nil }

func main() {
app := fx.New(
fx.Provide(NewConfig, NewDB),
fx.Invoke(func(lc fx.Lifecycle, db *DB) {
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error { fmt.Println("db ready"); return nil },
OnStop: func(ctx context.Context) error { fmt.Println("db closed"); return nil },
})
}),
)
app.Run()
}

模块化与接口绑定

1
2
3
4
5
6
7
8
9
10
11
type Repo interface { Save(ctx context.Context, v any) error }
type repo struct{ db *DB }

func NewRepo(db *DB) Repo { return &repo{db: db} }

var RepoModule = fx.Module("repo",
fx.Provide(
NewRepo,
fx.Annotate(NewRepo, fx.As(new(Repo))), // 接口绑定
),
)

服务与 HTTP 层模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
type Service struct{ r Repo }
func NewService(r Repo) *Service { return &Service{r} }

type HTTP struct{ s *Service }

func NewHTTP(lc fx.Lifecycle, s *Service) *HTTP {
h := &HTTP{s}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error { go h.serve(); return nil },
OnStop: func(ctx context.Context) error { return h.shutdown(ctx) },
})
return h
}

多实现与命名实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Cache interface { Get(k string) (string, bool) }
type lruCache struct{}
type redisCache struct{}

func NewLRU() Cache { return &lruCache{} }
func NewRedis() Cache { return &redisCache{} }

var CacheModule = fx.Options(
fx.Provide(
fx.Annotate(NewLRU, fx.As(new(Cache)), fx.ResultTags(`name:"local"`)),
fx.Annotate(NewRedis, fx.As(new(Cache)), fx.ResultTags(`name:"remote"`)),
),
fx.Invoke(func(@name("local") c1 Cache, @name("remote") c2 Cache) {
_ = c1; _ = c2
}),
)

配置、日志与可观测性注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type AppCfg struct{ Env string }
func LoadCfg() (*AppCfg, error) { return &AppCfg{Env: "prod"}, nil }

func NewLogger(cfg *AppCfg) *zap.Logger {
if cfg.Env == "prod" {
l, _ := zap.NewProduction()
return l
}
l, _ := zap.NewDevelopment()
return l
}

var ObsModule = fx.Module("obs",
fx.Provide(LoadCfg, NewLogger),
fx.Invoke(func(l *zap.Logger) { l.Info("logger ready") }),
)

测试与可替换性

利用 Provide 覆盖注入 Mock:

1
2
3
4
5
6
7
8
9
10
11
func TestSvc(t *testing.T) {
mockRepo := new(MockRepo)
app := fx.NewForTest(t,
fx.Provide(func() Repo { return mockRepo }),
fx.Provide(NewService),
fx.Invoke(func(s *Service) {
// 断言业务逻辑
}),
)
app.RequireStart().RequireStop()
}

最佳实践

  • Constructor 只做“构建”,不做重逻辑与 IO(放到 OnStart)
  • 明确模块边界,避免全局状态
  • 善用 Name/Group/As 解决多实现
  • 将 HTTP、Repo、Service、Config、Obs 切分模块,避免巨石 App

踩坑

  • 循环依赖:拆分接口或引入中间抽象
  • 过度注入:容器图臃肿,启动缓慢
  • OnStart 阻塞:确保耗时任务异步启动,并设定超时

FAQ

  • 如何优雅停机?OnStop 中关闭连接/等待协程退出;配合 K8s 优雅终止
  • 热更新配置?注入配置服务与 watch 机制,或结合 viper/fsnotify
  • 与 wire 相比?wire 静态注入、零运行时;fx 动态、带生命周期与模块化,选型看团队偏好