写点什么

万字长文:在 Go 中如何优雅的使用 wire 依赖注入工具提高开发效率?下篇

作者:江湖十年
  • 2024-06-14
    浙江
  • 本文字数:10613 字

    阅读完需:约 35 分钟

万字长文:在 Go 中如何优雅的使用 wire 依赖注入工具提高开发效率?下篇

《万字长文:在 Go 中如何优雅的使用 wire 依赖注入工具提高开发效率?上篇》,我讲解了 Go 语言中依赖注入工具 wire 的基本使用及高级用法。本篇就来介绍下 wire 的生产实践。

Wire 生产实践

这里以一个 user 服务作为示例,演示下一个生产项目中是如何使用 wire 依赖注入工具的。


user 项目目录结构如下:


$ tree useruser├── assets│   ├── curl.sh│   └── schema.sql├── cmd│   └── main.go├── go.mod├── go.sum├── internal│   ├── biz│   │   └── user.go│   ├── config│   │   └── config.go│   ├── controller│   │   └── user.go│   ├── model│   │   └── user.go│   ├── router.go│   ├── store│   │   └── user.go│   ├── user.go│   ├── wire.go│   └── wire_gen.go└── pkg    ├── api    │   └── user.go    └── db        └── db.go
12 directories, 16 files
复制代码


NOTE: user 项目源码在此,你可以点击查看,建议下载下来执行启动下程序,加深理解。


这是一个典型的 Web 应用,用来对用户进行 CRUD。不过为了保持代码简洁清晰,方便理解,user 项目仅实现了创建用户的功能。


我先简单介绍下各个目录的功能。


assets 努目录用于存放项目资源。schema.sql 中是建表语句,curl.sh 保存了一个 curl 请求命令,用于测试创建用户功能。


cmd 中当然是程序入口文件。


internal 下保存了项目业务逻辑。


pkg 目录存放可导出的公共库。api 用于存放请求对象;db 用于构造数据库对象。


项目设计了 4 层架构,controller 即对应 MVC 经典模式中的 Controller,biz 是业务层,store 层用于跟数据库交互,还有一个 model 层定义模型,用于映射数据库表。


router.go 用于注册路由。


user.go 用于定义创建和启动 user 服务的应用对象。


wire.gowire_gen.go 两个文件就无需我过多讲解了。


NOTE: 本项目目录结构遵循最佳实践,可以参考我的另一篇文章《如何设计一个优秀的 Go Web 项目目录结构》


简单介绍完了目录结构,再来梳理下我们所设计的 4 层架构依赖关系:首先 controller 层依赖 biz 层,然后 biz 层又依赖 store 层,接着 store 层又依赖了数据库(即依赖 pkg/db/),而 controllerbizstore 这三者又都依赖 model 层。


现在看了我的讲解,你可能有些发懵,没关系,下面我将主要代码逻辑都贴出来,加深你的理解。


assets/schema.sql 中的建表语句如下:


CREATE TABLE `user`(    `id`        BIGINT       NOT NULL AUTO_INCREMENT,    `email`     VARCHAR(255),    `nickname`  VARCHAR(255),    `username`  VARCHAR(255) NOT NULL,    `password`  VARCHAR(255) NOT NULL,    `createdAt` DATETIME,    `updatedAt` DATETIME,    PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
复制代码


user 项目仅有一张表。


cmd/main.go 代码如下:


package main
import ( user "github.com/jianghushinian/blog-go-example/wire/user/internal" "github.com/jianghushinian/blog-go-example/wire/user/internal/config" "github.com/jianghushinian/blog-go-example/wire/user/pkg/db")
func main() { cfg := &config.Config{ MySQL: db.MySQLOptions{ Address: "127.0.0.1:3306", Database: "user", Username: "root", Password: "123456", }, }
app, cleanup, err := user.NewApp(cfg) if err != nil { panic(err) }
defer cleanup() app.Run()}
复制代码


入口函数 main 中先创建了配置对象 cfg,接着实例化 app 对象,最后调用 app.Run() 启动 user 服务。


这也是一个典型的 Web 应用启动步骤。


Config 定义如下:


type Config struct {  MySQL db.MySQLOptions `json:"mysql" yaml:"mysql"`}
type MySQLOptions struct { Address string Database string Username string Password string}
复制代码


user.go 中的 App 定义如下:


// App 代表一个 Web 应用type App struct {  *config.Config
g *gin.Engine uc *controller.UserController}
// NewApp Web 应用构造函数func NewApp(cfg *config.Config) (*App, func(), error) { gormDB, cleanup, err := db.NewMySQL(&cfg.MySQL) if err != nil { return nil, nil, err } userStore := store.New(gormDB) userBiz := biz.New(userStore) userController := controller.New(userBiz) engine := gin.Default() app := &App{ Config: cfg, g: engine, uc: userController, }
return app, cleanup, err}
// Run 启动 Web 应用func (a *App) Run() { // 注册路由 InitRouter(a)
if err := a.g.Run(":8000"); err != nil { panic(err) }}
复制代码


