首页 > 其他分享 >Wire:Go最优雅的依赖注入工具

Wire:Go最优雅的依赖注入工具

时间:2023-03-03 18:11:29浏览次数:40  
标签:初始化 Wire nil 优雅 组件 wire go Go

阅读用时:4分钟

导语

“成熟的工具,要学会自己写代码”。本文介绍了 Go 依赖注入工具 [[Wire]] 及其使用方法,以及在实践中积累的各种运用技巧。当代码达到一定规模后,[[Wire]] 在组件解耦、开发效率、可维护性上都能发挥很大的作用,尤其在大仓场景。

依赖注入

当项目变得越来越大,代码中的组件也越来越多:各种数据库、中间件的客户端连接,分层设计中的各种库表 repositories 实例、services 实例……

这时为了代码的可维护性,应该避免组件之间的耦合。具体的做法可以遵守一个重要的设计准则:所有依赖应该在组件初始化时传递给它,这就是依赖注入(Dependency injection)。

Dependency injection is a standard technique for producing flexible and loosely coupled code, by explicitly providing components with all of the dependencies they need to work.

– Go 官方博客

下面是个简单的例子,所有组件 MessageGreeterEvent 自身的依赖都在初始化的时候获得。

1
2
3
4
5
6
7
func main() {  
    message := NewMessage()  
    greeter := NewGreeter(message)  
    event := NewEvent(greeter)  

    event.Start()  
}

Wire 介绍

当项目中实例依赖(组件)的数量越来越多,如果还是人工手动编写初始化代码和维护组件之间依赖关系的话,会是一件非常繁琐的事情,而且在大仓中尤其明显。因此,社区里已经有了不少的依赖注入框架。

除了来自 Google 的 Wire 以外,还有 Dig(Uber) 、Inject(Facebook)。其中 Dig 和 Inject 都是基于 Golang 的 Reflection 来实现的。这不仅对性能产生影响,而且依赖注入的机制对使用者不透明,非常的“黑盒”。

Clear is better than clever ,Reflection is never clear.

— Rob Pike

相比之下,Wire 完全基于代码生成。在开发阶段,wire 会自动生成组件的初始化代码,生成代码人类可读,可以提交仓库,也可以正常编译。因此 Wire 的依赖注入非常透明,也不会带来运行阶段的任何性能损耗。

上手介绍

这里快速介绍一下 Wire 的使用方法

第一步:下载安装 Wire

下载安装 wire 命令行工具

1
go install github.com/google/wire/cmd/wire@latest

第二步:创建 wire.go 文件

在生成代码之前,我们先声明各个组件的依赖关系和初始化顺序。在应用入口创建一个 wire.go 文件。

cmd/web/wire.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// +build wireinject

package main

import "..."  // 简化示例

var ProviderSet = wire.NewSet(
	configs.Get,
	databases.New,
	repositories.NewUser,
	services.NewUser,
	NewApp,
)

func CreateApp() (*App, error) {
	wire.Build(ProviderSet)
	return nil, nil
}

这个文件不会参与编译,只是为了告诉 Wire 各个组件的依赖关系,以及期望的生成结果。在这个文件:我们期望 Wire 生成一个返回 App 实例或 error 的 CreateApp 函数,App 实例初始化所需要的全部依赖都由 ProviderSet 这个组件列表提供,而 ProviderSet 声明了所有可能需要的组件的获取/初始化方法,也暗示组件之间的依赖顺序。

组件的获取/初始化方法,在 Wire 中叫做“组件的 provider”

还有几点需要注意:

  • 文件开头必须带上 // +build wireinject 和随后的空行,否则会影响编译
  • 在这个文件中,编辑器和 IDE 可能无法提供代码提示,但没关系,稍后会介绍如何解决这个问题
  • 其中 CreateApp 的返回(两个 nil)没有任何意义,只是为了兼容 Go 语法。

第三步:生成初始化代码

命令行执行 wire ./...,然后就能得到下面这个自动生成的代码文件。

cmd/web/wire_gen.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

import "..."  // 简化示例

func CreateApp() (*App, error) {
	conf, err := configs.Get()
	if err != nil {
		return nil, err
	}
	db, err := databases.New(conf)
	if err != nil {
		return nil, err
	}
	userRepo, err := repositories.NewUser(db)
	if err != nil {
		return nil, err
	}
	userSvc, err := services.NewUser(userRepo)
	if err != nil {
		return nil, err
	}
	app, err := NewApp(userSvc)
	if err != nil {
		return nil, err
	}
	return app, nil
}

