
本期我们分享人是组内小伙伴进峰,给大家分享下 Go 语言中的依赖注入。
一、前言
在现代软件开发中,线上项目往往依赖大量的资源,如数据库、消息队列、各种中间件等。随着项目业务的复杂化,单一项目内的业务模块也逐渐增多,如何高效、整洁地管理这些资源成为了一个重要的课题。
本文将从“术”的层面,深入探讨依赖注入(Dependency Injection, DI)的实现方式,并展示其在整洁架构(Clean Architecture)和领域驱动设计(DDD)等设计思想中的实际应用。
涉及内容:
- Google Wire:GitHub 13.5k Star 的 Go 语言依赖注入库 Wire
- Kratos:GitHub 23.9k Star 的 Go 微服务框架,默认使用 Wire 作为依赖注入工具 Kratos
- 其他运行时依赖注入框架与wire(编译时依赖注入框架)的对比
二、为什么使用依赖注入?
依赖注入(DI)是一种代码构造模式,它能够让你的代码更加易于维护和测试。
1. 案例分析:依赖注入的必要性
不使用依赖注入的风险:
- 全局变量的不安全性:全局变量容易被意外修改,存在覆写的风险。
- 资源管理混乱:资源散落在各处,可能导致重复创建,浪费内存,维护困难。
- 循环依赖风险:手动管理依赖关系时,容易引入循环依赖问题。
示例代码:
手动依赖注入:
package main
func NewMySQLClient(url string) *MySQLClient {
return &MySQLClient{url: url}
}
type MySQLClient struct {
url string
}
func (c *MySQLClient) Exec(query string, args ...interface{}) string {
return "data"
}
func NewApp(client *MySQLClient) *App {
return &App{client: client}
}
type App struct {
client *MySQLClient // App 持有唯一的 MySQLClient 实例
}
func (a *App) GetData(query string, args ...interface{}) string {
data := a.client.Exec(query, args...)
return data
}
func main() {
client := NewMySQLClient("mysql://blabla")
app := NewApp(client)
rest := app.GetData("select * from table where id = ?", "1")
println(rest)
}
不使用依赖注入:
package main
var (
mysqlUrl = "mysql://blabla"
db = NewMySQLClient(mysqlUrl) // 全局数据库实例
)
func NewMySQLClient(url string) *MySQLClient {
return &MySQLClient{url: url}
}
type MySQLClient struct {
url string
}
func (c *MySQLClient) Exec(query string, args ...interface{}) string {
return "data"
}
func NewApp() *App {
return &App{}
}
type App struct {}
func (a *App) GetData(query string, args ...interface{}) string {
data := db.Exec(query, args...)
return data
}
func main() {
app := NewApp()
rest := app.GetData("select * from table where id = ?", "1")
println(rest)
}
2. 总结:不使用依赖注入 vs 手动依赖注入
不使用依赖注入:
- 优点:代码简单,直接使用全局变量或单例模式,适合小型项目或快速原型开发。
- 缺点:
- 全局变量的不安全性:全局变量容易被意外修改,导致不可预见的错误。
- 资源管理混乱:资源散落在各处,可能导致重复创建,浪费内存,维护困难。
- 循环依赖风险:手动管理依赖关系时,容易引入循环依赖问题。
- 测试困难:由于依赖关系硬编码在代码中,单元测试时难以替换依赖项。
手动依赖注入:
- 优点:
- 解耦依赖:通过构造函数或方法参数显式传递依赖,降低了模块之间的耦合度。
- 提高可测试性:依赖项可以通过参数传递,便于在测试中使用模拟对象(Mock)。
- 资源管理清晰:依赖关系明确,资源创建和销毁更加可控。
- 缺点:
- 代码复杂度增加:随着依赖项增多,手动管理依赖关系会变得繁琐。
- 维护成本高:在大型项目中,手动管理依赖关系容易出错,且难以追踪依赖链。
依赖注入框架(如 Wire)的优势:
- 自动化依赖管理:通过代码生成工具自动管理依赖关系,减少手动编写依赖注入代码的工作量。
- 编译时检查:依赖注入问题在编译时即可发现,避免了运行时错误。
- 代码可读性:生成的代码符合常规编程习惯,易于理解和调试。
三、为什么使用Wire实现依赖注入?
Wire 是 Google 开源的依赖注入框架。官方定义是:“Wire 是一个代码生成工具,它使用依赖注入自动连接组件。” 简而言之,Wire 通过生成代码的方式,帮助开发者自动管理组件之间的依赖关系。
除了 Wire,Go 语言中还有其他依赖注入框架,如 Uber 的 dig 和 Facebook 的 inject。这些框架大多使用反射机制实现运行时依赖注入,而 Wire 则通过代码生成的方式实现编译时依赖注入。
Wire 的优势:
- 编译时依赖注入:Wire 在编译时生成依赖注入代码,避免了运行时反射带来的性能损失。
- 代码可读性:生成的代码符合常规编程习惯,易于理解和调试。
- 错误提前暴露:依赖注入问题在编译时即可发现,避免了运行时错误。
四、如何使用 Wire?
4.1 基本使用
安装 Wire:
go install github.com/google/wire/cmd/wire@latest
Provider 与 Injector:
Injector:Wire 生成的函数,用于按依赖顺序调用 Provider。
// wire.go
//go:build wireinject
// +build wireinject
package main
import "github.com/google/wire"
func wireApp(url string) *App {
wire.Build(NewMySQLClient, NewApp)
return nil
}
执行 wire 命令后,生成 wire_gen.go 文件:
// wire_gen.go
func wireApp(url string) *App {
mySQLClient := NewMySQLClient(url)
app := NewApp(mySQLClient)
return app
}
Provider:普通的 Go 函数,用于生成某个对象。Wire 通过 Provider 了解对象的依赖关系。
func NewMySQLClient(url string) *MySQLClient {
return &MySQLClient{url: url}
}
func NewApp(client *MySQLClient) *App {
return &App{client: client}
}
4.2 复杂项目的依赖注入设计
以 Kratos 的模板项目为例,展示如何在复杂项目中使用 Wire 进行依赖注入。
项目结构:
├── biz
├── conf
├── data
├── server
└── service

