基于泛型的轻量级依赖注入工具 do
承蒙大家厚爱,我的《Go语言之路》的纸质版图书已经上架京东,有需要的朋友请点击 此链接 购买。
在 Go 语言的开发实践中,我们经常需要处理各种依赖关系,例如,一个 service
层可能依赖一个或多个 repository
层。如何优雅地管理这些依赖,是我们在项目开发中需要重点关注的问题。一个好的依赖管理方案,可以显著提高代码的可读性、可维护性和可测试性。
今天,我们就来介绍一个 Go 语言生态中非常受欢迎的轻量级依赖注入库:samber/do
。它基于 Go 1.18+ 的泛型特性,实现了一套类型安全的依赖注入工具集。相较于其他依赖注入框架(uber-go/dig、google/wire),samber/do
具有轻量、无代码生成、无外部依赖等诸多优点,非常适合在中小型项目中使用。
本文将带你全面了解 samber/do
的核心概念和使用方法,并通过一个简单的 Web 应用示例,展示如何在实际项目中应用 samber/do
。
什么是依赖注入?
在介绍 samber/do
之前,我们先来回顾一下什么是依赖注入(Dependency Injection,简称 DI)。
依赖注入是一种软件设计模式,它的核心思想是:一个对象不应该自己创建它所依赖的对象,而应该由外部的某个实体(我们称之为“注入器”或“容器”)来创建并“注入”给它。
这样做的好处是显而易见的:
- 解耦:对象与其依赖项之间的耦合度大大降低。我们可以在不修改对象本身代码的情况下,替换其依赖的实现。
- 提高可测试性:在单元测试中,我们可以轻松地注入
mock
对象,从而实现对业务逻辑的独立测试。 - 代码更清晰:依赖关系的管理被集中到了一个地方,使得代码的组织结构更加清晰。
在 Go 语言中,我们通常通过接口(interface
)来实现依赖注入。但是,当项目变得复杂,依赖关系网变得错综复杂时,手动管理依赖注入就会变得非常繁琐。这时,一个好用的 DI 框架就能派上用场了。
samber/do
快速入门
samber/do
是一个纯 Go 实现的极简依赖注入库,它的 API 非常简单,几乎没有学习成本。它的核心思想是用工厂函数注册依赖,按需获取实例,自动处理依赖关系和生命周期。
下面我们通过一个简单的例子来快速上手。
1. 安装
首先,通过 go get
命令安装 samber/do
:
go get github.com/samber/do
2. 核心概念
samber/do
的核心概念主要有三个:
Injector
(注入器):Injector
是do
库的核心,它扮演着 DI 容器的角色,负责管理所有服务的生命周期。Provider
(提供者):Provider
是一个函数,用于创建和返回一个服务的实例。我们通过do.Provide
或do.ProvideNamed
方法,将Provider
注册到Injector
中。Invoker
(调用者): 当我们需要使用一个服务时,通过do.Invoke
或do.MustInvoke
方法从Injector
中获取该服务的实例。
3. 一个简单的例子
下面,我们来看一个简单的例子,演示如何使用 samber/do
来管理依赖。
假设我们有一个 UserService
,它依赖一个 UserRepository
来获取用户信息。
user_repository.go
package main
import "fmt"
type UserRepository struct{}
func NewUserRepository() *UserRepository {
return &UserRepository{}
}
func (r *UserRepository) GetUser(id int) string {
return fmt.Sprintf("User %d", id)
}
user_service.go
package main
import "github.com/samber/do"
type UserService struct {
repo *UserRepository
}
func NewUserService(i *do.Injector) (*UserService, error) {
repo := do.MustInvoke[*UserRepository](i)
return &UserService{repo: repo}, nil
}
func (s *UserService) GetUserName(id int) string {
return s.repo.GetUser(id)
}
main.go
package main
import (
"fmt"
"github.com/samber/do"
)
func main() {
// 1. 创建一个新的注入器
injector := do.New()
// 2. 注册服务
// 注册 UserRepository
do.Provide(injector, func(i *do.Injector) (*UserRepository, error) {
return NewUserRepository(), nil
})
// 注册 UserService
do.Provide(injector, NewUserService)
// 3. 获取并使用服务
userService := do.MustInvoke[*UserService](injector)
userName := userService.GetUserName(42)
fmt.Println(userName) // 输出: User 42
// 4. 关闭注入器,释放所有服务
injector.Shutdown()
}
在这个例子中,我们首先创建了一个 Injector
。然后,通过 do.Provide
方法,将 UserRepository
和 UserService
的构造函数注册为 Provider
。最后,通过 do.MustInvoke
获取 UserService
的实例,并调用其方法。do
会自动处理 UserService
对 UserRepository
的依赖关系。
samber/do
的进阶用法
除了基本的服务注册和获取,samber/do
还提供了许多实用的功能,以满足更复杂的应用场景。
1. 命名服务依赖
有时,我们可能需要为同一个类型注册多个不同的实例。例如,我们可能需要连接两个不同的数据库。这时,就可以使用“命名服务”来区分它们。
package main
import (
"database/sql"
"fmt"
"log"
"github.com/samber/do"
_ "github.com/mattn/go-sqlite3"
)
func main() {
injector := do.New()
// 注册名为 "db1" 的数据库连接
do.ProvideNamed(injector, "db1", func(i *do.Injector) (*sql.DB, error) {
db, err := sql.Open("sqlite3", "./db1.sqlite")
if err != nil {
log.Fatal(err)
}
return db, nil
})
// 注册名为 "db2" 的数据库连接
do.ProvideNamed(injector, "db2", func(i *do.Injector) (*sql.DB, error) {
db, err := sql.Open("sqlite3", "./db2.sqlite")
if err != nil {
log.Fatal(err)
}
return db, nil
})
// 获取名为 "db1" 的数据库连接
db1 := do.MustInvokeNamed[*sql.DB](injector, "db1")
fmt.Printf("DB1: %+v\n", db1)
// 获取名为 "db2" 的数据库连接
db2 := do.MustInvokeNamed[*sql.DB](injector, "db2")
fmt.Printf("DB2: %+v\n", db2)
injector.Shutdown()
}
2. 服务的生命周期
samber/do
支持两种服务生命周期:
- Singleton(单例): 这是默认的模式。服务在第一次被
Invoke
时创建,之后每次Invoke
都会返回同一个实例。 - Transient(瞬态): 服务在每次被
Invoke
时都会创建一个新的实例。
我们可以通过 do.ProvideTransient
来注册一个瞬态服务。
package main
import (
"fmt"
"math/rand"
"github.com/samber/do"
)
type RandomService struct {
value int
}
func main() {
injector := do.New()
// 注册一个瞬态的 RandomService
do.ProvideTransient(injector, func(i *do.Injector) (*RandomService, error) {
return &RandomService{value: rand.Int()}, nil
})
// 多次获取 RandomService
s1 := do.MustInvoke[*RandomService](injector)
s2 := do.MustInvoke[*RandomService](injector)
fmt.Printf("s1.value: %d\n", s1.value)
fmt.Printf("s2.value: %d\n", s2.value)
fmt.Printf("s1 == s2: %v\n", s1 == s2) // 输出: s1 == s2: false
}
3. 服务的健康检查
在生产环境中,我们经常需要监控服务的健康状态。samber/do
提供了优雅的服务健康检查机制。我们只需要让服务实现 do.Healthcheckable
接口即可。
package main
import (
"context"
"fmt"
"github.com/samber/do"
)
type DatabaseService struct{}
func (s *DatabaseService) HealthCheck() error {
// 在这里实现数据库连接的健康检查逻辑
fmt.Println("Checking database connection...")
return nil // 返回 nil 表示健康
}
func main() {
injector := do.New()
do.Provide(injector, func(i *do.Injector) (*DatabaseService, error) {
return &DatabaseService{}, nil
})
// 触发所有实现了 Healthcheckable 接口的服务的健康检查
health, err := injector.HealthCheck()
if err != nil {
fmt.Printf("Health check failed: %v\n", err)
} else {
fmt.Println("All services are healthy!")
}
for serviceName, serviceErr := range health {
if serviceErr != nil {
fmt.Printf("Service %s is unhealthy: %v\n", serviceName, serviceErr)
} else {
fmt.Printf("Service %s is healthy\n", serviceName)
}
}
}
4. 优雅关闭
当我们的应用需要关闭时,我们希望能够优雅地释放所有资源。samber/do
同样提供了优雅关闭的支持。我们只需要让服务实现 do.Shutdownable
接口。
package main
import (
"context"
"fmt"
"github.com/samber/do"
)
type WorkerService struct{}
func (s *WorkerService) Shutdown() error {
fmt.Println("Shutting down worker service...")
// 在这里实现资源释放的逻辑
return nil
}
func main() {
injector := do.New()
do.Provide(injector, func(i *do.Injector) (*WorkerService, error) {
return &WorkerService{}, nil
})
// ... 应用运行 ...
// 关闭注入器,会以注册的逆序调用所有实现了 Shutdownable 接口的服务的 Shutdown 方法
injector.Shutdown()
}
samber/do
还会自动监听 SIGINT
和 SIGTERM
信号,在收到信号时自动调用 Shutdown
,非常方便。
总结
samber/do
是一个功能强大且易于使用的 Go 语言依赖注入库。它基于 Go 1.18+ 的泛型特性,提供了类型安全的 API,并且没有任何外部依赖和代码生成,非常轻量。
通过本文的介绍,相信你已经对 samber/do
的核心概念和使用方法有了全面的了解。在你的下一个 Go 项目中,不妨试试用 samber/do
来管理依赖,相信它会给你带来更愉快的开发体验。
如果你想了解更多关于 samber/do
的信息,可以参考它的 GitHub 仓库。