第四步:使用初始化代码

Wire 已经帮我们生成了真正的 CreateApp 初始化方法,现在可以直接使用它。

cmd/web/main.go

1
2
3
4
5
// main.go
func main() {
	app := CreateApp()
	app.Run()
}

使用技巧

组件按需加载

Wire 有个优雅的特点,不管在 wire.Build 中传入了多少个组件的 provider,Wire 始终只会按照实际需要来初始化组件,所有不需要的组件都不会生成相应的初始化代码。

因此,我们在使用时可以尽可能地提供更多的 provider,把挑选组件的工作交给 Wire。这样我们在开发时不管引用新组件、还是弃用老组件,都不需要修改初始化步骤的代码 wire.go。

比如,可以把 services 层中所有的实例构造器都提供出去。

pkg/services/wire.go

1
2
3
4
package services

// 提供了所有 service 的实例构造器
var ProviderSet = wire.NewSet(NewUserService, NewFeedService, NewSearchService, NewBannerService)

在初始化中,尽可能地引用所有可能需要的组件 provider。

cmd/web/wire.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var ProviderSet = wire.NewSet(
	configs.ProviderSet,
	databases.ProviderSet,
	repositories.ProviderSet,
	services.ProviderSet,  // 引用了所有 service 的实例构造器
	NewApp,
)

func CreateApp() (*App, error) {
	wire.Build(ProviderSet)  // wire 会按照实际需要,选择性地进行初始化
	return nil, nil
}

在后续开发中,如果需要引用新组件,只需要加到参数里即可。Wire 会任劳任怨地按照实际需要,生成需要的组件的初始化代码。

1
2
func NewApp(user *UserService, banner *BannerService) {
}

即使 Wire 找不到组件的 provider,也会提前在编译阶段报错,不会在线上运行阶段出现问题。

wire: cmd/api/wire.go:23:1: inject CreateApp: no provider found for *io.WriteCloser

编辑器与 IDE 的辅助配置

因为 wire.go 文件中加了这行注释,Go 在编译时会跳过这个文件,但也因此会影响编辑器和 IDE 的代码提示。当你在编辑 wire.go 文件时,常见的编辑器和 IDE 都无法正常地提供代码补全和错误提示功能。

1
// +build wireinject

但这个问题很容易解决。找到 IDE/编辑器的 Go 环境配置,在 Go Build Flags 中添加这个参数 -tags=wireinject 就可以了。

这个配置可以让编辑器和 IDE 正常地为 wire.go 文件提供代码补全和错误提示功能,开发体验提高不只一个数量级~

多个同类型组件的冲突问题

这个问题比较少见,但项目大了总是容易遇到。

Wire 通过 provider 的参数与返回类型,来判断组件的依赖关系。有时候,依赖网络中可能出现同类型的不同组件,这时 Wire 无法正确判断依赖关系,会直接报错。

provider has multiple parameters of type ...

比如下面这个 provider,依赖的 MySQL 和 PostgreSQL 客户端实例的类型是完全相同的(都是 *gorm.DB),这时 Wire 无法根据类型正确地判断依赖关系,生成代码时会直接报错。

1
2
3
// 这个 service 同时使用了 mysql 和 pg 中的数据,但是两个组件的类型是相同的
func NewService(mysql *gorm.DB, pg *gorm.DB) *Service {
}

解决的方法也比较简单,只需要做一层类型的包装。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type Mysql gorm.DB
type Pg    gorm.DB

// 在参数中用类型别名进行区分
func ProviderSerivce(mysql *Mysql, pg *Pg) *Service {
	// 函数内再转回原来的类型
	r1 := (*gorm.DB)(mysql)
	r2 := (*gorm.DB)(pg)
	return NewService(r1, r2)
}

然后用 ProviderSerivce 代替 NewService 即可。

1
2
3
4
5
wire.Build(
	ProviderMysql,   // func() *Mysql
	ProviderPg,      // func() *Pg
	ProviderSerivce, // func(mysql *Mysql, pg *Pg) *Service
)

自动生成构造函数

当项目中充当抽象类的结构体越来越多,手动编写和维护结构体的构造函数,也是一件非常繁琐的事情。如果结构体中新增了一个指针类型的成员、却忘记更新构造函数,甚至还会引起线上 panic。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type Service struct {
	repo   *Repository
	logger *zap.Logger  // 添加这个成员后,忘记更新构造函数了
}

func NewService(repo *Repository) *Service {
	// 缺失 logger,可能在线上出现空指针错误
	return &Service {
		repo:   repo,
	}
}

