golang专注于简洁API和灵活性的DI容器插件库HnH/di的使用

Golang 专注于简洁 API 和灵活性的 DI 容器插件库 HnH/di 的使用

CircleCI codecov Go Report Card GoDoc

依赖注入

DI 是一个专注于简洁 API 和灵活性的依赖注入库。DI 有两种顶级抽象:Container 和 Resolver。 第一个负责接受构造函数和实现,并从中创建抽象绑定。 第二个针对一个或多个容器实现不同的实现解析场景。

最初这个库受到 GoLobby Container 的很大启发,但从那时起在结构、功能和 API 上有很多不兼容的更改。

要安装 DI,只需在项目目录中运行:

go get github.com/HnH/di

容器

type Container interface {
    Singleton(constructor any, opts ...Option) error
    Factory(constructor any, opts ...Option) error
    Implementation(implementation any, opts ...Option) error
    ListBindings(reflect.Type) (map[string]Binding, error)
    Reset()
}

Singleton

Singleton() 方法需要一个构造函数,该构造函数将返回抽象的实现。构造函数将被调用一次,返回的实现将始终绑定到抽象。

err = di.Singleton(func() (Abstraction, SecondAbstraction) {
    return Implementation, SecondImplementation
})

// Singleton 也可以接受命名选项,意味着返回的实现将仅在提供的名称下可用
err = di.Singleton(func() (Abstraction) {
    return Implementation
}, di.WithName("customName"))

// 如果返回多个实现,可以为每个实现提供名称
err = di.Singleton(func() (Abstraction, SecondAbstraction) {
    return Implementation, SecondImplementation
}, di.WithName("customName", "secondCustomName"))

// 如果只返回一个实现,可以为其提供多个别名
err = di.Singleton(func() (Abstraction) {
    return Implementation
}, di.WithName("customName", "secondCustomName"))

// WithFill() 选项在实例创建后立即调用 `resolver.Fill()`
err = di.Singleton(func() (Abstraction) {
    return Implementation
}, di.WithFill()) // di.resolver.Fill(Implementation) 将在底层调用

Factory

Factory() 方法需要一个构造函数,该构造函数将返回一个抽象的一个实现。每次抽象解析请求时都会调用构造函数。

err = di.Factory(func() (Abstraction) {
    return Implementation
})

// Factory 也可以选择性地接受命名选项
err := di.Factory(func() (Abstraction) {
    return Implementation
}, di.WithName("customName"))

// 类似于 Singleton 绑定,可以提供 WithFill() 选项
err = di.Factory(func() (Abstraction) {
    return Implementation
}, di.WithFill()) // di.resolver.Fill(Implementation) 将在底层调用

Implementation

Implementation() 接收准备好的实例并将其绑定到其真实类型,这意味着声明的抽象变量类型(接口)被忽略。

var circle Shape = newCircle()
err = di.Implementation(circle)

// 将返回错误 di: no binding found for di_test.Shape
var a Shape
err = di.Resolve(&a)

// 将解析 circle
var c *Circle
err = di.Resolve(&a)

// 也可以使用命名选项
err = di.Implementation(circle, di.WithName("customName"))
err = di.Resolve(&c, di.WithName("customName"))

解析器

type Resolver interface {
    With(implementations ...any) Resolver
    Resolve(receiver any, opts ...Option) error
    Call(function any, opts ...Option) error
    Fill(receiver any) error
}

With

With() 接受实例化实现列表,并尝试在解析场景中使用它们。

var circle Shape = newCircle()
err = di.Implementation(circle)

// di: no binding found for di_test.Shape
di.Call(func(s Shape) { return })

// ok
di.With(circle).Call(func(s Shape) { return }))

Resolve

Resolve() 需要一个抽象接收器(指针)并用适当的实现填充它。

var abs Abstraction
err = di.Resolve(&a)

// 也可以使用之前注册的名称进行解析
err = di.Resolve(&a, di.WithName("customName"))

Call

Call() 执行带有解析实现作为参数的函数。

err = di.Call(func(a Abstraction) {
    // `a` 将是 Abstraction 的实现
})

// 返回值可以通过提供选项绑定到变量
var db Database
err = di.Call(func(a Abstraction) Database {
    return &MySQL{a}
}, di.WithReturn(&db))
// db == &MySQL{a}

Fill

Fill() 方法接受一个结构体(指针)并解析其字段。

err = di.Singleton(func() Mailer { return &someMailer{} })

err = di.Singleton(func() (Database, Database) {
    return &MySQL{}, &Redis{} 
}, di.WithName("data", "cache"))

type App struct {
    mailer  Mailer     `di:"type"` // 按字段类型填充 (Mailer)
    data    Database   `di:"name"` // 按字段类型填充 (Mailer) 并要求绑定名称为字段名 (data)
    cache   Database   `di:"name"`
    inner   struct {
        cache Database `di:"name"`	
    } `di:"recursive"`             // 指示 DI 递归填充结构体
    another struct {
        cache Database `di:"name"` // 只要 App 结构体中的外部字段没有 `di:"recursive"` 标签就不会有任何影响
    }
}

var App = App{}
err = container.Fill(&myApp)

// [Typed Bindings]
// `App.mailer` 将是 Mailer 接口的实现

