引子:当依赖注入遇见编译时代码生成
在 Go 项目中管理依赖,我们常常陷入两难:要么使用 wire 忍受繁琐的集合与占位符,要么使用 fx 承担运行时反射的开销和 panic 风险。而 dig (github.com/shanjunmei/dig)提供了一种新思路------它采用与 fx 几乎一模一样的 API,却在编译时生成纯 Go 代码,最终产物零运行时依赖、零反射。
本文将以一个真实的模块化项目为例,带你从零开始使用 dig,并给出从 wire / fx 迁移到 dig 的完整步骤。所有代码均来自项目仓库的 example/ 目录,你可以直接克隆运行。
1. 安装
bash
go get github.com/shanjunmei/dig@v1.0.8
go install github.com/shanjunmei/dig/cmd/digen@latest
要求 Go 1.21+。
2. 项目结构概览
我们使用的示例项目结构如下:
ruby
example/
├── app/ # 应用组装层
│ ├── app.go # InitApp() 入口,调用 dig.Build
│ └── dig_gen.go # 生成的代码(忽略)
├── common/ # 公共配置
│ └── config.go
├── internal/logger/ # 内部日志包
│ └── logger.go
├── main.go # 入口,创建外部依赖并启动
├── role/ # 角色模块(含子模块 repository)
│ ├── module.go
│ ├── role.go
│ └── repository/
│ ├── module.go
│ └── repo.go
└── user/ # 用户模块(含子模块 repository)
├── module.go
├── user.go
└── repository/
├── module.go
└── repo.go
这是一个典型的微服务模块划分:user 和 role 各自独立,并拥有自己的 repository 子模块。app 层负责组装所有模块,common 和 internal/logger 作为全局基础设施。
3. 从入口开始:main.go 与外部依赖
main.go 负责创建外部依赖(配置和日志),然后调用 app.InitApp 得到启动函数并执行。
go
package main
import (
"context"
"fmt"
"github.com/shanjunmei/dig/example/app"
"github.com/shanjunmei/dig/example/common"
"github.com/shanjunmei/dig/example/internal/logger"
)
func main() {
cfg := common.NewConfig()
log := logger.NewLogger()
start := app.InitApp(cfg, log) // 传入外部依赖
if err := start(context.Background()); err != nil {
fmt.Printf("App failed: %v\n", err)
}
}
关键点 :cfg 和 log 作为 InitApp 的参数,会自动成为容器中的供应值 ,无需显式调用 dig.Supply。任何需要 *common.Config 或 *logger.Logger 的 Provide 或 Invoke 都能直接获取它们。
4. 应用组装层:app/app.go
app.go 是 dig.Build 的调用处,也是整个依赖图的组装中心。
go
//go:build digen
//go:generate digen ./...
package app
import (
"context"
"github.com/shanjunmei/dig/example/common"
"github.com/shanjunmei/dig/example/internal/logger"
"github.com/shanjunmei/dig/example/role"
"github.com/shanjunmei/dig/example/user"
"github.com/shanjunmei/dig"
)
func InitApp(cfg *common.Config, log *logger.Logger) func(context.Context) error {
return dig.Build(
user.Module(), // 用户模块
role.Module(), // 角色模块
// 额外提供一个泛型 Store[string]
dig.Provide(func() *user.Store[string] {
s := user.NewStore[string]()
s.Add("hello")
return s
}),
// 最后执行一个 Invoke,依赖 cfg、log 和 Store
dig.Invoke(func(s *user.Store[string], cfg *common.Config, log *logger.Logger) error {
log.Println("App Invoke: store len =", len(s.GetAll()))
return nil
}),
)
}
这里我们可以看到:
user.Module()和role.Module()是独立的模块,内部又嵌套了自己的子模块。dig.Provide支持闭包,但闭包不能捕获InitApp的局部变量 (这里user.NewStore是包级函数,合法)。dig.Invoke接收一个函数,其参数由容器自动解析。多个Invoke会按顺序执行,若有返回error则中断。
5. 模块的定义:以 user 模块为例
5.1 顶层模块 user/module.go
go
package user
import (
"github.com/shanjunmei/dig"
"github.com/shanjunmei/dig/example/user/repository"
)
func Module() dig.Option {
return dig.Module(
dig.Provide(NewStore[int]), // 泛型构造函数
repository.Module(), // 嵌套子模块
dig.Provide(func() string { return "user-module" }), // 提供字符串
dig.Invoke(ProcessStore[int]), // 泛型 Invoke
)
}
dig.Module将多个Option组合成一个逻辑单元,没有名称 (区别于fx.Module)。dig.Provide(NewStore[int])显式实例化泛型类型。repository.Module()嵌套了子模块,依赖关系自动合并。dig.Invoke(ProcessStore[int])会在启动时调用,并自动注入*Store[int]。
5.2 子模块 user/repository/module.go
go
package repository
import "github.com/shanjunmei/dig"
func Module() dig.Option {
return dig.Module(
dig.Provide(NewRepository[string]),
dig.Invoke(func(r *Repository[string]) { r.Print() }),
)
}
子模块同样使用 dig.Module 组织,提供泛型 Repository[string] 并在启动时打印其内容。
6. role 模块的类似设计
role/module.go 展示了 dig.Supply 和多个 Invoke 的用法:
go
package role
import (
"fmt"
"github.com/shanjunmei/dig"
"github.com/shanjunmei/dig/example/role/repository"
)
func Module() dig.Option {
return dig.Module(
dig.Provide(NewServer),
dig.Supply(100), // 直接提供 int
repository.Module(),
dig.Supply(Config("production")), // 提供自定义类型
dig.Invoke(func(cfg Config) { fmt.Printf("Config supplied: %s\n", cfg) }),
dig.Invoke(func(s *Server) { s.Run() }),
)
}
dig.Supply可以注入任意值(不仅仅是常量),这里传入了100和Config("production")。- 多个
Invoke会依次根据注册顺序执行(在确保依赖满足的情况下,框架会自动保障依赖顺序问题)。
7. 生成与运行
在项目根目录执行:
bash
cd example
digen ./... # 或 go generate ./...
go run .
预期输出(顺序可能略有不同):
ini
UserRepo: []
ProcessStore: items count=0
RoleRepo: []
Config supplied: production
Role Server 100 running
App Invoke: store len = 1
生成的 dig_gen.go 是纯 Go 代码,不导入 dig,你可以直接检查它来理解依赖解析的过程。
8. 核心 API 与注意事项
| 函数 | 作用 |
|---|---|
dig.Build(...Option) func(context.Context) error |
构建容器,返回启动函数 |
dig.Provide(any) Option |
注册构造函数(返回值作为提供类型) |
dig.Supply(any) Option |
直接注入一个值(任意表达式) |
dig.Invoke(any) Option |
注册启动时执行的函数,参数自动注入 |
dig.Module(...Option) Option |
将选项组合成模块(无名称) |
重要约束:
- 闭包捕获限制 :
Provide/Invoke中的闭包不能捕获外层函数的局部变量,只能使用包级符号或字面量。这是因为生成器会将闭包提升为包级函数。 - 泛型显式实例化 :所有泛型函数/类型在调用时必须指明类型参数,如
NewStore[int]。 - 外部参数自动供应 :
InitApp的参数会自动成为容器中的供应值,无需dig.Supply。 - 类型冲突 :若需要多个相同底层类型的值(如多个
bool),请定义不同的类型别名区分。
9. 迁移指南:从 Wire 或 Fx 迁移到 dig
9.1 从 Google Wire 迁移
典型 Wire 写法:
go
//go:build wireinject
package main
import "github.com/google/wire"
func InitApp() *App {
wire.Build(
NewConfig,
NewDB,
wire.Value(DefaultTimeout),
wire.Bind(new(Logger), new(*MyLogger)),
)
return nil
}
迁移到 dig 的步骤:
- 将
wire.NewSet替换为dig.Module(可选,可直接平铺)。 - 将
wire.Value(v)替换为dig.Supply(v),现在v可以是任意表达式。 - 删除
wire.Bind,改为让构造函数直接返回接口类型。 - 删除
return nil,让dig.Build直接返回func(ctx) error。 - 将启动逻辑(如
app.Run())放入dig.Invoke。 - 构建标签改为
//go:build digen,并添加//go:generate digen ./...。 - 运行
digen替代wire gen。
迁移后:
go
//go:build digen
//go:generate digen ./...
package main
import (
"context"
"github.com/shanjunmei/dig"
)
func InitApp() func(context.Context) error {
return dig.Build(
dig.Provide(NewConfig, NewDB),
dig.Supply(DefaultTimeout),
dig.Provide(func() Logger { return &MyLogger{} }),
dig.Invoke(func(app *App) error { return app.Start() }),
)
}
9.2 从 Uber Fx 迁移
典型 Fx 写法:
go
package main
import "go.uber.org/fx"
func main() {
app := fx.New(
fx.Provide(NewConfig, NewDB),
fx.Supply(DefaultTimeout),
fx.Invoke(func(srv *Server) { srv.Run() }),
)
app.Run()
}
迁移到 dig 的步骤:
- 将
fx.New替换为dig.Build,它返回func(ctx) error。 - 将
app.Run()改为调用返回的函数,并传入context。 - 移除
fx.Module中的名称字符串(dig 模块无名称)。 - 如果使用了
OnStart/OnStop钩子,可在dig.Invoke中手动处理,或保持简洁。 - 添加
//go:build digen和//go:generate digen ./...。 - 运行
digen,删除fx依赖。
迁移后:
go
// di.go
//go:build digen
//go:generate digen ./...
package main
import (
"context"
"github.com/shanjunmei/dig"
)
func InitApp() func(context.Context) error {
return dig.Build(
dig.Provide(NewConfig, NewDB),
dig.Supply(DefaultTimeout),
dig.Invoke(func(srv *Server) error { return srv.Run() }),
)
}
// main.go
func main() {
if err := InitApp()(context.Background()); err != nil {
panic(err)
}
}
10. 总结
dig 通过编译时代码生成,将 Fx 的优雅 API 与 Wire 的零运行时开销融为一体。你只需:
- 用
//go:build digen隔离生成代码; - 使用熟悉的
Provide/Supply/Invoke/Module组织依赖; - 运行
digen ./...,即可获得纯 Go 的启动函数。
无论你是从 Wire 迁移(摆脱繁琐的集合和占位符),还是从 Fx 迁移(消除运行时反射和 panic 风险),dig 都提供了平滑的过渡路径。更棒的是,生成的代码完全独立,你甚至可以将它提交到仓库,其他开发者无需安装 dig 也能编译。
现在就去体验吧:
bash
go get github.com/shanjunmei/dig@v1.0.8
go install github.com/shanjunmei/dig/cmd/digen@latest
让你的 Go 依赖注入变得既清爽又高效。
本文示例基于 dig v1.0.8 及项目 https://github.com/shanjunmei/dig/example/ 目录,欢迎在 GitHub 上提出 Issue 或 PR。