App 代表一个 Web 应用,它嵌入了配置、gin 框架的 *Engine 对象,以及 controller


NewAppApp 的构造函数,通过 Config 来创建一个 *App 对象。


根据其内部代码逻辑,也能看出项目的 4 层架构依赖关系:创建 App 对象依赖 ConfigConfig 是通过参数传递进来的;*Engine 对象可以通过 gin.Default() 得到;而 userController 则通过 controller.New 创建,controller 依赖 bizbiz 依赖 storestore 依赖 *gorm.DB


可以发现,依赖关系非常清晰,并且我们使用了依赖注入思想编写代码,那么此时,正是 wire 的用武之地。


不过,我们先不急着讲解如何在这里使用 wire。我先将项目剩余主要代码贴出来,便于你理解这个 Web 应用。


我们可以通过 pkg/db/db.go 中的 NewMySQL 创建出 *gorm.DB 对象:


// NewMySQL 根据选项构造 *gorm.DBfunc NewMySQL(opts *MySQLOptions) (*gorm.DB, func(), error) {  // 可以用来释放资源,这里仅作为示例使用,没有释放任何资源,因为 gorm 内部已经帮我们做了  cleanFunc := func() {}
db, err := gorm.Open(mysql.Open(opts.DSN()), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) return db, cleanFunc, err}
复制代码


有了 *gorm.DB 就可以创建 store 对象了,internal/store/user.go 主要代码如下:


package store
...
// ProviderSet 一个 Wire provider sets,用来初始化 store 实例对象,并将 UserStore 接口绑定到 *userStore 类型实现上var ProviderSet = wire.NewSet(New, wire.Bind(new(UserStore), new(*userStore)))
// UserStore 定义 user 暴露的 CRUD 方法type UserStore interface { Create(ctx context.Context, user *model.UserM) error}
// UserStore 接口实现type userStore struct { db *gorm.DB}
// 确保 userStore 实现了 UserStore 接口var _ UserStore = (*userStore)(nil)
// New userStore 构造函数func New(db *gorm.DB) *userStore { return &userStore{db}}
// Create 插入一条 user 记录func (u *userStore) Create(ctx context.Context, user *model.UserM) error { return u.db.Create(&user).Error}
复制代码


有了 store 就可以创建 biz 对象了,internal/biz/user.go 主要代码如下:


package biz
...
// ProviderSet 一个 Wire provider sets,用来初始化 biz 实例对象,并将 UserBiz 接口绑定到 *userBiz 类型实现上var ProviderSet = wire.NewSet(New, wire.Bind(new(UserBiz), new(*userBiz)))
// UserBiz 定义 user 业务逻辑操作方法type UserBiz interface { Create(ctx context.Context, r *api.CreateUserRequest) error}
// UserBiz 接口的实现type userBiz struct { s store.UserStore}
// 确保 userBiz 实现了 UserBiz 接口var _ UserBiz = (*userBiz)(nil)
// New userBiz 构造函数func New(s store.UserStore) *userBiz { return &userBiz{s: s}}
// Create 创建用户func (b *userBiz) Create(ctx context.Context, r *api.CreateUserRequest) error { var userM model.UserM _ = copier.Copy(&userM, r)
return b.s.Create(ctx, &userM)}
复制代码


接着,有了 biz 就可以创建 controller 对象了,internal/controller/user.go 主要代码如下:


