实战 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。

相关推荐
Sinclair16 小时前
认识安企CMS-系统概述
开源·go
cocoCola9166721 小时前
Golang中的接口
go
赫媒派1 天前
Gin 12年零破坏API,架构哲学如何练成?
后端·go·gin
唐青枫1 天前
别再只会 if err != nil:Go error 从错误链到工程实战详解
go
小满zs2 天前
Go语言第二章(小无相功)
后端·go
妙码生花2 天前
从 PHP 到 AI + Golang,程序员自救转型手记(十九):点选验证码代码逐行目检
前端·后端·go
老鹰8622 天前
Google Wire 被官方抛弃,Uber Fx 启动就 panic,Go DI 还有救吗?
go
golang学习记2 天前
Go面试官:说说struct{}为什么占用0字节
go
喵个咪3 天前
Go Wind UBA 拆解系列 - 架构总览:三服务、数据流与契约优先
大数据·后端·go