Wire简介
Wire 是一款使用了依赖注入的来让连接组件变得自动化的代码生成工具。组件之间依赖关系在 Wire 种体现为函数的参数,Wire 鼓励显式地初始化参数而不是定义全局变量。因为 Wire 在运行时不依赖运行时状态和反射,使用 Wire 写出来的代码甚至可以替代手写的初始化代码。
官方介绍文档:introductory blog post.
安装
使用下面的命令安装:
shell go get github.com/google/wire/cmd/wire
文档
快速开始
首先需要在项目跟目录下或者其他什么地方(最好跟 main.go 文件同级)创建一个 wire.go 文件,然后使用 // +build wireinject
标记这个文件,表示让编译器忽略这个文件不编译。
在 main.go 中声明以下几个结构体,它们之间存在着依赖关系:
```go package main
import "fmt"
type Message string
type Greeter struct { Message Message }
func NewGreeter(message Message) Greeter { return Greeter{Message: message} }
func (g Greeter) Greet() { fmt.Println(g.Message) }
type Event struct { Greeter Greeter }
func NewEvent(greeter Greeter) Event { return Event{Greeter: greeter} }
func (e Event) Start() { e.Greeter.Greet() } ```
在这个文件中,我们定义了 Greeter
结构体和 Event
结构体。Greeter
结构体依赖于 Message
,Event
结构体依赖于 Greeter
。通过 NewGreeter
和 NewEvent
函数来创建相应的实例。
从上面的代码可以看出,一个结构体的构造函数依赖于另一个结构体对象,然后进行组合赋值,一层嵌套一层。如果手动编写初始化函数的话,在结构体比较多的情况下会非常繁琐。
这时候就需要代码生成器来解决这个问题了。
在 wire.go 文件中声明以下函数:
```go // +build wireinject
package main
import "github.com/google/wire"
func InitializeEvent() Event { wire.Build(NewEvent, NewGreeter, NewMessage) return Event{} } ```
在这个文件中,我们定义了一个 InitializeEvent
函数,用于初始化依赖关系。使用 wire.Build
函数来声明依赖关系,并指定需要注入的结构体。
使用 wire
命令来生成依赖注入的代码。运行该命令后,会自动生成一个名为 wire_gen.go
的文件,其中包含了自动生成的依赖注入代码。最后,创建一个 main.go
文件,使用生成的依赖注入代码来初始化依赖关系并执行相关操作:
```go package main
func main() { event := InitializeEvent() event.Start() } ```
通过运行 go run main.go
,你将会看到程序输出了预定义的消息。
最佳实践
创建工程
下面介绍如何将 wire 与 工程化实践结合,将 wire 与 HTTP 服务器程序整合到一起。首先,创建一个工程设置好 go mod 属性。
安装好 wire 依赖:
shell go get github.com/google/wire
创建 main.go 文件,现在可以什么都不写。在项目根目录下,创建一个 wire 目录,在里面创建一个 wire.go 文件(别问我为什么,因为这是开发惯例),wire.go 文件中编写以下内容:
```go //go:build wireinject // +build wireinject
package wire
import ( "net/http"
"github.com/google/wire" "github.com/spf13/viper" "go.uber.org/zap"
"wire-first/provider" )
// wire.go 初始化模块 func NewApp(viper.Viper, *zap.Logger) (http.Server, error) { panic(wire.Build( provider.ServerSet, provider.HandlerSet, provider.ServiceSet, provider.DaoSet, )) } ```
这时候我们还没有安装日志库 zap 和配置库 viper,输入以下命令安装:
```shell go get go.uber.org/zap
go get github.com/spf13/viper ```
可以看到项目还缺少 provider 包,这是项目内置的包。
创建provider包
接着上一节,在项目根目录下创建 provier 包,包内创建 provider.go 文件,编写以下内容:
```go package provider
import ( "github.com/google/wire"
"wire-first/dao" "wire-first/handler" "wire-first/server" "wire-first/service" )
var ServerSet = wire.NewSet(server.NewServerHttp)
var HandlerSet = wire.NewSet(handler.NewHandler, handler.NewUserController, handler.NewRoleController)
var ServiceSet = wire.NewSet(service.NewService, service.NewUserService, service.NewRoleService)
var DaoSet = wire.NewSet(dao.NewDao, dao.NewUserDao, dao.NewRoleDao) ```
可能到这里你会觉得有疑问,NewSet 函数是什么?NewSet 函数是 wire 库提供的一个类似于分组的函数,将一些依赖注入项分组便于管理。上面代码可以清晰地看出分成了四组。第一个是 ServerSet 就是服务器层的依赖注入,第二个是处理器层的组,第三个是服务层的组,第四个是持久层的组,这是典型的业务层划分。
创建分层的包
现在开始创建对应的层次的包然后创建对应的文件。先创建 handler 包,创建 handler.go 和 user_handler.go 文件,其他 handler 文件就不演示了。
handler.go:
```go package handler
import ( "github.com/spf13/viper" "go.uber.org/zap" )
type Handler struct { conf *viper.Viper logger *zap.Logger }
func NewHandler(conf *viper.Viper, logger *zap.Logger) *Handler { return &Handler{conf: conf, logger: logger} } ```
handler.go 中声明一了 Handler 结构体这是为了让其他 Handler 都继承它,这样就可以共用全局配置和日志对象了,这点就是为了弥补没有像 SpringBoot 那样的操作字节码运行时注入功能了。
user_handler.go:
```go package handler
import "wire-first/service"
type UserHandler struct { *Handler userService *service.UserService }
func NewUserController(handler *Handler, userService *service.UserService) *UserHandler { return &UserHandler{ Handler: handler, userService: userService, } } ```
这样 UserHandler 就可以使用全局配置和日志对象了。这也可以看出 wire 是通过编译时构造器注入的。UserHandler 内部依赖了 UserService 结构体的指针,是不是很熟悉这个老味道?
完成之后开始创建 service 包,创建 service.go 和 user_service.go 文件。
service.go:
```go package service
import ( "github.com/spf13/viper" "go.uber.org/zap" )
type Service struct { conf *viper.Viper logger *zap.Logger }
func NewService(conf *viper.Viper, logger *zap.Logger) *Service { return &Service{conf: conf, logger: logger} } ```
同样是内部依赖了全局配置和日志对象,这就是为了处处使用这些对象。
user_service.go:
```go package service
import "wire-first/dao"
type UserService struct { *Service userDao *dao.UserDao }
func NewUserService(service *Service, userDao *dao.UserDao) *UserService { return &UserService{ Service: service, userDao: userDao, } } ```
UserService 继承了 Service 结构体,内部还依赖 UserDao 指针。
开始创建 dao.go 和 user_dao.go 文件。
dao.go:
```go package dao
import ( "github.com/spf13/viper" "go.uber.org/zap" )
type Dao struct { conf *viper.Viper logger *zap.Logger }
func NewDao(conf *viper.Viper, logger *zap.Logger) *Dao { return &Dao{conf: conf, logger: logger} } ```
Dao 结构体内部还可以依赖其他数据库操作对象,比如说 MySQL、Redis、MongoDB...按照业务需求添加即可。
user_dao.go:
```go package dao
type UserDao struct { *Dao }
func NewUserDao(dao *Dao) *UserDao { return &UserDao{Dao: dao} } ```
UserDao 继承了 Dao,就可以使用它内部的事先注入好的对象。
创建server包
在项目的根目录里面创建一个 server 包,创建一个 server.go 文件,在里面编写有关创建 HTTP 服务器的代码:
```go package server
import ( "fmt" "net/http"
"github.com/spf13/viper" "go.uber.org/zap"
"wire-first/handler" )
func NewServerHttp( conf *viper.Viper, logger *zap.Logger, userHandler *handler.UserHandler, ) *http.Server { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("你好世界!")) }) server := http.Server{ Handler: mux, Addr: fmt.Sprintf(":%s", conf.GetString("app.port")), } return &server } ```
可以看到,UserHandler 通过参数传进来了,其他全局配置和日志对象也是通过参数传进来了。到这里的时候,就可以利用传进来的 conf 和 handler 进行对 Server 的配置以及路由的设置。
最终返回 http.Server 对象。这里的 http 框架你也可以换成别的框架,比如说 Gin、Fiber、Echo、Beego 等,我这里演示的是最原始的 net/http 包以及它的多路复用器。
创建config包
现在需要在项目中创建 config 包用于做全局配置对象 viper 的初始化。
config/config.go:
``` package config
import ( "github.com/spf13/viper" )
func NewConfig(path string) *viper.Viper { conf := viper.New() conf.SetConfigFile(path) err := conf.ReadInConfig() if err != nil { panic(err) } return conf } ```
最后还需要创建一个 app.yml 项目的配置文件:
yml app: port: 8080
完善main.go文件
现在开始编写 main 函数:
```go package main
import ( "github.com/alecthomas/kingpin/v2" "go.uber.org/zap"
"wire-first/config" "wire-first/wire" )
var ( cfgPath = kingpin.Flag("config", "the path of the config file").Default("app.yml").String() )
func main() { kingpin.Parse() conf := config.NewConfig(*cfgPath) // 创建logger logger, err := zap.NewDevelopment() if err != nil { panic(err) } defer logger.Sync() app, err := wire.NewApp(conf, logger) if err != nil { logger.Error("Initialization failed", zap.Error(err), ) }
logger.Info("Server's running", zap.String("address", app.Addr)) if err := app.ListenAndServe(); err != nil { logger.Error("Server Error", zap.String("key", "value"), zap.Error(err), ) } } ```
可以看到配置文件路径通过命令行参数获取,logger 和 viper 的初始化都是通过在 main 函数里面进行然后传入到 NewApp 函数里面返回 http.Server 对象,最后启动服务器。
大体的 wire 整合 Web 后端项目差不多就是这样,但是你也可以根据需求自定义,不一定按照这个来。
Happ Hacking! Gopher!