关于 go-dependency-injection
go-dependency-injection 是面向 Go 开发的技能,介绍显式传递依赖和在组合根集中组装,并说明何时适合采用 DI 工具。内容以手动组装为默认方式,也比较了多种工具模型。
主要功能
- 01通过构造函数注入依赖
- 02在 main 或 run 集中组装
- 03避免可变全局状态
- 04为小型接口注入函数
- 05DI 工具取舍建议
适用场景
- 01组织服务依赖
- 02移除包级单例
- 03使用替身测试组件
- 04选择 Go 依赖注入方案
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ eduardo-sl/ go-agent-skills --skill go-dependency-injection安装后在 Claude Code 中输入/go-dependency-injection确认已加载。
详细介绍
Go 依赖注入
在 Go 中,DI 是一种模式,而非框架:将依赖传递给构造函数,并在 main 中显式完成所有组装。只有在手动组装确实造成明显负担时,才考虑使用框架。
社区默认做法。如果项目技能或 Agent 配置规则明确取代了
go-dependency-injection,则以该规则为准。
1. 构造函数注入——默认做法
// ✅ Good — dependencies are explicit parameters
type OrderService struct {
repo OrderRepository
payments PaymentGateway
logger *slog.Logger
}
func NewOrderService(repo OrderRepository, payments PaymentGateway, logger *slog.Logger) *OrderService {
return &OrderService{repo: repo, payments: payments, logger: logger}
}
// ❌ Bad — hidden dependencies reached through globals
func (s *OrderService) Place(ctx context.Context, o Order) error {
db := database.Get() // global singleton
log.Printf("placing order") // global logger
// untestable without touching process-wide state
}
规则:
- 对服务调用的依赖,接受接口;构造函数返回具体类型。
- 在函数签名中明确列出每项依赖——如果列表显得过长,说明类型承担的职责也过多(应将其拆分),不要为了缩短列表而隐藏依赖。
- 在构造函数中验证必需的依赖,并返回错误(或接受 nil 安全的默认值,例如
logger = slog.Default())。
2. 组合根
所有组装都集中在一个位置——main(或它调用的 run 函数)。构造顺序遵循依赖顺序,由编译器检查:
func run(ctx context.Context, cfg Config) error {
db, err := store.Open(ctx, cfg.DatabaseURL)
if err != nil {
return fmt.Errorf("open db: %w", err)
}
defer db.Close()
orderRepo := store.NewOrderRepo(db)
payments := stripe.NewGateway(cfg.StripeKey)
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
orders := service.NewOrderService(orderRepo, payments, logger)
server := handler.NewServer(cfg.Addr, orders)
return server.ListenAndServe(ctx)
}
- 任何包都不应自行构建依赖;它应接收依赖。
- 不使用
init()组装,也不声明包级变量var DB *sql.DB。 - 如果两个二进制程序需要不同的组装方式,就使用两个 main,共用相同的组件。
3. 消除全局状态
// ❌ Before — package-level singleton
var defaultClient *api.Client
func Fetch(id string) (*Item, error) {
return defaultClient.Get(id)
}
// ✅ After — the dependency moves into a struct
type Fetcher struct {
client *api.Client
}
func NewFetcher(c *api.Client) *Fetcher { return &Fetcher{client: c} }
func (f *Fetcher) Fetch(id string) (*Item, error) {
return f.client.Get(id)
}
旧代码库的迁移路径:引入结构体,保留一个已弃用的包级包装函数,让它委托给在 main 中构建的单个实例;逐步迁移调用方,然后删除包装函数。
可接受的包级状态:纯常量、已编译的正则表达式,以及由 sync.Once 保护且不持有任何配置的进程级单例。
4. 小型接缝使用函数依赖
对于一个函数而言,完整接口过于繁琐——注入函数即可:
type Service struct {
now func() time.Time
genID func() string
publish func(ctx context.Context, e Event) error
}
// Production: Service{now: time.Now, genID: uuid.NewString, publish: bus.Publish}
// Test: Service{now: fixedTime, genID: constID, publish: capture}
5. 何时值得承担框架的复杂性
手动组装的扩展能力往往超出预期——100 行的 run 函数仍然清晰易读,并且会由编译器检查。当组装涉及数百个组件,或多个团队共用一个二进制程序时,可以考虑使用工具。
| 工具 | 模式 | 权衡 |
|---|---|---|
| google/wire | 编译时代码生成 | 组装过程仍是普通 Go 代码,并由编译器检查;但会增加一个代码生成步骤 |
| uber-go/fx | 运行时容器 + 生命周期 | 管理应用生命周期(启动/停止钩子);错误会在运行时出现,堆栈跟踪中也会有魔法般的行为 |
| uber-go/dig | 运行时容器(fx 的核心) | 具有相同的运行时权衡,但没有生命周期层 |
决策规则:优先手动组装;如果必须使用代码生成,则优先选择 wire(编译时失败胜过启动时失败);只有在同时需要其生命周期管理,且团队接受运行时容器时,才采用 fx。
切勿混用模式:只使用一个组合根和一种机制。
6. Wire 示例(选择使用时)
//go:build wireinject
func InitializeServer(cfg Config) (*handler.Server, error) {
wire.Build(
store.Open,
store.NewOrderRepo,
stripe.NewGateway,
service.NewOrderService,
handler.NewServer,
)
return nil, nil // replaced by generated code
}
wire 会生成按顺序调用构造函数的代码;生成的文件应像手写代码一样提交并审查。
验证清单
- 每个服务/处理程序都通过构造函数参数接收依赖
- 不存在包级可变单例(
var DB、var logger、Get()访问器) - 所有组装都集中在 main/run 中——不通过
init()构造 - 依赖以接口(或函数)形式接收,返回具体类型
- 构造函数会验证必需的依赖
- 通过传入伪对象即可测试组件——测试中不设置进程级全局状态
- 如果使用 DI 工具:只使用一种,且仅用于组合根
go build ./...通过——组装错误会在编译时显现
评论
支持 Markdown