[wire]Go依赖注入库wire最佳实践

original-59266f4dea1c2aa43f2064cc0f3b165a.webp

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 结构体依赖于 MessageEvent 结构体依赖于 Greeter。通过 NewGreeterNewEvent 函数来创建相应的实例。

从上面的代码可以看出,一个结构体的构造函数依赖于另一个结构体对象,然后进行组合赋值,一层嵌套一层。如果手动编写初始化函数的话,在结构体比较多的情况下会非常繁琐。

这时候就需要代码生成器来解决这个问题了。

在 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!

目录图

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值