mcpField
G
go-dependency-injection

go-dependency-injection

github.com/eduardo-sl开发8396 次安装2 周前更新
代码仓库

关于 go-dependency-injection

go-dependency-injection 是面向 Go 开发的技能,介绍显式传递依赖和在组合根集中组装,并说明何时适合采用 DI 工具。内容以手动组装为默认方式,也比较了多种工具模型。

主要功能

  1. 01通过构造函数注入依赖
  2. 02在 main 或 run 集中组装
  3. 03避免可变全局状态
  4. 04为小型接口注入函数
  5. 05DI 工具取舍建议

适用场景

  1. 01组织服务依赖
  2. 02移除包级单例
  3. 03使用替身测试组件
  4. 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 会生成按顺序调用构造函数的代码;生成的文件应像手写代码一样提交并审查。

验证清单

  1. 每个服务/处理程序都通过构造函数参数接收依赖
  2. 不存在包级可变单例(var DB、var logger、Get() 访问器)
  3. 所有组装都集中在 main/run 中——不通过 init() 构造
  4. 依赖以接口(或函数)形式接收,返回具体类型
  5. 构造函数会验证必需的依赖
  6. 通过传入伪对象即可测试组件——测试中不设置进程级全局状态
  7. 如果使用 DI 工具:只使用一种,且仅用于组合根
  8. go build ./... 通过——组装错误会在编译时显现

评论

支持 Markdown