你的
main.go是不是也像一个“大泥球”,塞满了各种NewService?当依赖关系越来越复杂,你是否也曾被对象的创建顺序搞得头昏脑胀?本文将以开源项目easyms.golang为例,分享一次从尝试google/wire到回归“手动DI”的完整思考过程。我们将深入探讨依赖注入的核心思想,并最终沉淀出一套清晰、可维护、无“黑魔法”的Go依赖注入实践方案。
大家好,我是Louis。
今天我们继续优化 EasyMs 架构, 来聊一个能显著提升Go项目工程化水平的话题:依赖注入(Dependency Injection, DI)。很多初学者对依赖注入不够重视,其实他真不难,而且是你项目走向工程化的必经之路。本来一种方式就可以完成的,我今天分别用手动和自动两种方式来全面揭开它神秘的面纱。
很多从其他语言转过来的开发者,初次接触Go时都会有些困惑:“Go没有Spring、Autofac那样的DI框架,我该怎么管理我的对象依赖?”。 记得好8年前的一个SignalR的项目,因为开发人员使用Autofac不当,一上线服务器压力就扛不住了,也因此我每次项目开发中对依赖注入部分多一点关注。
上一篇文章的评论区有位朋友提出了一个”依赖DY“的案例问题,长期处理可能需要重新进行框架治理(重新分层),短期的处理就是清理依赖关系,将构建和接口应用分离,再加上Mock,感兴趣的朋友可以查看上一篇文章。
我们先来看看不使用依赖注入的代码,很多Go项目的main函数,就成了下面这样:
func main() {
// 一个典型的“意大利面条”式 main 函数
cfg := config.Load()
logger := logger.New(cfg.Log)
db, err := database.New(cfg.DB)
if err != nil { /* ... */ }
userRepo := repository.NewUserRepo(db)
orderRepo := repository.NewOrderRepo(db)
authSvc := service.NewAuthService(userRepo)
orderSvc := service.NewOrderService(orderRepo, logger)
authHandler := handler.NewAuthHandler(authSvc)
orderHandler := handler.NewOrderHandler(orderSvc)
engine := gin.New()
engine.POST("/login", authHandler.Login)
engine.POST("/orders", orderHandler.Create)
engine.Run()
}
这个main函数就像一个“大总管”,负责所有对象的创建和组装。项目初期还好,一旦服务变复杂,这里的代码就会迅速膨胀成一坨难以维护的“意大利面条”。
在easyms.golang项目中,我决定对这个问题发起挑战。我分别在两个核心服务中尝试了两种截然不同的DI方案,好让大家真正的明白如何做依赖注入:
order-svc(订单服务):采用手动编写DI容器的方案。user-svc(用户服务):采用**google/wire自动生成**的方案。
今天,我就来复盘一下这两次实战的得失。
方案一:手动DI容器(以order-svc为例)
这是最朴素、最直接的方案。既然main.go太乱,那我们就把初始化的逻辑抽离出来,放到一个专门的文件里。
我们创建了一个app.go文件,它就是我们手动的“依赖注入容器”。
1. 定义App结构体
首先,我们定义一个App结构体,它持有所有我们需要管理生命周期的核心组件。
// internal/services/order/cmd/ordersvc/app.go
type App struct {
engine *gin.Engine
ds *discovery.Discovery
relayService *service.RelayService
// ...
}
2. 编写InitializeApp函数
然后,我们编写一个InitializeApp函数,在这里集中处理所有的初始化逻辑。
func InitializeApp(serverName string, env string) (*App, func(), error) {
// --- 1. 配置加载 ---
appConfig, err := provideAppConfig(...)
if err != nil { return nil, nil, err }
// --- 2. 基础组件初始化 ---
dbase, err := provideDatabase(appConfig)
// ...
// --- 3. 服务层初始化 ---
orderService := service.NewOrderService(dbase)
// ...
// --- 4. 构建 App ---
app := &App{ ... }
// --- 5. 定义清理函数 ---
cleanup := func() {
// 按相反顺序关闭资源
relayService.Stop()
// ...
}
return app, cleanup, nil
}
3. 简洁的main.go
最终,order-svc的main.go变得非常清爽:
func main() {
// 调用我们手动编写的初始化函数
app, cleanup, err := InitializeApp("order-svc", "dev")
if err != nil {
panic(err)
}
defer cleanup()
app.engine.Run(...)
}
评价:这种方案清晰、可控、无黑魔法。所有的依赖关系和初始化顺序都一目了然,非常适合中小型项目。
方案二:google/wire自动注入(以user-svc为例)
手动写InitializeApp虽然清晰,但如果依赖关系非常复杂,写起来也很累。于是,我在user-svc中尝试了Google官方的wire工具。
wire是一个编译时的DI工具。你只需要告诉它“零件”有哪些,它就能自动帮你把“车”组装好。
1. 编写wire.go,定义“蓝图”
我们不再手写初始化逻辑,而是定义一个ProviderSet。
// internal/services/user/cmd/usersvc/wire.go
//go:build wireinject
// +build wireinject
package main
import "github.com/google/wire"
var providerSet = wire.NewSet(
// 告诉 wire 如何创建各个组件
config.InitAppConfigStore,
provideDiscovery,
provideAppConfig,
provideDatabase,
service.NewUserService,
handles.NewUserHandler,
NewApp,
)
// InitializeApp 是 wire 的入口点
func InitializeApp(inputs ConfigInputs) (*App, func(), error) {
// wire 会自动填充这个函数体
wire.Build(providerSet)
return nil, nil, nil
}
2. 生成代码
运行wire命令后,它自动生成了一个wire_gen.go文件。打开一看,里面的代码逻辑和我们在order-svc中手写的一模一样!wire帮我们完成了那些繁琐的组装工作。
3. 同样的main.go
user-svc的main.go代码与order-svc几乎完全一致,也是直接调用InitializeApp。
评价:wire方案自动化程度高、编译时检查依赖。虽然初次配置(如处理vendor、构建标签)有些门槛,但一旦跑通,对于大型项目来说,它能极大地减少重复劳动。
总结:我的Go DI实践建议
经过这次对比实战,我沉淀出了一套在Go项目中进行依赖注入的实践建议:
-
拥抱接口,面向抽象编程:这是DI的基础。无论用不用工具,你的业务逻辑都应该依赖于接口(如
db.Database),而不是具体实现。 -
坚持使用构造函数:为你的每一个结构体提供一个
New...开头的构造函数,明确声明它的依赖。这是让依赖关系变得清晰的关键。 -
根据项目规模选择方案:
- 中小型项目:推荐使用手动DI容器(如
order-svc)。它足够简单,没有学习成本,且完全可控。 - 大型/复杂项目:推荐使用**
google/wire**(如user-svc)。当依赖关系网变得庞大时,wire的静态分析和自动生成能力将成为你的得力助手。
- 中小型项目:推荐使用手动DI容器(如
无论选择哪种方式,核心目标都是一样的:让main.go回归纯粹,让依赖关系清晰可见。
如果你想深入了解这两种方案的完整代码实现,欢迎访问项目的源码地址,给我一个 Star ⭐!
- GitHub: github.com/louis-xie-p…
- Gitee: gitee.com/louis_xie/e…
读者互动:
如果觉得这篇文章对你有用,欢迎点赞、推荐支持。
在你的Go项目中,你是如何管理依赖注入的?你更倾向于手动控制还是自动生成?欢迎在评论区分享你的看法!👇