wire优雅的管理go项目

2,359 阅读6分钟

前言

你是否遇到这种情况,为go项目当中定义了太多的全局变量以及紊乱的依赖关系而苦恼。

就拿一个user模块举例

db := config.NewGormDB()
userDao := dao.NewUserDao(db)
userRepo := repo.NewUserRepo(userDao)
userService := service.NewUserService(userRepo)
userHandler := api.NewUserHandler(userService)

为了管理这些模块,需要自己手动分包,以及管理子模块中Router的引入,然后再统一从子模块引入初始化,有时候处理不当就会导致空指针异常,因为没有正确的处理好依赖关系

快速开始

wire库恰好可以帮助解决这个问题,谷歌出品,必定难用,对初学者非常的不友好

安装

go install github.com/google/wire/cmd/wire@latest

项目引入

go get github.com/google/wire/cmd/wire

测试

wire -h # 查看是否安装成功

项目初始化

我们举个简单的案例,假设我们现在已经写好user模块

domian层

package domain

import "gorm.io/gorm"

type User struct {
	gorm.Model `gorm:"embedded"`
	Name       string `gorm:"unique"`
	Age        int
	Gender     string
}

dao层

package dao

import (
	"gorm.io/gorm"
	"wire_learn/internal/user/domain"
)

type UserDao struct {
	db *gorm.DB
}

func NewUserDao(db *gorm.DB) *UserDao {
	return &UserDao{db}
}

type IUserDao interface {
	Create(user domain.User) error
	FindIsExistByName(name string) bool
}

var _ IUserDao = (*UserDao)(nil)

func (r *UserDao) Create(user domain.User) error {
	return r.db.Create(&user).Error
}

func (r *UserDao) FindIsExistByName(name string) bool {
	return r.db.Where("name=?", name).First(&domain.User{}).RowsAffected != 0
}

service层

package service

import (
	"wire_learn/internal/user/domain"
	"wire_learn/internal/user/repo"
)

type UserService struct {
	repo repo.IUserRepo
}

type IUserService interface {
	CreateUser(user domain.User) error
}

var _ IUserService = (*UserService)(nil)

func NewUserService(repo repo.IUserRepo) *UserService {
	return &UserService{repo}
}

func (s *UserService) CreateUser(user domain.User) error {
	return s.repo.Create(user)
}

api层

package api

import (
	"github.com/gin-gonic/gin"
	"net/http"
	"wire_learn/internal/user/domain"
	"wire_learn/internal/user/service"
	"wire_learn/pkg/resp"
)

type UserHandler struct {
	serv service.IUserService
}

type IUserHandler interface {
	Login(c *gin.Context)
}

var _ IUserHandler = (*UserHandler)(nil)

func NewUserHandler(serv service.IUserService) *UserHandler {
	return &UserHandler{
		serv: serv,
	}
}

func (h *UserHandler) Login(c *gin.Context) {
	var userReq UserReq
	if err := c.ShouldBindJSON(&userReq); err != nil {
		resp.FailWithMsg(c, http.StatusBadRequest, "UserReq类型不匹配")
		return
	}
	user := domain.User{
		Name:   userReq.Username,
		Age:    userReq.Age,
		Gender: userReq.Gender,
	}
	err := h.serv.CreateUser(user)
	if err != nil {
		resp.FailWithMsg(c, http.StatusBadRequest, err.Error())
		return
	}
	resp.Success(c)
}

不使用wire怎么写

在没有接触wire之前,我是这样写的

  • 初始化gormDB,挂载到全局变量DB上
  • 然后新建一个router文件夹,里面分包,在user模块下建立一个initUserRouter的函数初始化当前模块的路由(初始化dao,repo,service,api)
  • 然后在router文件夹下的enter.go入口文件初始化总路由initAppRouter,然后在main.go中引入。

使用wire又该怎么写呢

我会慢慢的引导大家,报错先别急

第一步 wire.Build

main.go的同级目录下新建一个wire.go

