拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Go依赖注入:Wire代码生成实战

Go依赖注入:Wire代码生成实战摘要: 本篇讲解Go语言Wire依赖注入库实战涵盖provider和injector概念、接口绑定实现依赖抽象、参数对象组织多个依赖、Cleanup函数释放资源分享循环依赖导致wire生成代码报错的踩坑经历对比Wire代码生成与fx运行时依赖注入两种方案。开篇故事我们有个订单服务依赖7个组件数据库连接、Redis、日志、配置、消息队列、限流器、追踪。启动函数里手动new每个组件再传给订单服务代码写了50行顺序还不能错依赖谁必须先初始化谁。每次加一个新组件启动函数要改初始化顺序要调很容易写错。有一次同事把Redis初始化放在了订单服务后面订单服务拿到的是nil指针运行时panic。排查了15分钟才发现初始化顺序错了。后来我用Wire重写了依赖管理。每个组件写一个provider函数Wire自动分析依赖关系生成初始化代码。加新组件只要写provider不用管初始化顺序Wire帮你算好。这篇把Wire的用法和坑讲清楚。一、Provider和Injector概念Wire的核心概念两个。provider是构造函数告诉Wire怎么造一个对象。injector是入口函数声明最终要什么对象Wire根据provider依赖关系生成代码。先定义几个业务对象。packagemain// 各个组件的定义// Config 配置对象typeConfigstruct{DBHoststringDBPortintCacheAddrstring}// DB 数据库连接(简化版)typeDBstruct{hoststringportint}// Query 模拟查询方法func(d*DB)Query(sqlstring)string{returnresult from db}// Cache 缓存客户端typeCachestruct{addrstring}// Get 模拟缓存读取func(c*Cache)Get(keystring)(string,bool){return,false}// Logger 日志接口typeLoggerinterface{Info(msgstring)}// ConsoleLogger 控制台日志实现typeConsoleLoggerstruct{}func(l*ConsoleLogger)Info(msgstring){// 实际项目写到stdout或文件}// OrderService 订单服务依赖DB和CachetypeOrderServicestruct{db*DB cache*Cache log Logger}// CreateOrder 创建订单业务方法func(s*OrderService)CreateOrder()string{// 先查缓存ifv,ok:s.cache.Get(order);ok{returnv}// 缓存没有查数据库returns.db.Query(select * from orders)}定义provider函数和injector。packagemain// 这段代码放在wire.go文件里// 加build约束只在wire工具生成代码时编译//go:build wireinjectimportgithub.com/google/wire// Provider函数告诉Wire怎么造对象 // NewConfig 配置providerfuncNewConfig()*Config{returnConfig{DBHost:127.0.0.1,DBPort:3306,CacheAddr:127.0.0.1:6379,}}// NewDB 数据库provider依赖ConfigfuncNewDB(cfg*Config)*DB{returnDB{host:cfg.DBHost,port:cfg.DBPort,}}// NewCache 缓存provider依赖ConfigfuncNewCache(cfg*Config)*Cache{returnCache{addr:cfg.CacheAddr}}// NewLogger 日志providerfuncNewLogger()Logger{returnConsoleLogger{}}// NewOrderService 订单服务provider// 依赖DB、Cache、Logger三个对象funcNewOrderService(db*DB,cache*Cache,log Logger)*OrderService{returnOrderService{db:db,cache:cache,log:log,}}// ProviderSet 把一组provider打包 // CommonSet 通用组件集合// 包含Config和Logger多个服务共享varCommonSetwire.NewSet(NewConfig,NewLogger)// Injector函数声明最终要什么 // InitializeOrderService 是injector入口// wire.Build告诉Wire用哪些provider构造目标// 返回值类型决定Wire生成的代码结构funcInitializeOrderService()*OrderService{wire.Build(CommonSet,// Config和LoggerNewDB,// 数据库NewCache,// 缓存NewOrderService,// 订单服务)returnnil// 这行不会被编译wire会替换}写完后执行wire命令Wire分析依赖关系生成wire_gen.go文件。生成的代码大致长这样。// 以下是wire生成的代码不要手动修改funcInitializeOrderService()*OrderService{cfg:NewConfig()db:NewDB(cfg)cache:NewCache(cfg)logger:NewLogger()orderService:NewOrderService(db,cache,logger)returnorderService}Wire自动算出了初始化顺序Config最先DB和Cache依赖Config所以靠后OrderService最后。加新组件只要加provider函数不用管顺序。二、接口绑定与参数对象实际项目里依赖往往是接口不是具体类型。比如Logger是个接口可能有多个实现控制台版和文件版。Wire用wire.Bind绑定接口和实现。packagemain//go:build wireinjectimportgithub.com/google/wire// FileLogger 文件日志实现typeFileLoggerstruct{pathstring}func(l*FileLogger)Info(msgstring){// 实际写文件}// NewFileLogger 文件日志provider// 注意返回值是具体类型*FileLoggerfuncNewFileLogger()*FileLogger{returnFileLogger{path:/var/log/app.log}}// AppService 应用服务依赖Logger接口typeAppServicestruct{log Logger}// NewAppService 应用服务provider// 参数是Logger接口不是具体类型funcNewAppService(log Logger)*AppService{returnAppService{log:log}}// ProviderSet 绑定接口和实现varAppSetwire.NewSet(NewFileLogger,// wire.Bind告诉Wire用*FileLogger实现Logger接口// 第一个参数是接口指针第二个是实现指针wire.Bind(new(Logger),new(*FileLogger)),NewAppService,)// 参数对象: 当一个provider依赖太多参数时// 用结构体组织避免参数列表过长// OrderServiceParams 订单服务参数对象// 把多个依赖打包成一个结构体typeOrderServiceParamsstruct{DB*DB Cache*Cache Log Logger}// NewOrderServiceWithParams 用参数对象构造服务// Wire自动识别参数对象的字段逐个填充funcNewOrderServiceWithParams(p OrderServiceParams)*OrderService{returnOrderService{db:p.DB,cache:p.Cache,log:p.Log,}}// InitializeApp injector入口funcInitializeApp()*AppService{wire.Build(CommonSet,// ConfigAppSet,// Logger和AppService)returnnil}wire.Bind的第一个参数是接口的指针类型new(Logger)第二个是实现的指针类型new(*FileLogger)。Wire生成代码时遇到需要Logger的地方自动用NewFileLogger的返回值。参数对象解决了provider参数过多的问题。原来NewOrderService(db, cache, log)三个参数扩展到7个参数时函数签名很难看。用参数对象所有依赖放结构体里Wire按字段类型自动填充。三、Cleanup函数释放资源有些provider创建的资源需要释放比如数据库连接、文件句柄。Wire支持返回cleanup函数。packagemainimport(contextgithub.com/google/wire)// Conn 模拟需要释放的连接typeConnstruct{addrstring}func(c*Conn)Close()error{// 释放连接returnnil}// NewConn 连接provider// 返回值第二个是cleanup函数// Wire生成代码时会在defer里调用cleanupfuncNewConn(addrstring)(*Conn,func(),error){c:Conn{addr:addr}// cleanup函数关闭连接cleanup:func(){c.Close()}returnc,cleanup,nil}// Repository 数据访问层typeRepositorystruct{conn*Conn}// NewRepository repository provider// 依赖ConnConn有cleanupfuncNewRepository(conn*Conn)*Repository{returnRepository{conn:conn}}// App 应用依赖RepositorytypeAppstruct{repo*Repository}funcNewApp(repo*Repository)*App{returnApp{repo:repo}}// AppSet 包含带cleanup的providervarConnSetwire.NewSet(NewConn,NewRepository,NewApp,)// InitializeAppWithCleanup injector// 返回值多了cleanup和errorfuncInitializeAppWithCleanup(ctx context.Context)(*App,func(),error){wire.Build(ConnSet)returnnil,nil,nil}// wire生成的代码大致是这样// 注意cleanup层层传递funcgeneratedInitApp(ctx context.Context)(*App,func(),error){conn,cleanup,err:NewConn(127.0.0.1:3306)iferr!nil{returnnil,nil,err}// Repository和App没有cleanuprepo:NewRepository(conn)app:NewApp(repo)// 返回最外层对象和cleanupreturnapp,func(){cleanup()},nil}cleanup函数从底层provider往上传递。Conn有cleanupRepository和App没有。Wire生成代码时最终返回的cleanup函数里调用Conn的cleanup。调用方拿到App和cleanup用完App后调cleanup释放Conn。四、踩坑经验:循环依赖报错这个坑很经典。我们有个用户服务和订单服务用户服务要查订单(展示用户订单列表)订单服务要查用户(显示下单人姓名)。两个互相依赖Wire生成代码时报错cycle detected。packagemain// 错误写法: 两个服务互相依赖// UserService 用户服务typeUserServicestruct{orderSvc*OrderService// 依赖订单服务}// OrderService 订单服务typeOrderServicestruct{userSvc*UserService// 依赖用户服务循环了}// NewUserService 依赖OrderServicefuncNewUserService(orderSvc*OrderService)*UserService{returnUserService{orderSvc:orderSvc}}// NewOrderService 依赖UserServicefuncNewOrderService(userSvc*UserService)*OrderService{returnOrderService{userSvc:userSvc}}// wire.Build这两个provider会报错: cycle detected循环依赖的本质是两个对象在构造期互相需要没法确定谁先创建。解决方案是引入第三个对象把共享的数据抽出来。packagemain// 正确写法: 抽出共享依赖// UserRepo 用户数据访问typeUserRepostruct{}func(r*UserRepo)FindUser(idint)string{return张三}// OrderRepo 订单数据访问typeOrderRepostruct{}func(r*OrderRepo)FindOrders(uidint)[]string{return[]string{订单1,订单2}}// UserService2 用户服务只依赖UserRepotypeUserService2struct{userRepo*UserRepo}funcNewUserService2(repo*UserRepo)*UserService2{returnUserService2{userRepo:repo}}// OrderService2 订单服务只依赖OrderRepotypeOrderService2struct{orderRepo*OrderRepo}funcNewOrderService2(repo*OrderRepo)*OrderService2{returnOrderService2{orderRepo:repo}}// UserOrderFacade 外观模式组合两个服务// 需要同时用到用户和订单数据时用这个typeUserOrderFacadestruct{userSvc*UserService2 orderSvc*OrderService2}// NewUserOrderFacade facade依赖两个服务但不构成循环funcNewUserOrderFacade(u*UserService2,o*OrderService2)*UserOrderFacade{returnUserOrderFacade{userSvc:u,orderSvc:o}}// GetUserOrders 组合查询用户订单func(f*UserOrderFacade)GetUserOrders(uidint)[]string{// 调两个服务的数据拼装结果user:f.userSvc.userRepo.FindUser(uid)orders:f.orderSvc.orderRepo.FindOrders(uid)result:[]string{user的订单:}resultappend(result,orders...)returnresult}// 两个Repo没有互相依赖打破循环funcNewUserRepo()*UserRepo{returnUserRepo{}}funcNewOrderRepo()*OrderRepo{returnOrderRepo{}}funcmain(){// 手动构造顺序清晰无循环userRepo:NewUserRepo()orderRepo:NewOrderRepo()// 两个服务各依赖自己的RepouserSvc:NewUserService2(userRepo)orderSvc:NewOrderService2(orderRepo)// facade组合两个服务facade:NewUserOrderFacade(userSvc,orderSvc)// 组合查询用户订单results:facade.GetUserOrders(1001)for_,r:rangeresults{// 输出: 张三的订单: 订单1 订单2println(r)}}// Wire的injector写法(放在wire.go文件加build约束)// var AppSet2 wire.NewSet(// NewUserRepo, NewOrderRepo,// NewUserService2, NewOrderService2,// NewUserOrderFacade,// )// func InitializeFacade() *UserOrderFacade {// wire.Build(AppSet2)// return nil// }解决循环依赖的思路是把互相依赖改成共享依赖。UserService和OrderService都依赖底层的UserRepo和OrderRepo不再互相依赖。需要组合数据时用一个Facade对象调两个服务。架构上叫依赖倒置把高层依赖抽到底层。五、对比分析DI方案实现方式编译期检查性能学习成本Wire代码生成支持零开销中fx运行时反射不支持有反射开销高手动构造手写代码支持零开销低dig(fx底层)运行时容器不支持有开销高Wire代码生成编译期能检查依赖是否完整缺依赖编译报错运行时零开销。fx运行时反射灵活但性能有损耗依赖错误运行时才暴露。手动构造适合简单项目组件少于5个直接写。dig是fx的底层容器一般不直接用。总结与预告Wire用provider声明构造函数injector声明目标自动生成初始化代码。接口用wire.Bind绑定实现参数对象解决参数过多问题cleanup函数处理资源释放。循环依赖要靠依赖倒置打破抽共享底层数据。Wire适合编译期确定依赖的项目动态加载场景考虑fx。下一篇讲Go反射看看reflect包怎么操作类型和值。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门