在 Go 语言的开发实践中,我们经常需要处理各种依赖关系,例如,一个 service 层可能依赖一个或多个 repository 层。如何优雅地管理这些依赖,是我们在项目开发中需要重点关注的问题。一个好的依赖管理方案,可以显著提高代码的可读性、可维护性和可测试性。

今天,我们就来介绍一个 Go 语言生态中非常受欢迎的轻量级依赖注入库:samber/do。它基于 Go 1.18+ 的泛型特性,实现了一套类型安全的依赖注入工具集。相较于其他依赖注入框架(uber-go/diggoogle/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(注入器): Injectordo 库的核心,它扮演着 DI 容器的角色,负责管理所有服务的生命周期。
  • Provider(提供者): Provider 是一个函数,用于创建和返回一个服务的实例。我们通过 do.Providedo.ProvideNamed 方法,将 Provider 注册到 Injector 中。
  • Invoker(调用者): 当我们需要使用一个服务时,通过 do.Invokedo.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 方法,将 UserRepositoryUserService 的构造函数注册为 Provider。最后,通过 do.MustInvoke 获取 UserService 的实例,并调用其方法。do 会自动处理 UserServiceUserRepository 的依赖关系。

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 还会自动监听 SIGINTSIGTERM 信号,在收到信号时自动调用 Shutdown,非常方便。

总结

samber/do 是一个功能强大且易于使用的 Go 语言依赖注入库。它基于 Go 1.18+ 的泛型特性,提供了类型安全的 API,并且没有任何外部依赖和代码生成,非常轻量。

通过本文的介绍,相信你已经对 samber/do 的核心概念和使用方法有了全面的了解。在你的下一个 Go 项目中,不妨试试用 samber/do 来管理依赖,相信它会给你带来更愉快的开发体验。

如果你想了解更多关于 samber/do 的信息,可以参考它的 GitHub 仓库


扫码关注微信公众号