func InitUserHandler() *api.UserHandler {
	wire.Build(
        config.NewGormDB, # 绑定db
		api.NewUserHandler,
		service.NewUserService,
		repo.NewUserRepo,
		dao.NewUserDao,
	)
	return nil
}

含义

wire.Build中写入各个模块的构造函数,db->dao->repo->service->api,返回值写入你想要生成函数的返回值,通常是最高层模块对应的结构体指针

然后在wire.go中所在的目录打开终端输入:

wire

出现报错

D:\Code\Go\wire-learn>wire
wire: D:\Code\Go\wire-learn\wire.go:11:1: inject InitUserHandler: no provider found for *github.com/gin-gonic/gin.Engine
        needed by *wire_learn/internal/user/api.UserHandler in provider "NewUserHandler" (D:\Code\Go\wire-learn\internal\user\api\user.go:21:6)
wire: D:\Code\Go\wire-learn\wire.go:11:1: inject InitUserHandler: no provider found for wire_learn/internal/user/service.IUserService
        needed by *wire_learn/internal/user/api.UserHandler in provider "NewUserHandler" (D:\Code\Go\wire-learn\internal\user\api\user.go:21:6)
wire: wire_learn: generate failed
wire: at least one generate failure

在软件设计层面,一般要遵循依赖倒置原则

也就是说,高层模块要维护低层模块的抽象类(接口)

代码层面,UserHandler要维护IUserService这个接口

type UserHandler struct {
	serv service.IUserService
}

所以说要让wire去识别某个接口实现了某个结构体,比如说api.NewUserHandler返回的是IUserService,而service.NewUserService依赖于UserService结构体,而不是IUserService接口,所以要绑定接口和结构体之间的关系

第二步 wire.Bind

func InitUserHandler() *service.UserService {
	wire.Build(
		config.NewGormDB,
        wire.Bind(new(repo.IUserService), new(*repo.UserService)),
		wire.Bind(new(repo.IUserRepo), new(*repo.UserRepo)),
		wire.Bind(new(dao.IUserDao), new(*dao.UserDao)),
        api.NewUserHandler,
		service.NewUserService,
		repo.NewUserRepo,
		dao.NewUserDao,
	)
	return nil
}

第三步 使用wire.NetSet

有时候可能多个类型有相同的依赖,我们每次都将相同的构造器传给wire.Build()不仅繁琐,而且不易维护,一个依赖修改了,所有传入wire.Build()的地方都要修改。为此,wire提供了一个ProviderSet(构造器集合),可以将多个构造器打包成一个集合,后续只需要使用这个集合即可。

在user模块下新建ioc.go

// UserProvider 依赖注入
var UserProvider = wire.NewSet(
	config.NewGormDB,
    wire.Bind(new(repo.IUserService), new(*repo.UserService)),
    wire.Bind(new(repo.IUserRepo), new(*repo.UserRepo)),
    wire.Bind(new(dao.IUserDao), new(*dao.UserDao)),
    api.NewUserHandler,
    service.NewUserService,
    repo.NewUserRepo,
    dao.NewUserDao,
)

然后在wire.Bind中注入

func InitUserHandler() *service.UserService {
	wire.Build(
		ioc.UserProvider
	)
	return nil
}

这样就能有效管理provider,统一引入,防止大量代码写在一个Build里面

第四步 生成代码

wire # 识别当前目录的wire.build
wire ./... # 识别当前目录和子目录

第五步 添加wireinject

wire.go头部上添加

//go:build wireinject
// +build wireinject

因为同个包下不能有两个相同的函数名,否则报错,保留自动化生成的wire_gen.go文件

在gin中优雅的使用wire

重点

又回到刚刚那个问题,怎么做才能避免初始化大量的Router和比较有效的避免使用一些没有意义的全局变量

大多数的数据库,对象初始化应该是一个单例模式,所以只能有一个wire.go,如果说在不同的模块下建立多个wire.go,这样做法是错误的。

为了解决Router的问题,应该在NewHandler(即初始化api接口构造函数)的时候,*将router gin.Engine传入,api初始化的时候顺带挂载路由

第一步 优化api层的构造函数

