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

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

在 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 将选项组合成模块(无名称)

重要约束:

  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。

相关推荐
猪猪拆迁队16 小时前
跨电脑复制共享-局域网直连踩坑
前端·后端·go
tachibana216 小时前
Go 语言的 Map 是否是并发安全的?
开发语言·数据结构·后端·golang·go·语言
tachibana217 小时前
Go 语言 Map 的扩容过程是怎样的?
开发语言·后端·golang·go·语言
共饮一杯无18 小时前
LiveKit本地录制落盘路径排查与修复实践
java·go·livekit
探索云原生20 小时前
KServe 工作流程:从 InferenceService 到 vLLM 服务
docker·ai·云原生·kubernetes·go
人人都是软件工程师21 小时前
13-错误处理:把失败变成可读、可判断的结果
go
林壮Allen2 天前
用 Go 从零实现多 Agent 协作框架:两个让我踩坑的设计问题
go·ai编程
代码方舟2 天前
Go数据工程:利用天远柠檬查出险优化二手车批发拍卖车源出险核查合规体验
人工智能·go
不是万能的豆学长2 天前
排查Go服务40天后的内存泄漏问题
开发语言·后端·golang·go
用户125758524362 天前
Golang后台管理框架推荐:XYGo Admin与devinggo怎么选?
后端·go·ai编程