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

1,738 阅读6分钟

ChatGPT Image 2026年1月13日 09_52_47.png

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

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

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

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

目录图

源代码

源代码仓库地址:wire-first: 依赖注入库wire的示例工程 (gitee.com)