实战 dig:Go 编译时依赖注入的完整教程与迁移指南

引子:当依赖注入遇见编译时代码生成

在 Go 项目中管理依赖,我们常常陷入两难:要么使用 wire 忍受繁琐的集合与占位符,要么使用 fx 承担运行时反射的开销和 panic 风险。而 diggithub.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

这是一个典型的微服务模块划分:userrole 各自独立,并拥有自己的 repository 子模块。app 层负责组装所有模块,commoninternal/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)
	}
}

关键点cfglog 作为 InitApp 的参数,会自动成为容器中的供应值 ,无需显式调用 dig.Supply。任何需要 *common.Config*logger.LoggerProvideInvoke 都能直接获取它们。


4. 应用组装层:app/app.go

app.godig.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 可以注入任意值(不仅仅是常量),这里传入了 100Config("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 将选项组合成模块(无名称)

重要约束

  1. 闭包捕获限制Provide/Invoke 中的闭包不能捕获外层函数的局部变量,只能使用包级符号或字面量。这是因为生成器会将闭包提升为包级函数。
  2. 泛型显式实例化 :所有泛型函数/类型在调用时必须指明类型参数,如 NewStore[int]
  3. 外部参数自动供应InitApp 的参数会自动成为容器中的供应值,无需 dig.Supply
  4. 类型冲突 :若需要多个相同底层类型的值(如多个 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 的步骤

  1. wire.NewSet 替换为 dig.Module(可选,可直接平铺)。
  2. wire.Value(v) 替换为 dig.Supply(v),现在 v 可以是任意表达式。
  3. 删除 wire.Bind,改为让构造函数直接返回接口类型。
  4. 删除 return nil,让 dig.Build 直接返回 func(ctx) error
  5. 将启动逻辑(如 app.Run())放入 dig.Invoke
  6. 构建标签改为 //go:build digen,并添加 //go:generate digen ./...
  7. 运行 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 的步骤

  1. fx.New 替换为 dig.Build,它返回 func(ctx) error
  2. app.Run() 改为调用返回的函数,并传入 context
  3. 移除 fx.Module 中的名称字符串(dig 模块无名称)。
  4. 如果使用了 OnStart/OnStop 钩子,可在 dig.Invoke 中手动处理,或保持简洁。
  5. 添加 //go:build digen//go:generate digen ./...
  6. 运行 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。

相关推荐
newerp17 小时前
Go net/http 标准库基础
后端·程序员·go
26519405112648017 小时前
01-整体架构与高可用
go
程序员爱钓鱼19 小时前
Go if 判断详解
前端·后端·go
名字还没想好☜1 天前
Go 的 unsafe.Pointer 实战:零拷贝 []byte↔string 转换与三条铁律
开发语言·后端·golang·go·unsafe
阿里云云原生1 天前
阿里云联合 Datadog,补齐 Go 可观测性最后短板
云原生·go
可观测性用观测云1 天前
零码改造!Go 语言应用上报观测云完整最佳实践
go
斐波那契数列2 天前
参考react 实现一个golang gui
go
程序员爱钓鱼2 天前
Go 运算符详解
后端·面试·go
angryshan2 天前
goLang配置断点流程
go·bug
名字还没想好☜3 天前
Go 的 sync.Cond 实战:用条件变量做等待/通知,比忙轮询省 CPU
开发语言·数据库·后端·golang·go