package controller
...
// UserController 用来处理用户请求type UserController struct { b biz.UserBiz}
// New controller 构造函数func New(b biz.UserBiz) *UserController { return &UserController{b: b}}
// Create 创建用户func (ctrl *UserController) Create(c *gin.Context) { var r api.CreateUserRequest if err := c.ShouldBindJSON(&r); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "err": err.Error(), }) return }
if err := ctrl.b.Create(c, &r); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "err": err.Error(), }) return }
c.JSON(http.StatusOK, gin.H{})}
复制代码


这些对象都有了,就可以调用 NewApp 构造出 App 了。


App 在启动前,还会调用 InitRouter 进行路由注册:


// InitRouter 初始化路由func InitRouter(a *App) {  // 创建 users 路由分组  u := a.g.Group("/users")  {    u.POST("", a.uc.Create)  }}
复制代码


现在 user 项目逻辑已经清晰了,是时候启动应用程序了:


$ cd user   $ go run cmd/main.go
复制代码


程序启动后,会监听 8000 端口,可以使用 assets/curl.sh 中的 curl 命令进行访问:


$ curl --location --request POST 'http://127.0.0.1:8000/users' \--header 'Content-Type: application/json' \--data-raw '{    "email": "jianghushinian007@outlook.com",    "nickname": "江湖十年",    "username": "jianghushinian",    "password": "pass"}'
复制代码


不出意外,你将在数据库中看到新创建的用户。


执行以下 SQL:


USE user;SELECT * FROM user;
复制代码


将输出新创建出来的用户。


+----+-------------------------------+----------+----------------+----------+---------------------+---------------------+| id | email                         | nickname | username       | password | createdAt           | updatedAt           |+----+-------------------------------+----------+----------------+----------+---------------------+---------------------+|  1 | jianghushinian007@outlook.com | 江湖十年  | jianghushinian | pass     | 2024-06-11 00:01:35 | 2024-06-11 00:01:35 |+----+-------------------------------+----------+----------------+----------+---------------------+---------------------+
复制代码


现在,是时候讨论如何在 user 项目中使用 wire 来提高开发效率了。


回顾下 NewApp 的定义:


// NewApp Web 应用构造函数func NewApp(cfg *config.Config) (*App, func(), error) {  gormDB, cleanup, err := db.NewMySQL(&cfg.MySQL)  if err != nil {    return nil, nil, err  }    userStore := store.New(gormDB)  userBiz := biz.New(userStore)  userController := controller.New(userBiz)    engine := gin.Default()  app := &App{    Config: cfg,    g:      engine,    uc:     userController,  }
return app, cleanup, err}
复制代码


其实这里面一层层的依赖注入,都是套路代码,基本上一个 Web 应用都可以按照这个套路来写。


这就涉及到套路代码写多了其实是比较烦的,这还只是一个微型项目,如果是中大项目,可以预见这个 NewApp 代码量会很多,所以是时候让 wire 出场了:


func NewApp(cfg *config.Config) (*App, func(), error) {  engine := gin.Default()  app, cleanup, err := wireApp(engine, cfg, &cfg.MySQL)
return app, cleanup, err}
复制代码


我们可以将 NewApp 中的主逻辑全部拿走,放在 wireApp 中(在 wire.go 文件中)。


wireApp 定义如下:


func wireApp(engine *gin.Engine, cfg *config.Config, mysqlOptions *db.MySQLOptions) (*App, func(), error) {  wire.Build(    db.NewMySQL,    store.ProviderSet,    biz.ProviderSet,    controller.New,    wire.Struct(new(App), "*"),  )  return nil, nil, nil}
复制代码


有了前文的讲解,其实这里无需我多言,你都能够看懂,因为并没有新的知识。


不过我们还是简单分析下这里都用到了 wire 的哪些特性。


首先 wireApp 返回值是典型的三件套:(*App, func(), error),对象、清理函数和 error


这里使用了两个 wire.ProviderSet 进行分组,定义如下:


var ProviderSet = wire.NewSet(New, wire.Bind(new(UserStore), new(*userStore)))var ProviderSet = wire.NewSet(New, wire.Bind(new(UserBiz), new(*userBiz)))
复制代码


并且在构造 wire.ProviderSet 时,还使用了 wire.Bind(new(UserStore), new(*userStore)) 将一个结构体绑定到接口。


最后,我们使用了 struct 作为 providerwire.Struct(new(App), "*") ,通配符 * 用来表示所有字段。


在真实项目中,wire 就这么使用。


