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 动态、带生命周期与模块化,选型看团队偏好