像这种繁琐、重复、容易出错的工作,就应该交给自动工具来完成。这里我毛遂自荐一个自动工具 newc(意为 “New Construtor”),它可以自动生成与更新结构体的构造函数代码。

使用方法非常简单,只需要给结构体添加这行注释。

//go:generate go run github.com/Bin-Huang/[email protected]

比如这样:

1
2
3
4
5
6
7
// My User Service
//go:generate go run github.com/Bin-Huang/[email protected]
type UserService struct {
	baseService
	userRepository *repositories.UserRepository
	proRepository  *repositories.ProRepository
}

然后命令行执行 go generate ./... 即可获得构造函数代码:

constructor_gen.go

1
2
3
4
5
6
7
8
// NewUserService Create a new UserService
func NewUserService(baseService baseService, userRepository *repositories.UserRepository, proRepository *repositories.ProRepository) *UserService {
	return &UserService{
		baseService:    baseService,
		userRepository: userRepository,
		proRepository:  proRepository,
	}
}

这个工具和 Wire 搭配使用,开发体验非常好。要使用新组件时,直接在结构体中添加成员就好了,不需要手动更新构造函数,也不需要考虑初始化的问题,所有重复的工作都交给自动工具(Wire 和 Newc)来完成。线下推荐过的同学,用过都说好。

当然这个工具也一定有考虑不周的情况,很期待大家的反馈和建议。

Don’t repeat yourself “DRY”

总结

Wire 可以完美地解决依赖注入的问题,但它不是一个框架,它没有”魔法“,也不是黑盒。它只是一个命令行工具,它根据实际需要,自动生成了各个组件的初始化代码。然后问题就解决了,没有额外的复杂性,没有运行的性能损耗。

Wire 和 [[Golang]] 的气质如出一辙,简单、直接、实用主义,不愧是 Go 最优雅的依赖注入工具!

Keep it simple stupid “K.I.S.S”

标签:初始化,Wire,nil,优雅,组件,wire,go,Go
From: https://www.cnblogs.com/gongxianjin/p/17176604.html

相关文章

  • 敏捷工具leangoo领歌时间线视图上线啦
    https://www.leangoo.com/17735.htmlLeangoo企业版新增「时间线视图」,通过「时间线视图」你可以在项目管理中非常直观的了解每个人的工作分配及各个任务的排期,方便及时......
  • [Go语言tips04]二维数组与二维切片
    0.引言既然在Go语言中数组和切片同时存在并且是两个不同的类型,那当他们是二维时又会产生什么样的问题?因为数组和切片同时存在,在Go语言中二维的使用就会显得和别的语言很......
  • Go组件库总结之协程睡眠唤醒
    本篇文章我们用Go封装一个利用gopark和goready实现协程睡眠唤醒的库。文章参考自:https://github.com/brewlin/net-protocol1.gopark和goready的声明//go:linknamegopark......
  • 如何让错误处理更加优雅
    1.go采用c的err方法,但是容易产生大量的外部判断。packagekillerimport"fmt"typeBookstruct{NamestringPriceintStoreintMember......
  • protobuf golang&&python序列化反序列化测试
    1.概要最近考虑采用protobuf来实现kafka消息传递,所以先测试一下golang和python之前序列化互通问题。由于go和python对于二进制的表示在ide层面是无法统一的,直接把python......
  • Django+vue 上传execl文件并解析
    Django+vue上传execl文件并解析VUE<template><el-buttontype="primary"class="but_list_but1"><inputtype="file"name="avatar"id="avatar"style="display......
  • Study for Go! Chapter one - Type
    StudyforGo!-Type1.Variable关键字为"var"自动初始化为二进制零值编译器推测数据类型变量类型放在变量名之后可以一次性定义多个变量且可以是不同......
  • python+playwright 学习-21.文件上传-优雅处理
    前言如果你之前用过selenium,肯定遇到过文件上传头疼的事,有些控件是input输入框,可以直接传本地文件地址,然而有些需要弹出本地文件选择器的时候就不好处理了。playwright......
  • Gossip
    共识性算法GossipGossip也叫EpidemicProtocol(流行病协议),这个协议基于最终一致性以及去中心化设计思想。主要用于分布式节点之间进行信息交换和数据同步,这种场景的一......
  • python+playwright 学习-19.监听dialog事件-优雅处理对话框
    前言网页上的alert弹出框你不知道什么时候弹出来,selenium处理alert弹出框的方式是先判断有没alert再处理,并且只能处理这一次。playwright框架可以监听dialog事件,不管......