Go语言之Wire依赖注入

一、为什么需要依赖注入?

在 Go 项目中,我们常常会遇到这样的依赖关系:

  • Handler 依赖 Service
  • Service 依赖 DAO
  • DAO 依赖 数据库连接

如果手动初始化,会出现类似这样的代码:

1
2
3
4
5
6
7
8
func main() {
db := initDB()
userDAO := NewUserDAO(db)
userService := NewUserService(userDAO)
userHandler := NewUserHandler(userService)

http.ListenAndServe(":8080", userHandler)
}

看似简单,但随着依赖层级变多,维护和修改变得繁琐。
依赖注入(DI) 的思想就是:让依赖关系的初始化逻辑交由框架或工具自动生成,从而保持代码简洁、解耦。

Google 提供的 wire 就是 Go 中常用的 编译时依赖注入工具


二、Wire 的核心思想

wire 不是一个运行时框架,它会在编译时生成依赖注入的代码,生成后再由我们项目编译。
流程如下:

  1. 定义各层的 构造函数(Provider)
  2. wire.go 中声明依赖关系
  3. 执行 wire 命令生成 wire_gen.go
  4. 使用生成的代码运行项目

这样,我们就不需要手写繁琐的依赖初始化逻辑。


三、示例项目结构

我们以一个用户服务(User Service)为例,项目结构如下:

1
2
3
4
5
6
7
8
9
10
.
├── dao
│ └── user_dao.go
├── service
│ └── user_service.go
├── handler
│ └── user_handler.go
├── main.go
├── wire.go
└── go.mod

四、编写各层代码

1. DAO 层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// dao/user_dao.go
package dao

import "database/sql"

type UserDAO struct {
db *sql.DB
}

func NewUserDAO(db *sql.DB) *UserDAO {
return &UserDAO{db: db}
}

func (d *UserDAO) GetUser(id int) string {
// 模拟从数据库获取用户
return "User#" + string(rune(id))
}

2. Service 层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// service/user_service.go
package service

import "myapp/dao"

type UserService struct {
dao *dao.UserDAO
}

func NewUserService(dao *dao.UserDAO) *UserService {
return &UserService{dao: dao}
}

func (s *UserService) GetUserName(id int) string {
return s.dao.GetUser(id)
}

3. Handler 层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// handler/user_handler.go
package handler

import (
"fmt"
"myapp/service"
"net/http"
)

type UserHandler struct {
service *service.UserService
}

func NewUserHandler(service *service.UserService) *UserHandler {
return &UserHandler{service: service}
}

func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
name := h.service.GetUserName(1)
fmt.Fprintf(w, "Hello, %s", name)
}

五、使用 Wire 管理依赖

1. 安装 wire

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

2. 创建 wire.go

在项目根目录新建 wire.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//go:build wireinject
// +build wireinject

package main

import (
"database/sql"
"myapp/dao"
"myapp/handler"
"myapp/service"

"github.com/google/wire"
)

func initDB() *sql.DB {
// 这里简单模拟返回一个 *sql.DB
return &sql.DB{}
}

// 初始化整个应用依赖
func InitApp() *handler.UserHandler {
wire.Build(
initDB,
dao.NewUserDAO,
service.NewUserService,
handler.NewUserHandler,
)
return &handler.UserHandler{}
}

注意:

  • //go:build wireinject + // +build wireinject 用于标记此文件只在 wire 生成代码时使用,编译时会被忽略。
  • wire.Build 声明了依赖关系,Wire 会自动推导构造函数的调用顺序。

3. 生成 wire_gen.go

运行:

1
wire

会生成 wire_gen.go

1
2
3
4
5
6
7
8
9
// Code generated by Wire. DO NOT EDIT.

func InitApp() *handler.UserHandler {
db := initDB()
userDAO := dao.NewUserDAO(db)
userService := service.NewUserService(userDAO)
userHandler := handler.NewUserHandler(userService)
return userHandler
}

六、在 main.go 中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// main.go
package main

import (
"log"
"net/http"
)

func main() {
handler := InitApp()

log.Println("Server running on :8080")
http.ListenAndServe(":8080", handler)
}

到这里,一个完整的 Handler + Service + DAO 依赖关系,就通过 Wire 自动管理了。


七、最佳实践

  1. 保持 Provider 函数简单

    • 每个 Provider 只做一件事:返回依赖对象。
    • 不要把复杂逻辑塞进 Provider。
  2. 分组管理 ProviderSet
    可以为每层定义一个 wire.ProviderSet,保持清晰。

    1
    2
    3
    var DaoSet = wire.NewSet(NewUserDAO)
    var ServiceSet = wire.NewSet(NewUserService)
    var HandlerSet = wire.NewSet(NewUserHandler)

    然后在 wire.go 中引入:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func InitApp() *handler.UserHandler {
    wire.Build(
    initDB,
    dao.DaoSet,
    service.ServiceSet,
    handler.HandlerSet,
    )
    return &handler.UserHandler{}
    }
  3. 避免全局变量

    • 不要使用全局 db、全局 service,让依赖通过注入管理。
  4. 结合接口提高可测试性

    • Service 层依赖 DAO 接口,而不是具体实现,这样单测可以用 mock 替代。
    1
    2
    3
    4
    5
    6
    7
    type UserDAOInterface interface {
    GetUser(id int) string
    }

    type UserService struct {
    dao UserDAOInterface
    }
  5. 只在应用根目录使用 Wire

    • Wire 适合管理项目整体依赖,不要在子包随意生成 wire_gen.go,否则维护困难。

八、总结

通过 wire,我们实现了:

  • 自动化依赖管理(不用手写依赖链条)
  • 分层解耦(Handler、Service、DAO 清晰分工)
  • 更易测试(通过接口 + 依赖注入替换实现)

Wire 的最大优势在于 编译期生成代码,无运行时开销,非常适合 Go 的项目架构。