使用 Google Wire 在 Go 中进行依赖注入
liuian 2025-05-09 20:03 28 浏览
关注点分离、松耦合系统和依赖反转原则等概念在软件工程中是众所周知的,并且在创建良好的计算机程序过程中至关重要。在本文中,我们将讨论一个同时应用了这三个原则的技术,称为依赖注入。我们将尽可能地实践,更加重点地讨论如何在 Go 应用程序中实现依赖注入。但在进一步讨论之前,让我们重新审视一下,究竟什么是依赖注入?
如前所述,依赖注入是一种技术,其关注点在于确保想要使用特定服务的对象或函数不必知道如何构造这样的服务,从而将构造对象和使用对象的关注点分离,导致松耦合的程序。接收对象或函数由外部注入器提供其依赖项,而它并不知道这些依赖项。依赖注入还鼓励使用依赖反转原则,即接收对象只需要声明其使用的服务的接口,而不需要它们的具体实现。
Robert van Gen 在他的一篇文章中指出,虽然依赖注入在小规模上运行良好,但对于具有复杂依赖关系图的更大型应用程序,它将导致大量的初始化代码块。这就是 Wire 等工具非常有用的地方。Wire 是 Go 中用于依赖注入的代码生成器。是的,你猜对了,Wire 将为我们生成必要的初始化代码。我们只需要定义提供程序和注入器。提供程序是普通的 Go 函数,根据它们的依赖项提供值,而注入器是按依赖顺序调用提供程序的函数。为了更好地说明这一点,将呈现一个示例。
设置游乐场
假设我们正在开发一个 HTTP 服务器,为用户注册提供端点。尽管只有一个端点,但它是使用通常出现在更复杂应用程序中的三层设计:存储库、用例和控制器。出于不那么重要的原因,让我们假设它具有以下目录结构,
.
├── go.mod
├── go.sum
├── internal
│ ├── domain
│ │ ├── model
│ │ │ └── user.go
│ │ └── repository
│ │ └── user.go
│ ├── handler
│ │ └── handler.go
│ ├── interface
│ │ └── datastore
│ │ └── user.go
│ └── usecase
│ ├── request
│ │ └── user.go
│ ├── user
│ │ └── user.go
│ └── user.go
└── main.go
现在,让我们在
internal/interface/datastore/user.go 中定义我们的第一个提供程序。在以下代码片段中,New 是一个提供者函数,它以 *sql.DB 为依赖项,并返回 Repository 的具体实现。
// internal/interface/datastore/user.go
package datastore
import (
"context"
"database/sql"
"inject/internal/domain/model"
)
type Repository struct {
db *sql.DB
}
func New(db *sql.DB) *Repository {
return &Repository{db: db}
}
func (r Repository) Create(ctx context.Context, user model.User) error {
// TODO: implement me
return nil
}
这个 Repository 的具体实现将通过抽象或接口由 UseCase 层使用。换句话说,我们的 UseCase 层的提供者函数依赖于接口,而不是 Repository 的具体实现。从技术上讲,这个接口应该由消费层拥有,但是 — 我个人认为 — 这并不意味着它们两者必须位于同一个包中。在我们的示例中,Usecase 的提供者和 Repository 的接口分别在
internal/usecase/user/user.go 和
internal/domain/repository/user.go 中定义。
// internal/usecase/user/user.go
package user
import (
"context"
"inject/internal/domain/repository"
"inject/internal/usecase/request"
)
type Usecase struct {
repository repository.Repository
}
func New(repository repository.Repository) *Usecase {
return &Usecase{repository: repository}
}
func (u Usecase) Create(ctx context.Context, req request.CreateUserRequest) error {
// TODO: implement me
return nil
}
就像之前 Repository 的提供者一样,这里我们 UseCase 的提供者也返回一个具体的实现。
// internal/domain/repository/user.go
package repository
import (
"context"
"inject/internal/domain/model"
)
type Repository interface {
Create(ctx context.Context, user model.User) error
}
最后,具体的 UseCase 实现将被 Controller 使用,同样通过抽象或接口。Controller 的提供程序和 UseCase 的接口在
internal/handler/handler.go 和 internal/usecase/user.go 中定义,如下所示:
// internal/interface/datastore/user.go
package handler
import (
"inject/internal/usecase"
"net/http"
)
type Handler struct {
usecase usecase.Usecase
}
func New(usecase usecase.U
secase) *Handler {
return &Handler{usecase: usecase}
}
func (h Handler) Create() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// TODO: implement me
w.WriteHeader(http.StatusOK)
}
}
// internal/usecase/user.go
package usecase
import (
"context"
"inject/internal/usecase/request"
)
type Usecase interface {
Create(ctx context.Context, req request.CreateUserRequest) error
}
现在,所有必要的提供者都完成了,我们可以在我们的 main.go 中手动执行依赖注入,就像这样:
// main.go
package main
import (
"database/sql"
"log"
"net/http"
"inject/internal/handler"
"inject/internal/interface/datastore"
"inject/internal/usecase/user"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "dataSourceName")
if err != nil {
log.Fatalf("sql.Open: %v", err)
}
repository := datastore.New(db)
usecase := user.New(repository)
handler := handler.New(usecase)
mux := http.NewServeMux()
mux.HandleFunc("POST /users", handler.Create())
log.Fatalf("http.ListenAndServe: %v", http.ListenAndServe(":8000", mux))
}
接下来,如何使用 Wire 生成像上面那样的初始化代码呢?
使用 Wire
通过 Wire,我们打算使我们最终的 main.go 看起来更简化,像这样:
// main.go
package main
import (
"log"
"net/http"
)
func main() {
handler, err := InitializeHandler()
if err != nil {
log.Fatal(err)
}
log.Fatal(http.ListenAndServe(":8000", handler))
}
我们可以开始创建一个文件,通常命名为 wire.go。它可以在一个单独的包中定义,但在这个示例中,我们将其定义在项目的根目录。但在继续创建 wire.go 之前,最好重构我们之前的一些代码,特别是创建数据库连接实例和注册 API 路由的部分。以下新提供者将起到这个目的,
// pkg/mysql/mysql.go
package mysql
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func New() (*sql.DB, error) {
db, err := sql.Open("mysql", "dataSourceName")
if err != nil {
return nil, err
}
return db, nil
}
// internal/handler/route.go
package handler
import "net/http"
func Register(handler *Handler) *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("POST /users", handler.Create())
return mux
}
提供程序函数 Register 上面接受 Handler 的具体实现。当然,也可以使用抽象或接口。但我们将其保留,就像我们让 Repository 的提供程序函数接受类型 *sql.DB 的具体实现一样。这不违反我们之前提到的依赖反转原则。实际上,这可能是一个很好的例子,如果没有立即需要的情况,我们不必在我们的代码中创建抽象。
好的,现在让我们回到我们的 wire.go。根据我们简化的 main.go 文件,你可能已经意识到 InitializeHandler 函数可能是由 Wire 生成的 — 是的,你说对了!要正确生成这样的函数,我们可以将我们的 wire.go 编写如下:
//go:build wireinject
// +build wireinject
package main
import (
"net/http"
"inject/internal/domain/repository"
"inject/internal/handler"
"inject/internal/interface/datastore"
"inject/internal/usecase"
"inject/internal/usecase/user"
"inject/pkg/mysql"
"github.com/google/wire"
)
func InitializeHandler() (*http.ServeMux, error) {
wire.Build(
mysql.New,
datastore.New,
wire.Bind(new(repository.Repository), new(*datastore.Repository)),
user.New,
wire.Bind(new(usecase.Usecase), new(*user.Usecase)),
handler.New,
handler.Register,
)
return &http.ServeMux{}, nil
}
基本上,在 wire.go 中,我们告诉 Wire 关于初始化器函数 InitializeHandler 的模板。它返回 *http.ServeMux 和 error。请注意,(&http.ServeMux{}, nil) 的返回值仅用于满足编译器。为了正确返回所需的值,我们在 Build 函数中声明了所有必要的提供程序:mysql.New、datastore.New、user.New、handler.New 和 handler.Register。
虽然 Wire 足够聪明,可以识别依赖图,但仍然需要明确告诉它某个具体实现满足某个接口。记住,datastore.New 和 user.New 返回类型为 *datastore.Repository 和 *user.Usecase 的具体实现,满足 repository.Repository 和 usecase.Usecase 接口的情况。这两种情况的必要显式声明通过 Bind 函数实现。
请注意,我们需要将 wire.go 从最终的二进制文件中排除。这通过在 wire.go 文件顶部添加一个构建约束来完成。
接下来,我们可以在应用程序的根目录中调用 wire 命令,
wire
如果之前没有安装 Wire,请先运行以下命令,
go install github.com/google/wire/cmd/wire@latest
这个 wire 命令将生成一个名为 wire_gen.go 的文件,其内容是 InitializeHandler 函数的生成代码,如下所示:
// Code generated by Wire. DO NOT EDIT.
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package wire
import (
"inject/internal/handler"
"inject/internal/interface/datastore"
"inject/internal/usecase/user"
"inject/pkg/mysql"
"net/http"
)
// Injectors from wire.go:
//go:generate wire
func InitializeHandler() (*http.ServeMux, error) {
db, err := mysql.New()
if err != nil {
return nil, err
}
repository := datastore.New(db)
usecase := user.New(repository)
handlerHandler := handler.New(usecase)
serveMux := handler.Register(handlerHandler)
return serveMux, nil
}
生成的初始化器代码看起来与我们之前在 main.go 的第一个版本中编写的代码非常相似。
修改依赖项
假设我们想要修改我们的 mysql.New 提供者,以接受一个配置结构,因为我们不想直接在其中硬编码数据源名称 — 这通常被认为是不好的做法。为了实现这一点,我们创建一个特殊的目录来存储应用程序配置文件和一个新的提供者,该提供者读取文件并返回一个配置结构。我们最终的目录结构将如下所示:
.
├── config
│ ├── config.go
│ └── file
│ └── config.json
├── go.mod
├── go.sum
├── internal
│ ├── domain
│ │ ├── model
│ │ │ └── user.go
│ │ └── repository
│ │ └── user.go
│ ├── handler
│ │ ├── handler.go
│ │ └── route.go
│ ├── interface
│ │ └── datastore
│ │ └── user.go
│ └── usecase
│ ├── request
│ │ └── user.go
│ ├── user
│ │ └── user.go
│ └── user.go
├── main.go
├── pkg
│ └── mysql
│ └── mysql.go
├── wire_gen.go
└── wire.go
在 config/config.go 中,我们定义了 Config 结构以及它的提供者,
package config
type Config struct {
DatabaseDSN string
AppPort string
}
func Load() (Config, error) {
// TODO: implement me
return Config{}, nil
}
接下来,我们只需要将这个新提供者添加到我们的 wire.go 文件中。是的,你说得对,将其作为 Build 函数的一部分插入即可,
//go:build wireinject
// +build wireinject
package wire
import (
"net/http"
"inject/config"
"inject/internal/domain/repository"
"inject/internal/handler"
"inject/internal/interface/datastore"
"inject/internal/usecase"
"inject/internal/usecase/user"
"inject/pkg/mysql"
"github.com/google/wire"
)
func InitializeHandler() (*http.ServeMux, error) {
wire.Build(
config.Load,
mysql.New,
datastore.New,
wire.Bind(new(repository.Repository), new(*datastore.Repository)),
user.New,
wire.Bind(new(usecase.Usecase), new(*user.Usecase)),
handler.New,
handler.Register,
)
return &http.ServeMux{}, nil
}
再次运行 wire 命令 — 或者这次我们也可以运行 go generate 命令 — 将告诉 Wire 重新生成初始化代码,结果如下:
// Code generated by Wire. DO NOT EDIT.
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package wire
import (
"inject/config"
"inject/internal/handler"
"inject/internal/interface/datastore"
"inject/internal/usecase/user"
"inject/pkg/mysql"
"net/http"
)
// Injectors from wire.go:
func InitializeHandler() (*http.ServeMux, error) {
configConfig, err := config.Load()
if err != nil {
return nil, err
}
db, err := mysql.New(configConfig)
if err != nil {
return nil, err
}
repository := datastore.New(db)
usecase := user.New(repository)
handlerHandler := handler.New(usecase)
serveMux := handler.Register(handlerHandler)
return serveMux, nil
}
很简单,对吧?
最后的话
我们已经介绍了使用 Wire 的简单示例,演示了它如何帮助我们构建具有依赖注入的初始化代码。但这并不是 Wire 的全部故事。实际上,它仍然有一些其他有用的功能尚未在这里讨论。要充分利用 Wire,可以参考这里的文档。
相关推荐
- python入门到脱坑函数—定义函数_如何定义函数python
-
Python函数定义:从入门到精通一、函数的基本概念函数是组织好的、可重复使用的代码块,用于执行特定任务。在Python中,函数可以提高代码的模块性和重复利用率。二、定义函数的基本语法def函数名(...
- javascript函数的call、apply和bind的原理及作用详解
-
javascript函数的call、apply和bind本质是用来实现继承的,专业点说法就是改变函数体内部this的指向,当一个对象没有某个功能时,就可以用这3个来从有相关功能的对象里借用过来...
- JS中 call()、apply()、bind() 的用法
-
其实是一个很简单的东西,认真看十分钟就从一脸懵B到完全理解!先看明白下面:例1obj.objAge;//17obj.myFun()//小张年龄undefined例2shows(...
- Pandas每日函数学习之apply函数_apply函数python
-
apply函数是Pandas中的一个非常强大的工具,它允许你对DataFrame或Series中的数据应用一个函数,可以是自定义的函数,也可以是内置的函数。apply可以作用于DataF...
- Win10搜索不习惯 换个设定就好了_window10搜索用不了怎么办
-
Windows10的搜索功能是真的方便,这点用惯了Windows10的小伙伴应该都知道,不过它有个小问题,就是Windows10虽然会自动联网搜索,但默认使用微软自家的Bing搜索引擎和Edge...
- 面试秘籍:call、bind、apply的区别,面试官为什么总爱问这三位?
-
引言你有没有发现,每次JavaScript面试,面试官总爱问你call、bind和apply的区别?好像这三个方法成了通关密码,掌握了它们,就能顺利过关。其实不难理解,面试官问这些问题,不...
- 记住这8招,帮你掌握“追拍“摄影技法—摄影早自习第422日
-
杨海英同学提问:请问叶梓老师,我练习追拍时,总也不能把运动的人物拍清晰,速度一般掌握在1/40-1/60,请问您如何把追拍拍的清晰?这跟不同的运动形式有关系吗?请您给讲讲要点,谢谢您!摄影:Damia...
- [Sony] 有点残酷的测试A7RII PK FS7
-
都是好机!手中利器!主要是最近天天研究fs5,想知道fs5与a7rii后期匹配问题,苦等朋友的fs5月底到货,于是先拿手里现有的fs7小测一下,十九八九也能看到fs5的影子,另外也了解一下fs5k标配...
- AndroidStudio_Android使用OkHttp发起Http请求
-
这个okHttp的使用,其实网络上有很多的案例的,但是,如果以前没用过,copy别人的直接用的话,可以发现要么导包导不进来,要么,人家给的代码也不完整,这里自己整理一下.1.引入OkHttp的jar...
- ESL-通过事件控制FreeSWITCH_es事务控制
-
通过事件提供的最底层控制机制,允许我们有效地利用工具箱,适时选择使用其中的单个工具。FreeSWITCH是一个核心交换与混合矩阵,它周围有几十个模块提供各种功能特性。我们完全控制了所有的即时信息,这些...
- 【调试】perf和火焰图_perf生成火焰图
-
简介perf是linux上的性能分析工具,perf可以对event进行统计得到event的发生次数,或者对event进行采样,得到每次event发生时的相关数据(cpu、进程id、运行栈等),利用这些...
- 文本检索控件也玩安卓?dtSearch Engine发布Android测试版
-
dtSearchEngineforLinux(原生64-bit/32-bitC++和JavaAPIs)和dtSearchEngineforWin&.NET(原生64-bi...
- 网站后台莫名增加N个管理员,记一次SQL注入攻击
-
网站没流量,但却经常被SQL注入光顾。最近,网站真的很奇怪,网站后台不光莫名多了很多“管理员”,所有的Wordpres插件还会被自动暂停,导致一些插件支持的页面,如WooCommerce无法正常访问、...
- 多元回归树分析Multivariate Regression Trees,MRT
-
多元回归树(MultivariateRegressionTrees,MRT)是单元回归树的拓展,是一种对一系列连续型变量递归划分成多个类群的聚类方法,是在决策树(decision-trees)基础...
- JMETER性能测试_JMETER性能测试指标
-
jmeter为性能测试提供了一下特色:jmeter可以对测试静态资源(例如js、html等)以及动态资源(例如php、jsp、ajax等等)进行性能测试jmeter可以挖掘出系统最大能处...
- 一周热门
-
-
【验证码逆向专栏】vaptcha 手势验证码逆向分析
-
Python实现人事自动打卡,再也不会被批评
-
Psutil + Flask + Pyecharts + Bootstrap 开发动态可视化系统监控
-
一个解决支持HTML/CSS/JS网页转PDF(高质量)的终极解决方案
-
再见Swagger UI 国人开源了一款超好用的 API 文档生成框架,真香
-
网页转成pdf文件的经验分享 网页转成pdf文件的经验分享怎么弄
-
C++ std::vector 简介
-
飞牛OS入门安装遇到问题,如何解决?
-
系统C盘清理:微信PC端文件清理,扩大C盘可用空间步骤
-
10款高性能NAS丨双十一必看,轻松搞定虚拟机、Docker、软路由
-
- 最近发表
-
- python入门到脱坑函数—定义函数_如何定义函数python
- javascript函数的call、apply和bind的原理及作用详解
- JS中 call()、apply()、bind() 的用法
- Pandas每日函数学习之apply函数_apply函数python
- Win10搜索不习惯 换个设定就好了_window10搜索用不了怎么办
- 面试秘籍:call、bind、apply的区别,面试官为什么总爱问这三位?
- 记住这8招,帮你掌握“追拍“摄影技法—摄影早自习第422日
- [Sony] 有点残酷的测试A7RII PK FS7
- AndroidStudio_Android使用OkHttp发起Http请求
- ESL-通过事件控制FreeSWITCH_es事务控制
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)
- mysql刷新权限 (34)