Wire简介
Wire 是一款使用了依赖注入的来让连接组件变得自动化的代码生成工具。组件之间依赖关系在 Wire 种体现为函数的参数,Wire 鼓励显式地初始化参数而不是定义全局变量。因为 Wire 在运行时不依赖运行时状态和反射,使用 Wire 写出来的代码甚至可以替代手写的初始化代码。
官方介绍文档:introductory blog post.
安装
使用下面的命令安装:
go get github.com/google/wire
# 安装wire命令行工具用于生成依赖注入代码,在wire.go文件的同级位置输入wire命令即可,会生成
# wire_gen.go文件,里面包含真正的依赖注入代码。
go install github.com/google/wire/cmd/wire@latest
文档
快速开始
首先需要在项目跟目录下或者其他什么地方(最好跟 main.go 文件同级)创建一个 wire.go 文件,然后使用 // +build wireinject 标记这个文件,表示让编译器忽略这个文件不编译。
在 main.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 文件中声明以下函数:
// +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 文件,使用生成的依赖注入代码来初始化依赖关系并执行相关操作:
package main
func main() {
event := InitializeEvent()
event.Start()
}
通过运行 go run main.go,你将会看到程序输出了预定义的消息。
最佳实践
创建工程
下面介绍如何将 wire 与 工程化实践结合,将 wire 与 HTTP 服务器程序整合到一起。首先,创建一个工程设置好 go mod 属性。
安装好 wire 依赖:
go get github.com/google/wire
创建 main.go 文件,现在可以什么都不写。在项目根目录下,创建一个 wire 目录,在里面创建一个 wire.go 文件(别问我为什么,因为这是开发惯例),wire.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,输入以下命令安装:
go get go.uber.org/zap
go get github.com/spf13/viper
可以看到项目还缺少 provider 包,这是项目内置的包。
创建provider包
接着上一节,在项目根目录下创建 provier 包,包内创建 provider.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:
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:
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:
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:
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:
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:
package dao
type UserDao struct {
*Dao
}
func NewUserDao(dao *Dao) *UserDao {
return &UserDao{Dao: dao}
}
UserDao 继承了 Dao,就可以使用它内部的事先注入好的对象。
创建server包
在项目的根目录里面创建一个 server 包,创建一个 server.go 文件,在里面编写有关创建 HTTP 服务器的代码:
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 项目的配置文件:
app:
port: 8080
完善main.go文件
现在开始编写 main 函数:
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!
目录图