func NewUserHandler(r *gin.Engine, serv service.IUserService) *UserHandler {
	h := &UserHandler{
		serv: serv,
	}
	v := r.Group("user")
	{
		v.POST("add", h.Login)
	}
	return h
}

同时初始化路由

第二步 初始化一个无中间件的gin服务器

middleware/logger.go

package middleware

import (
	"time"

	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
)

// GinLogger 是一个 Gin 中间件,用于记录 HTTP 请求的日志信息
func GinLogger(logger *zap.Logger) gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now() // 记录请求开始时间

		// 获取请求的路径和查询参数
		status := c.Writer.Status()
		method := c.Request.Method
		path := c.Request.URL.Path
		query := c.Request.URL.RawQuery
		clientIP := c.ClientIP()
		userAgent := c.Request.UserAgent()
		date := time.Now().Format("2006-01-02 15:04:05")

		// 处理请求
		c.Next()

		// 计算请求耗时
		duration := time.Since(start)

		// 记录日志
		logger.Info(
			"请求日志",
			zap.Int("status", status),
			zap.String("method", method),
			zap.Duration("duration", duration),
			zap.String("path", path),
			zap.String("query", query),
			zap.String("clientIP", clientIP),
			zap.String("userAgent", userAgent),
			zap.String("date", date),
		)
	}
}

config/initGin.go

package config

import (
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
	"wire_learn/pkg/middleware"
)

func NewGin(logger *zap.Logger) *gin.Engine {
	r := gin.New()
	r.Use(middleware.GinLogger(logger))
    // 使用其他中间件
    ...
	return r
}

还可添加cors,recover等中间件

第三步. 初始化httpServer

type HttpServer struct {
	Engine  *gin.Engine
	UserApi *api.UserHandler
}

func (receiver *HttpServer) Runserver() {
    // 加载env配置

    // casbin配置
    
    ...
	// 启动gin服务端
	receiver.Engine.Run()
}

定义了一个HttpServer的结构体,里面有Engine,UserApi两个字段,目的就是在初始化完gin服务器后,再通过Engine对象去初始化路由,然后通过wire,将前面的一系列的封装成一个initApp函数,返回*HttpServer对象

第四步 生成wire_gen.go

wire.go代码

func InitApp() *config.HttpServer {
	wire.Build(
		wire.Struct(new(config.HttpServer), "*"),
		config.NewGormDB,
		config.NewLogger,
		config.NewGin,
		user.UserProvider,
	)
	return nil
}

wire.Struct(new(config.HttpServer), "*")实际上是初始化HttpServer对象,wire.Build除了构造函数也可以识别这种格式,*代表所有字段

第五步 执行wire

wire

生成代码如下

// Injectors from wire.go:

func InitApp() *config.HttpServer {
	logger := config.NewLogger()
	engine := config.NewGin(logger)
	db := config.NewGormDB()
	userDao := dao.NewUserDao(db)
	userRepo := repo.NewUserRepo(userDao)
	userService := service.NewUserService(userRepo)
	userHandler := api.NewUserHandler(engine, userService)
	httpServer := &config.HttpServer{
		Engine:  engine,
		UserApi: userHandler,
	}
	return httpServer
}

第六步 main.go中调用

package main

func main() {
	app := InitApp()
	app.Runserver()
}

image-20250215163501665

总结

将handler挂载到httpServer结构体当中,如果你新增子模块,只需在httpServer加入对应的handler和build加入对应的provider,就能自动管理路由,而不需要手动分包初始化,是不是非常方便😎😎😎

Wire 是一个强大的依赖注入工具,可以帮助你更好地管理 Go 项目的依赖关系,提高代码的可维护性和可测试性。虽然学习曲线比较陡峭,但对于大型项目来说,使用 Wire 可以带来显著的好处。在使用 Wire 的过程中,需要注意错误处理、循环依赖、单例模式、编译时间等问题。尤其是在与 gin 结合使用时,需要注意 API Handler 和中间件的初始化方式。

还有一点的是小项目可能体现不出来优势,大项目用wire管理是一种非常方便的选择

项目地址

参考资料