Golang 使用 Wire 进行依赖注入

前言

在参与 Star Horizon 的一个项目时,我得到团队内同学的启发,发现了 Wire 这个神奇的东西。Wire 是 Google 研发并开源的一个 Golang 依赖注入解决方案,它通过解释原有文件生成新文件并用 Go Build Injector 实现编译环节的代码区分。

根据 Go Blog 所属,Wire 最先用于 Google 开源的 Go Cloud 项目中。

google/wire

应用

场景

我们通常会在构建 SDK 时在 Client 中嵌套 Services,在这种场景下 Client 依赖于 Services。

但常见的问题是:随着 Services 的增多,代码变得越来越冗杂,甚至增加一个 Service 就要进行数十行的代码变更。而一旦有新人参与开发,很容易漏掉一些必须的代码变更从而引起
Bug 的产生。

Google 自然也是面临这种问题,毕竟其体量巨大。为此,他们创造了 Wire 用来解决依赖注入问题。

安装

首先,我们需要在项目中引入 github.com/google/wire 这一 Package 进行标识,同时在全局安装:

# 项目中添加依赖
go get -u github.com/google/wire

# 全局安装 Wire 的命令行程序
go install github.com/google/wire/cmd/wire@latest

随后,我们可以通过 wire 调用命令行程序:

bafkreicnx4argsmo5i2rct2jbqmbwihd74tx4nyqilrisgzw4hodn5wq4m.png

Wire 进行依赖注入的过程

当然,调用之前需要我们先进行完下面的步骤。

差异

传统依赖注入方法

通常情况下,在 pkg 中配置 Client 我们会进行如下操作

// service/foo.go

package service

import "fmt"

type Foo interface {
  Foo()
}

type foo struct {
}

func (f *foo) Foo() {
  fmt.Println("foo")
}

func NewFoo() Foo {
  return &foo{}
}
// service/bar.go

package service

import "fmt"

type Bar interface {
  Bar()
}

type bar struct {
}

func (b *bar) Bar() {
  fmt.Println("bar")
}

func NewBar() Bar {
  return &bar{}
}
// service/client.go

package service

type Client struct {
  Foo Foo
  Bar Bar
}

func NewClient(foo Foo, bar Bar) *Client {
  return &Client{
    Foo: foo,
    Bar: bar,
  }
}
// main.go

package main

import "wire_tutorial/service"

func main() {
  foo := service.NewFoo()
  bar := service.NewBar()
  client := service.NewClient(foo, bar)
  client.Foo.Foo()
  client.Bar.Bar()
}

此时编译执行当前目录我们会得到如下结果:

bafkreiba3cppzn4t5vbwn3vvj6qr7iwj24i7ob5bthtvnwnd6633mk6t7u.png

在当前场景,Client 依赖于 FooBar。这是一个非常微小的 Client,所以可能并不是很能体现依赖注入的问题。

使用 Wire

在原有代码不变的基础下,我们新增了 service/wire.go 并修改了 main.go

// service/wire.go

package service

import "github.com/google/wire"

func BuildClient() *Client {
  wire.Build(NewClient, NewFoo, NewBar)
  return nil
}
// main.go

package main

import "wire_tutorial/service"

func main() {
  client := service.BuildClient()
  client.Foo.Foo()
  client.Bar.Bar()
}

当前情况下,如果你编译运行会发现,因为 Client 的属性 Foo Bar 都为 nil
,因此会直接报出 panic: runtime error: invalid memory address or nil pointer dereference 的错误。

此时,我们在终端执行 wire ./...

wire ./...

# 或
go run github.com/google/wire/cmd/wire@latest ./...

会发现 Wire 创造了一个新文件:service/wire_gen.go

bafkreibq774jq3eeggqhtkadmlmw6yao5zhvldcxxskm2jtjm7uh2ofwpq.png

而这个生成文件的内容是这样:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
// 需要注意,上边的两行叫做 Injector,其意义是在执行 go build 的时候如果携带 wireinject 的 arg 就不编译当前文件

package service

// Injectors from wire.go:

func BuildClient() *Client {
  serviceFoo := NewFoo()
  serviceBar := NewBar()
  client := NewClient(serviceFoo, serviceBar)
  return client
}

此时如果你再进行编译会发现仍然无法正常运行,因为 service/wire.goservice/wire_gen.goBuildClient() *Client
函数起了冲突,而此时我们可以根据 Google 的实例,通过 Injector 来避免项目打包时同时编译两个文件:

// service/wire.go

//go:build wireinject
// +build wireinject

package service

import "github.com/google/wire"

func BuildClient() *Client {
  wire.Build(NewClient, NewFoo, NewBar)
  return nil
}

此时,只有携带 wireinject 的参数进行编译时,当前文件才会被编译在软件内。

再次编译,发现程序可以正常输出,此时 Wire 的配置就完成了。

bafkreibcaephy2fv2ywhpaxc6x4qfsh4bqirukrtd4mvsnfx2z4dznco7a.png