// [Named Bindings]
// `App.data` 将是 Database 接口的 MySQL 实现
// `App.cache` 将是 Database 接口的 Redis 实现
// `App.inner.cache` 将是 Database 接口的 Redis 实现

// `App.another` 将被忽略,因为它没有 `di` 标签

注意,默认情况下,如果无法解析任何结构体字段,Fill() 方法会返回错误。如果其中一个字段是可选的,应在 di 标签中添加 omitempty 后缀。

type App struct {
    mailer  Mailer `di:"type,omitempty"` // 如果未提供 Mailer,Fill 不会返回错误
}

或者,可以提供 map[string]Type 或 []Type。它将填充所有可用的提供的类型的实现。

var list []Shape
container.Fill(&list)

// []Shape{&Rectangle{}, &Circle{}}

var list map[string]Shape
container.Fill(&list)

// map[string]Shape{"square": &Rectangle{}, "rounded": &Circle{}} 

提供者

提供者是为容器提供某些东西的实体的抽象。

type Provider interface {
    Provide(Container) error
}

构造函数

构造函数实现了 Construct() 方法,该方法在绑定到容器时(单例情况下)或在调用工厂方法后被调用。 注意,在可以调用构造函数方法之前,必须在容器中提供 context.Context

type Constructor interface {
    Construct(context.Context) error
}

上下文传播

通过 di.Context 抽象可以进行上下文传播。快速示例:

var container = di.NewContainer()
container.Implementation(newCircle())

var (
    ctx = di.Ctx(context.Background).Put(container)
    shp Shape
)

err = ctx.Resolver().Resolve(&shp) // err == nil

更多关于golang专注于简洁API和灵活性的DI容器插件库HnH/di的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang专注于简洁API和灵活性的DI容器插件库HnH/di的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


HnH/di - Go语言简洁灵活的DI容器库

HnH/di 是一个专注于简洁API和灵活性的Go语言依赖注入(DI)容器库。它提供了轻量级的依赖注入解决方案,适合中小型Go项目使用。

主要特性

  1. 简洁的API设计:提供直观易用的接口
  2. 灵活的依赖解析:支持多种依赖注册和解析方式
  3. 生命周期管理:支持单例和临时实例
  4. 轻量级:无额外依赖,核心代码精简

基本用法

安装

go get github.com/hnhcodes/di

简单示例

package main

import (
	"fmt"
	"github.com/hnhcodes/di"
)

type Database struct {
	ConnectionString string
}

type Service struct {
	DB *Database `di:"inject"`
}

func main() {
	// 创建容器
	container := di.New()
	
	// 注册依赖
	container.Provide(&Database{
		ConnectionString: "localhost:5432",
	})
	
	// 解析依赖
	var service Service
	if err := container.Resolve(&service); err != nil {
		panic(err)
	}
	
	fmt.Printf("Service DB connection: %s\n", service.DB.ConnectionString)
	// 输出: Service DB connection: localhost:5432
}

核心功能详解

1. 依赖注册

HnH/di 提供多种依赖注册方式:

// 方式1: 直接提供实例
container.Provide(&Database{ConnectionString: "dsn"})

// 方式2: 提供构造函数
container.Provide(func() (*Database, error) {
	return &Database{ConnectionString: "dsn"}, nil
})

// 方式3: 带名称的注册
container.ProvideNamed("db", &Database{ConnectionString: "dsn"})

2. 依赖解析

// 方式1: 通过结构体标签注入
type App struct {
	DB *Database `di:"inject"`
}

var app App
container.Resolve(&app)

// 方式2: 直接获取实例
db, err := container.Get((*Database)(nil))

// 方式3: 按名称获取
db, err := container.GetByName("db")

3. 生命周期管理

// 默认是单例模式
container.Provide(&Database{}) // 单例

// 临时实例(每次解析都新建)
container.Provide(func() (*Database, error) {
	return &Database{}, nil
}, di.WithTransient())

4. 接口绑定

type DB interface {
	Query(string) error
}

type Postgres struct{}

func (p *Postgres) Query(q string) error { return nil }

// 注册实现
container.Provide(&Postgres{})

// 绑定接口
container.Bind((*DB)(nil), (*Postgres)(nil))

// 使用接口
var db DB
container.Resolve(&db)

高级用法

条件注入

type Config struct {
	Env string
}

type DevService struct{}
type ProdService struct{}

container.Provide(&Config{Env: "dev"})
container.Provide(&DevService{}, di.When(
	func(c *Config) bool { return c.Env == "dev" },
))
container.Provide(&ProdService{}, di.When(
	func(c *Config) bool { return c.Env == "prod" },
))

循环依赖检测

HnH/di 会自动检测循环依赖并返回错误:

type A struct { B *B `di:"inject"` }
type B struct { A *A `di:"inject"` }

var a A
err := container.Resolve(&a)
// 会返回循环依赖错误

最佳实践

  1. 在应用启动时集中注册依赖
  2. 优先使用接口绑定
  3. 合理使用单例和临时实例
  4. 避免过度使用DI,保持代码可读性

与其它DI库对比

相比更复杂的DI库如uber/dig或facebook/inject,HnH/di的优势在于:

  1. 更简单的API
  2. 更少的魔法
  3. 更直观的错误处理
  4. 更轻量的实现

HnH/di非常适合需要轻量级DI解决方案的项目,特别是那些不希望引入复杂依赖管理的中小型Go应用。

回到顶部