依赖关系:
- data:业务数据访问层,包含数据库、缓存等封装。
- biz:业务逻辑层,类似 DDD 的 domain 层。
- service:服务层,处理 DTO 到 DO 的转换。
- server:HTTP 和 gRPC 服务的创建与配置。

Provider 定义:
// biz 目录
var ProviderSet = wire.NewSet(NewGreeterUsecase)
type GreeterUsecase struct {
repo GreeterRepo
log *log.Helper
}
func NewGreeterUsecase(repo GreeterRepo, logger log.Logger) *GreeterUsecase {
return &GreeterUsecase{repo: repo, log: log.NewHelper(logger)}
}
Injector 定义:
// wire.go
func wireApp(*conf.Server, *conf.Data, log.Logger) (*kratos.App, func(), error) {
panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet))
}
生成代码:
运行wire命令生成依赖注入代码
wire
// wire_gen.go
func wireApp(confServer *conf.Server, confData *conf.Data, logger log.Logger) (*kratos.App, func(), error) {
dataData, cleanup, err := data.NewData(confData, logger)
if err != nil {
return nil, nil, err
}
greeterRepo := data.NewGreeterRepo(dataData, logger)
greeterUsecase := biz.NewGreeterUsecase(greeterRepo, logger)
greeterService := service.NewGreeterService(greeterUsecase)
grpcServer := server.NewGRPCServer(confServer, greeterService, logger)
httpServer := server.NewHTTPServer(confServer, greeterService, logger)
app := newApp(logger, grpcServer, httpServer)
return app, func() {
cleanup()
}, nil
}
4.3 Wire 的高级用法
Provider 集合:
var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)
携带错误返回值的 Provider:
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {
if bar.X == 0 {
return Baz{}, errors.New("cannot provide baz when bar is zero")
}
return Baz{X: bar.X}, nil
}
五、(dig、inject、springboot)运行时依赖注入框架 与 Wire 的依赖注入对比
相同点:
- 降低耦合度:两者都通过依赖注入实现代码的松耦合。
- 提高可测试性:依赖注入使得单元测试更加容易。
- 灵活性:在不修改组件代码的情况下替换依赖项。
不同点:
- 实现方式:
- 运行时依赖注入 依赖运行时反射机制。
- Wire 依赖编译时代码生成。
- 配置方式:
- 运行时依赖注入 使用配置文件和注解。
- Wire 使用 Go 文件和代码生成指令。
- 运行时开销:
- 运行时依赖注入依赖反射运行,存在运行时开销。
- Wire 在编译时生成代码,运行时无额外开销。
六、参考资料
通过本文,你应该对 Go 语言中的依赖注入有了更深入的理解,并能够使用 Wire 框架来管理项目中的依赖关系。无论是小型项目还是复杂的微服务架构,Wire 都能帮助你构建更加整洁、可维护的代码、同时显著减少程序员的心智和体力负担。
本文作者 进峰(海鸥技术部落),原载于团队 GitBook 技术 Wiki。
Go 语言中的依赖注入:由浅入深Wire 框架
本期我们分享人是组内小伙伴进峰,给大家分享下 Go 语言中的依赖注入。 一、前言 在现代软件开发中,线上项目往往依赖大量的资源,如数据库、消息队列、各种中间件等。随着项目业务的复杂化,单一项目内的业务模块也逐渐增多,如何高效、整洁地管理这些资源成为了一个重要的课题。 本文将从“术”的层面,深入探讨依赖注入(Dependency Injection, DI)的实现方式,并展示其在整洁架构(Clean