如果你觉得 user 项目太小,使用 wire 的价值还不够大。你可以看看 onex 项目,比如 usercenter 中的代码,这个开源项目完全是生产级别。

为什么选择 Wire

通常来说,这部分内容是应该放在文章开头的。我将其放在这里,目的是为了让你熟悉 wire 后,再回过头来对比,wire 有哪些优势,加深你对为什么选择 wire 的理解。


其实 Go 生态中依赖注入工具不止有 Google 的 wire 一家独大,还有 Uber 开源的 dig,以及 Facebook 开源的 inject 比较流行。


但我为什么要选择 wire?


一句话概括:wire 使用代码生成,而非反射


我们可以分别举例看下 dig 以及 inject 是如何使用的。


dig 的使用示例如下:


package main
import ( "fmt" "log"
"go.uber.org/dig")

type User struct { name string}
// NewUser - Creates a new instance of Userfunc NewUser(name string) User { return User{name: name}}
// Get - A method with user as dependencyfunc (u *User) Get(message string) string { return fmt.Sprintf("Hello %s - %s", u.name, message)}
// Run - Depends on user and calls the Get method on Userfunc Run(user User) { result := user.Get("It's nice to meet you!") fmt.Println(result)}
func main() { // Initialize a new dig container container := dig.New() // Provide a name parameter to the container container.Provide(func() string { return "jianghushinian" }) // Provide a new User instance to the container using the name injected above if err := container.Provide(NewUser); err != nil { log.Fatal(err) } // Invoke the Run function; Dig automatically injects the User instance provided above if err := container.Invoke(Run); err != nil { log.Fatal(err) }}
复制代码


简单解释下示例代码:


dig.New() 实例化一个 dig 容器。


container.Provide(func() string { return "jianghushinian" }) 将一个匿名函数提供给容器。


然后调用 container.Provide(NewUser),dig 首先将字符串值 jianghushinian 作为 name 参数提供给 NewUser 函数。之后,NewUser 函数会根据此值创建出来一个 User 结构体的新实例,随后 dig 将其提供给容器。


最后,container.Invoke(Run) 会将容器中保存的 User 结构体传递给 Run 函数并运行。


我们可以类比 wire 来学习 dig:可以把 Provide 看作 providersInvoke 看作 injectors,这样就好理解了。


以上示例代码可以直接执行,无需像使用 wire 一样需要提前生成代码:


$ go run main.goHello jianghushinian - It's nice to meet you!
复制代码


这就是 dig 的使用。


再来看一个 inject 的使用示例:


package main
import ( "fmt" "log"
"github.com/facebookgo/inject")
type User struct { Name string `inject:"name"`}
// Get - A method with user as dependencyfunc (u *User) Get(message string) string { return fmt.Sprintf("Hello %s - %s", u.Name, message)}
// Run - Depends on user and calls the Get method on Userfunc Run(user *User) { result := user.Get("It's nice to meet you!") fmt.Println(result)}
func main() { // new an inject Graph var g inject.Graph
// inject name name := "jianghushinian"
// provide string value err := g.Provide(&inject.Object{Value: name, Name: "name"}) if err != nil { log.Fatal(err) }
// create a User instance and supply it to the dependency graph user := &User{} err = g.Provide(&inject.Object{Value: user}) if err != nil { log.Fatal(err) }
// resolve all dependencies err = g.Populate() if err != nil { log.Fatal(err) }
Run(user)}
复制代码


这个示例代码我就不详细讲解了,学会了 wire 和 dig,这段代码很容易理解。


可以发现的是,无论是 dig 还是 inject,它们使用的都是运行时反射机制,来实现依赖注入功能。


这会带来最直观的两个问题:


  1. 使用反射可能影响性能。

  2. 我们需要根据工具的要求编写代码,而这份代码正确与否,只有在运行期间才能确定。也就是说,代码是“黑盒”的,通过 review 代码,很难一眼看出代码是否存在问题。


而 wire 采用代码生成,它会根据我们编写的 injector 函数签名,生成最终代码。所以在执行代码之前,我们就已经有了 injector 函数的源码。


这既不会影响性能,也不会让代码变成“黑盒”,在执行程序之前我们就知道代码长什么样。而这样做还能带来一个好处,能够大大简化我们排错的过程。


Python 之禅中有一句话叫「显式优于隐式」,wire 做到了。

Wire 命令行工具

文章最后,我再来简单介绍下 wire 命令行工具。


之所以放在最后讲解,是因为 wire 的子命令确实不太常用,如果你去网上搜索,几乎没人介绍。不过为了保证文章的完整性,我还是简单讲解下,作为扩展内容,你好有个印象。


使用 --help 查看使用帮助信息。


$ wire --helpUsage: wire <flags> <subcommand> <subcommand args>
Subcommands: check print any Wire errors found commands list all command names diff output a diff between existing wire_gen.go files and what gen would generate flags describe all known top-level flags gen generate the wire_gen.go file for each package help describe subcommands and their syntax show describe all top-level provider sets
复制代码


可以发现 wire 连最基本的 --version 命令都不存在,即不支持查看版本信息。起初这点我是疑惑的,不过看了官方描述,也就不足为奇了。因为 wire 已经不再加入新功能,所以你可以理解为它就这一个版本。


官方描述说当前项目状态不接受新功能,只接受错误报告和 Bug fix。看来官方也想保持 wire 的简洁。


有人说项目不维护了。但我认为这又何尝不是一件好事情,其实项目还在维护,只是不增加新功能了。这在日新月异的技术行业里,是好事,极大的好事。我们不用投入太多精力学习这个工具,学一次受用很久。这也是我写这篇想着尽量把 wire 功能介绍完全,方便大家学习。


回归正题,首先要讲解的是 gen 子命令。已经是我们的老朋友了,可以根据我们编写的 injector 函数签名,自动生成目标代码。


其实如果直接使用 wire 命令,后面什么也不接,wire 默认会调用 gen 子命令:


$ wire       wire: github.com/jianghushinian/blog-go-example/wire/getting-started: wrote /Users/jianghushinian/projects/blog-go-example/wire/getting-started/wire_gen.go
复制代码


check 子命令可以帮我们检查代码错误,比如我们将 Wire 快速开始 部分的示例中的 injector 函数 InitializeEvent 故意写错。


InitializeEvent 代码如下:


func InitializeEvent() Event {  wire.Build(NewEvent, NewGreeter, NewMessage)  return Event{}}
复制代码


现在修改成错误的,漏写了 NewMessage 方法:


func InitializeEvent() Event {  wire.Build(NewEvent, NewGreeter)  return Event{}}
复制代码


使用 wire check 检查代码错误:


$ wire checkwire: wire.go:7:1: inject InitializeEvent: no provider found for github.com/jianghushinian/blog-go-example/wire/getting-started.Message        needed by github.com/jianghushinian/blog-go-example/wire/getting-started.Greeter in provider "NewGreeter" (main.go:15:6)        needed by github.com/jianghushinian/blog-go-example/wire/getting-started.Event in provider "NewEvent" (main.go:27:6)wire: error loading packages
复制代码


但其实我们直接执行 wire 命令生成代码时,也会得到相同的错误。


commands 子命令可以打印 wire 支持的所有子命令,嗯,仅此而已。


$ wire commandscommandsflagshelpcheckdiffgenshow
复制代码


flags 子命令可以打印每个子命令接收的标志:


$ wire flags gen  -header_file string        path to file to insert as a header in wire_gen.go  -output_file_prefix string        string to prepend to output file names.  -tags string        append build tags to the default wirebuild
复制代码


可以发现 gen 子命令支持 3 个标志,至于效果你可以自行尝试。


diff 子命令用于打印 wire 生成的 wire_gen.go 文件和之前有何不同:


$ wire diff .    github.com/jianghushinian/blog-go-example/wire/getting-started: diff from wire_gen.go:@@ -11,2 +11,2 @@-func InitializeEvent() Event {-       message := NewMessage()+func InitializeEvent(string2 string) Event {+       message := NewMessage(string2)
复制代码


show 子命令用于分析和展示指定包中的依赖注入配置:


$ wire show .    
Injectors: "github.com/jianghushinian/blog-go-example/wire/getting-started".InitializeEvent
复制代码


wire 命令行工具的讲解就介绍到这里。

总结

终于到了总结环节,又是喜闻乐见的万字长文系列,上下两篇共计 2w+ 字。


本文主旨是为了讲解在 Go 中,如何优雅的使用 wire 依赖注入工具提高开发效率。


首先介绍了什么是依赖注入,以及在 Go 中如何使用依赖注入思想编写代码。


接着又对依赖注入工具 wire 进行了简单介绍,并安装了 wire 命令行工具。


然后通过一个 wire 快速开始的示例程序,极速入门 wire 的使用。


有了使用经验,我又讲解了为什么要使用 wire?因为它们帮我们自动生成依赖注入代码,提高开发效率。


接下来我对 wire 的核心概念进行了讲解。我们知道了什么是 providersinjectors,知道了这两个核心概念,wire 就入门了。


我还介绍了 wire 和很多高级特性。injector 函数支持参数,也支持返回清理函数和错误。我们可以使用 ProviderSetproviders 进行分组。可以使用 wire.Struct 将一个结构体作为 provider。也可以指定结构体的具体某个字段作为 providerwire.Value 可以将一个值构造成 providerwire.InterfaceValue 可以将一个接口构造成 provider。通过 wire.Bind(new(Fooer), new(MyFoo))) 可以将 MyFoo 结构体绑定到 Fooer 接口。wire 还为我们提供了备用注入器语法,可以使用 panic 取代在 injector 函数中编写返回值。


wire 的用法都讲解完成以后,我又以一个 user Web 应用作为案例,为你讲解了在生产实践中 wire 的使用。


既然我们学会了 wire,那就应该知道我们为什么要选择使用 wire。我对比了 Uber 开源的 dig,以及 Facebook 开源的 inject,为你讲解了选择 wire 的原因。可以用一句话概括:wire 使用代码生成,而非反射。


最后,我又简单介绍了 wire 命令行工具的使用。


记住,依赖注入并不神秘,wire 的作用也显而易见,就是为了解放双手。如果你更喜欢手动编写代码,那么也完全没有任何问题。不要过于神化依赖注入工具,起码在 Go 语言中是这样。


本文示例源码我都放在了 GitHub 中,欢迎点击查看。


由于篇幅所限,有些示例文章中并没有给出执行结果,你一定要把我的示例代码 clone 下来,依次执行一遍,这样才能更加深刻的理解。


至此本文完结,如果你想要更深入的了解 wire,那就去看它的源码吧,祝你好运 :)。


希望此文能对你有所启发。


延伸阅读


  • Compile-time Dependency Injection With Go Cloud's Wire: https://go.dev/blog/wire

  • Wire README: https://github.com/google/wire/blob/main/README.md

  • Wire Documentation: https://pkg.go.dev/github.com/google/wire/internal/wire

  • Wire 源码: https://github.com/google/wire

  • onex usercenter: https://github.com/superproj/onex/tree/master/internal/usercenter

  • Go Dependency Injection with Wire: https://blog.drewolson.org/go-dependency-injection-with-wire/

  • Golang Dependency Injection Using Wire: https://clavinjune.dev/en/blogs/golang-dependency-injection-using-wire/

  • Dependency Injection in Go using Wire: https://www.mohitkhare.com/blog/go-dependency-injection/

  • Wire 依赖注入: https://go-kratos.dev/docs/guide/wire/

  • Dependency Injection with Dig: https://www.jetbrains.com/guide/go/tutorials/dependency_injection_part_one/di_with_dig/

  • inject Documentation: https://pkg.go.dev/github.com/facebookgo/inject

  • Build Constraints: https://pkg.go.dev/go/build#hdr-Build_Constraints

  • 控制反转: https://zh.wikipedia.org/wiki/控制反转

  • 依赖注入: https://zh.wikipedia.org/wiki/依赖注入

  • SOLID (面向对象设计): https://zh.wikipedia.org/wiki/SOLID_(面向对象设计)

  • 设计模式之美 —— 19 | 理论五:控制反转、依赖反转、依赖注入,这三者有何区别和联系?: https://time.geekbang.org/column/article/177444

  • 本文 GitHub 示例代码:https://github.com/jianghushinian/blog-go-example/tree/main/wire


联系我


发布于: 刚刚阅读数: 6
用户头像

江湖十年

关注

野生程序员 2018-11-10 加入

分享不限于 Go、Python、Docker、K8s 技术。

评论

发布
暂无评论
万字长文:在 Go 中如何优雅的使用 wire 依赖注入工具提高开发效率?下篇_golang_江湖十年_InfoQ写作社区