编译目录

日后只有在修改 service/wire.go 时才需要重新生成 service/wire_gen.go,因此根据 Google
的文档所述,你可以将 service/wire_gen.go 带入版本控制系统而非每次重新生成。

更多

参数传递

Wire 对于参数传递具有限制。

例如,对于以下情况的代码,他无法生成 wire_gen.go 文件:

// service/client.go

package service

import "fmt"

type Client struct {
  Foo Foo
  Bar Bar
}

var Debug bool

func NewClient(foo Foo, bar Bar) *Client {
  if Debug {
    fmt.Println("debug mode")
  }
  
  return &Client{
    Foo: foo,
    Bar: bar,
  }
}
// service/wire.go

//go:build wireinject
// +build wireinject

package service

import "github.com/google/wire"

func BuildClient() *Client {
  wire.Build(Debug, NewClient, NewFoo, NewBar)
  return &Client{}
}
// service/main.go

package main

import (
  "flag"
  "wire_tutorial/service"
)

var (
  debug bool
)

func init() {
  flag.BoolVar(&debug, "debug", false, "debug mode")
  flag.Parse()
}

func main() {
  service.Debug = debug
  client := service.BuildClient()
  client.Foo.Foo()
  client.Bar.Bar()
}

此时,执行 wire ./... 会出现以下错误:

这是因为 Wire 将 NewClient 这类函数当做 Provider,而 Debug 这个变量并非一个合法的 Provider。

我们只需做如下操作:

// service/client.go

package service

import "fmt"

type Client struct {
  Foo Foo
  Bar Bar
}

// 注意此处将全局变量 Debug 移动到了函数参数内
func NewClient(debug bool, foo Foo, bar Bar) *Client {
  if debug {
    fmt.Println("debug mode")
  }

  return &Client{
    Foo: foo,
    Bar: bar,
  }
}
// service/wire.go

//go:build wireinject
// +build wireinject

package service

import "github.com/google/wire"

// 此处新增了和上方类型名称一致的形参 debug
func BuildClient(debug bool) *Client {
  wire.Build(NewClient, NewFoo, NewBar)
  return &Client{}
}
// main.go

package main

import (
  "flag"
  "wire_tutorial/service"
)

var (
debug bool
)

func init() {
  flag.BoolVar(&debug, "debug", false, "debug mode")
  flag.Parse()
}

func main() {
  client := service.BuildClient(debug)
  client.Foo.Foo()
  client.Bar.Bar()
}

此时重新执行 wire ./...,会发现 service/wire_gen.go 变为如下内容:

// Code generated by Wire. DO NOT EDIT.

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

package service

// Injectors from wire.go:

func BuildClient(debug bool) *Client {
  serviceFoo := NewFoo()
  serviceBar := NewBar()
  client := NewClient(debug, serviceFoo, serviceBar)
  return client
}

此时,功能可正常使用。

鸣谢

Google 提供的文档用于理解 Wire 的基本原理。

@topjohncian 使我了解 Wire 和其基本使用方法。

Read more

Web 后端应用程序的可观测性改进

Web 后端应用程序的可观测性改进

相信日志,即 Logging,对于大多数开发人员是不陌生的。 日志是一种记录应用程序运行状态的重要手段,它可以帮助我们了解应用程序的运行情况,排查问题,甚至是监控应用程序的性能。在 Web 后端应用程序中,日志是不可或缺的一部分,它可以记录用户请求、应用程序的错误、警告等信息,帮助我们更好地了解应用程序的运行情况。 但它真的不可或缺吗?读完这篇文章后我想我给你的答案是:不是。日志的形式很单一,只是文本,这注定了它很冗杂的特点,我们很难从中提取我们需要的信息。即使你使用 AI 如 ChatGPT,也并不一定可以得到一个有建设性的答案。对于自己构建的应用程序,ChatGPT 既不了解也不可能去真的全部了解你的代码,这就带来了问题。 为什么日志不是不可或缺的 日志的形式单一,以纯文本呈现,信息常常显得冗余且难以提取。即便是使用 AI 进行分析,也不一定能提供清晰的洞见。日志的主要问题在于: * 冗余性和庞大的数据量:日志往往包含大量无用信息,查找特定问题的关键信息耗时。 * 缺乏上下文关联:单条日志难以呈现多个服务之间的调用关系和上下文,尤其是在微服务架构中

By AHdark
我如何从零开始学习一门编程语言

我如何从零开始学习一门编程语言

作为一门全栈开发,我理应掌握多门语言。截止 2024 年 9 月,我掌握了超过 30 门编程语言,可以使用它们构建简单的应用程序、为开源社区提交代码、为公司开发产品。我们不讨论对于“掌握”的定义,让我梳理思路,详细阐述我是怎么一步步掌握如 此多的编程语言的。如果你也想学习一门新的编程语言,希望这篇文章能够帮助到你。

By AHdark