golang Spring风格依赖注入容器插件goioc/di的使用

Golang Spring风格依赖注入容器插件goioc/di的使用

为什么要在Go中使用DI?为什么需要IoC?

我已经通过Spring Framework在Java中使用依赖注入近10年了。虽然不是说没有它就无法生存,但它已被证明对大型企业级应用程序非常有用。你可能会说Go遵循完全不同的意识形态,重视与Java不同的原则和范式,在这个更好的世界中不需要DI。我甚至可以部分同意这一点。然而,我决定为Go创建这个轻量级的类似Spring的库。毕竟,你可以选择不使用它🙂

这是Go中唯一的DI库吗?

不,当然不是。周围有一堆服务于类似目的的库(我甚至从其中一些库中获得了灵感)。问题是我在所有这些库中都缺少一些东西…因此我决定创建一个统治它们的又一个IoC容器。欢迎你使用任何其他库,例如这个不错的项目。不过,我还是推荐你在这里停一下😉

那么,它是如何工作的?

展示比描述更好。看看这个玩具示例(为了最小化代码片段,省略了错误处理):

services/weather_service.go

package services

import (
	"io"
	"net/http"
)

type WeatherService struct {
}

func (ws *WeatherService) Weather(city string) (*string, error) {
	response, err := http.Get("https://wttr.in/" + city)
	if err != nil {
		return nil, err
	}
	all, err := io.ReadAll(response.Body)
	if err != nil {
		return nil, err
	}
	weather := string(all)
	return &weather, nil
}

controllers/weather_controller.go

package controllers

import (
	"di-demo/services"
	"github.com/goioc/di"
	"net/http"
)

type WeatherController struct {
	// 注意注入甚至适用于未导出的字段
	weatherService *services.WeatherService `di.inject:"weatherService"`
}

func (wc *WeatherController) Weather(w http.ResponseWriter, r *http.Request) {
	weather, _ := wc.weatherService.Weather(r.URL.Query().Get("city"))
	_, _ = w.Write([]byte(*weather))
}

init.go

package main

import (
	"di-demo/controllers"
	"di-demo/services"
	"github.com/goioc/di"
	"reflect"
)

func init() {
	_, _ = di.RegisterBean("weatherService", reflect.TypeOf((*services.WeatherService)(nil)))
	_, _ = di.RegisterBean("weatherController", reflect.TypeOf((*controllers.WeatherController)(nil)))
	_ = di.InitializeContainer()
}

main.go

package main

import (
	"di-demo/controllers"
	"github.com/goioc/di"
	"net/http"
)

func main() {
	http.HandleFunc("/weather", func(w http.ResponseWriter, r *http.Request) {
		di.GetInstance("weatherController").(*controllers.WeatherController).Weather(w, r)
	})
	_ = http.ListenAndServe(":8080", nil)
}

如果你运行它,你应该能够在http://localhost:8080/weather?city=London(或任何其他城市)看到一个整洁的天气预报。

当然,对于这样一个简单的例子,它可能看起来有点小题大做。但对于具有许多相互连接的服务和复杂业务逻辑的大型项目,它可以真正简化你的生活!

看起来不错…给我一些细节!

该库的主要组件是包含和管理你的结构实例(称为"beans")的控制反转容器。

Bean的类型

  • Singleton。在容器中只存在一个副本。每次你从容器中检索实例(或每次它被注入到另一个bean中)时,它都是同一个实例。
  • Prototype。它可以存在于多个副本中:每次从容器中检索(或注入到另一个bean中)时都会创建一个新副本。
  • Request。类似于Prototype,但由于其生命周期与Web请求绑定,因此有一些差异和特性:
    • 不能注入到其他bean中。
    • 不能手动从容器中检索。
    • Request beans会自动注入到相应http.Request的context.Context中。
    • 如果Request bean实现了io.Closer,它将在相应请求取消时被"关闭"。

Bean的注册

为了让容器知道beans,必须手动注册它们(与Java不同,不幸的是,我们不能扫描类路径来自动完成,因为Go运行时不包含关于类型的高级信息)。如何在容器中注册beans?

  • 按类型。这在上面的例子中已经描述过。一个结构体声明有一个带有di.scope:"<scope>"标记的字段。这个字段甚至可以省略 - 在这种情况下,默认范围将是Singleton。然后像这样完成注册:
di.RegisterBean("beanID", reflect.TypeOf((*YourAwesomeStructure)(nil)))
  • 使用预先创建的实例。如果你已经有一个想要注册为bean的实例?你可以这样做:
di.RegisterBeanInstance("beanID", yourAwesomeInstance)

对于这种类型的beans,唯一支持的范围是Singleton,因为我不敢克隆你的实例来启用原型😅

  • 通过bean工厂。如果你有一个为你生成实例的方法,你可以将其注册为bean工厂:
di.RegisterBeanFactory("beanID", Singleton, func(context.Context) (interface{}, error) {
    return "My awesome string that is going to become a bean!", nil
})

对于这种方法,你可以使用任何范围。顺便说一句,你甚至可以在工厂中查找其他beans:

di.RegisterBeanFactory("beanID", Prototype, func(context.Context) (interface{}, error) {
    return di.GetInstance("someOtherBeanID"), nil
})

请注意,工厂方法接受context.Context。这对于请求范围的beans可能很有用(在这种情况下设置了HTTP请求上下文)。对于所有其他beans,它将是context.Background()。

Beans初始化

有一个特殊的接口InitializingBean可以实现,为你的bean提供一些初始化逻辑,这些逻辑将在容器初始化后(对于Singleton beans)或在Prototype/Request实例创建后执行。同样,你也可以在初始化期间查找其他beans(因为此时容器已准备就绪):

type PostConstructBean1 struct {
	Value string
}

func (pcb *PostConstructBean1) PostConstruct() error {
	pcb.Value = "some content"
	return nil
}

type PostConstructBean2 struct {
	Scope              Scope `di.scope:"prototype"`
	PostConstructBean1 *PostConstructBean1
}

func (pcb *PostConstructBean2) PostConstruct() error {
	instance, err := di.GetInstanceSafe("postConstructBean1")
	if err != nil {
		return err
	}
	pcb.PostConstructBean1 = instance.(*PostConstructBean1)
	return nil
}

Beans后处理器

初始化beans的另一种方法是使用所谓的"beans后处理器"。看这个例子:

type postprocessedBean struct {
	a string
	b string
}

_, _ := RegisterBean("postprocessedBean", reflect.TypeOf((*postprocessedBean)(nil)))

_ = RegisterBeanPostprocessor(reflect.TypeOf((*postprocessedBean)(nil)), func(instance interface{}) error {
    instance.(*postprocessedBean).a = "Hello, "
    return nil
})

_ = RegisterBeanPostprocessor(reflect.TypeOf((*postprocessedBean)(nil)), func(instance interface{}) error {
instance.(*postprocessedBean).b = "world!"
    return nil
})

_ = InitializeContainer()

instance := GetInstance("postprocessedBean")

postprocessedBean := instance.(*postprocessedBean)
println(postprocessedBean.a+postprocessedBean.b) // 打印出 "Hello, world!"

Beans注入

如上所述,一个bean可以通过PostConstruct方法注入到另一个bean中。然而,更便捷的方法是使用特殊标记:

type SingletonBean struct {
	SomeOtherBean *SomeOtherBean `di.inject:"someOtherBean"`
}

…或者通过接口…

type SingletonBean struct {
	SomeOtherBean SomeOtherBeansInterface `di.inject:"someOtherBean"`
}

注意,你可以通过指针或接口引用依赖项,但不能通过值引用。提醒一下:你不能注入Request beans。

有时我们可能希望有可选的依赖项。默认情况下,所有声明的依赖项都被认为是必需的:如果在容器中找不到某些依赖项,你会得到一个错误。但是,你可以像这样指定一个可选的依赖项:

type SingletonBean struct {
	SomeOtherBean *string `di.inject:"someOtherBean" di.optional:"true"`
}

在这种情况下,如果在容器中找不到someOtherBean,你将得到nil注入到这个字段中。

事实上,你不需要bean ID来执行注入!看看这个:

type SingletonBean struct {
	SomeOtherBean *string `di.inject:""`
}

在这种情况下,DI将尝试自动找到一个注入候选(在类型为*string的已注册beans中)。很酷,不是吗?🤠 但是,如果没有找到候选(并且依赖项没有标记为可选),或者找到多个候选,它会panic。

最后,你可以将beans注入到切片和映射中。它的工作方式类似于上面的无ID注入,但会注入找到的所有候选:

type SingletonBean struct {
	someOtherBeans []*string `di.inject:""`
}
type SingletonBean struct {
	someOtherBeans map[string]*string `di.inject:""`
}

循环依赖

所有IoC容器的问题在于beans的互连可能会受到所谓的循环依赖的影响。考虑这个例子:

type CircularBean struct {
	Scope        Scope         `di.scope:"prototype"`
	CircularBean *CircularBean `di.inject:"circularBean"`
}

尝试使用这样的bean将导致circular dependency detected for bean: circularBean错误。如果它是一个Singleton bean,从自身引用bean本身没有问题。但是对Prototype/Request beans这样做会导致实例的无限创建。所以,要小心这一点:"能力越大,责任越大"🕸

中间件呢?

我们有一些😎 这是一个使用gorilla/mux路由器的例子(但可以自由使用任何其他路由器)。 基本上,这是第一个带有天气控制器的例子的扩展,但这次我们添加了Request beans并通过请求的上下文访问它们。 此外,这个例子展示了DI如何自动为你关闭资源(在这种情况下是DB连接)。为了简单起见,再次省略了适当的错误处理。

controllers/weather_controller.go

package controllers

import (
	"database/sql"
	"di-demo/services"
	"github.com/goioc/di"
	"net/http"
)

type WeatherController struct {
	// 注意注入甚至适用于未导出的字段
	weatherService *services.WeatherService `di.inject:"weatherService"`
}

func (wc *WeatherController) Weather(w http.ResponseWriter, r *http.Request) {
	dbConnection := r.Context().Value(di.BeanKey("dbConnection")).(*sql.Conn)
	city := r.URL.Query().Get("city")
	_, _ = dbConnection.ExecContext(r.Context(), "insert into log values (?, ?, datetime('now'))", city, r.RemoteAddr)
	weather, _ := wc.weatherService.Weather(city)
	_, _ = w.Write([]byte(*weather))
}

controllers/index_controller.go

package controllers

import (
	"database/sql"
	"fmt"
	"github.com/goioc/di"
	"net/http"
	"strings"
	"time"
)

type IndexController struct {
}

func (ic *IndexController) Log(w http.ResponseWriter, r *http.Request) {
	dbConnection := r.Context().Value(di.BeanKey("dbConnection")).(*sql.Conn)
	rows, _ := dbConnection.QueryContext(r.Context(), "select * from log")
	columns, _ := rows.Columns()
	_, _ = w.Write([]byte(strings.ToUpper(fmt.Sprintf("Requests log: %v\n\n", columns))))
	for rows.Next() {
		var city string
		var ip string
		var dateTime time.Time
		_ = rows.Scan(&city, &ip, &dateTime)
		_, _ = w.Write([]byte(fmt.Sprintln(city, "\t", ip, "\t", dateTime)))
	}
}

init.go

package main

import (
	"context"
	"database/sql"
	"di-demo/controllers"
	"di-demo/services"
	"github.com/goioc/di"
	"os"
	"reflect"
)

func init() {
	_, _ = di.RegisterBean("weatherService", reflect.TypeOf((*services.WeatherService)(nil)))
	_, _ = di.RegisterBean("indexController", reflect.TypeOf((*controllers.IndexController)(nil)))
	_, _ = di.RegisterBean("weatherController", reflect.TypeOf((*controllers.WeatherController)(nil)))
	_, _ = di.RegisterBeanFactory("db", di.Singleton, func(context.Context) (interface{}, error) {
		_ = os.Remove("./di-demo.db")
		db, _ := sql.Open("sqlite3", "./di-demo.db")
		db.SetMaxOpenConns(1)
		_, _ = db.Exec("create table log ('city' varchar not null, 'ip' varchar not null, 'time' datetime not null)")
		return db, nil
	})
	_, _ = di.RegisterBeanFactory("dbConnection", di.Request, func(ctx context.Context) (interface{}, error) {
		db, _ := di.GetInstanceSafe("db")
		return db.(*sql.DB).Conn(ctx)
	})
	_ = di.InitializeContainer()
}

main.go

package main

import (
	"di-demo/controllers"
	"github.com/goioc/di"
	"github.com/gorilla/mux"
	_ "github.com/mattn/go-sqlite3"
	"net/http"
)

func main() {
	router := mux.NewRouter()
	router.Use(di.Middleware)
	router.Path("/").HandlerFunc(di.GetInstance("indexController").(*controllers.IndexController).Log)
	router.Path("/weather").Queries("city", "{*?}").HandlerFunc(di.GetInstance("weatherController").(*controllers.WeatherController).Weather)
	_ = http.ListenAndServe(":8080", router)
}

好的…更多例子?

请查看单元测试以获取更多示例。


更多关于golang Spring风格依赖注入容器插件goioc/di的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang Spring风格依赖注入容器插件goioc/di的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


goioc/di: Golang 中的 Spring 风格依赖注入容器

goioc/di 是一个受 Spring 框架启发的 Golang 依赖注入容器,它提供了类似 Spring 的依赖注入功能,让 Golang 开发者能够以更声明式的方式管理应用程序组件及其依赖关系。

基本使用

安装

go get github.com/goioc/di

基本示例

package main

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

type UserService interface {
	GetUserName(id int) string
}

type UserServiceImpl struct{}

func (s *UserServiceImpl) GetUserName(id int) string {
	return fmt.Sprintf("User-%d", id)
}

func main() {
	// 1. 初始化容器
	di.Init()

	// 2. 注册Bean
	_, err := di.RegisterBean("userService", di.BeanConstructor(func() (interface{}, error) {
		return &UserServiceImpl{}, nil
	}))
	if err != nil {
		panic(err)
	}

	// 3. 获取Bean
	userService, err := di.GetBean("userService")
	if err != nil {
		panic(err)
	}

	// 类型断言
	service := userService.(UserService)
	fmt.Println(service.GetUserName(123)) // 输出: User-123
}

高级功能

自动装配

package main

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

type DatabaseService struct{}

func (d *DatabaseService) Query() string {
	return "data from database"
}

type UserService struct {
	Database *DatabaseService `di.inject:"databaseService"`
}

func (s *UserService) GetUser() string {
	return s.Database.Query()
}

func main() {
	di.Init()

	// 注册依赖
	_, _ = di.RegisterBean("databaseService", di.BeanConstructor(func() (interface{}, error) {
		return &DatabaseService{}, nil
	}))

	// 注册需要自动装配的服务
	_, _ = di.RegisterBean("userService", di.BeanConstructor(func() (interface{}, error) {
		return &UserService{}, nil
	}))

	// 获取并使用服务
	userService, _ := di.GetBean("userService")
	fmt.Println(userService.(*UserService).GetUser()) // 输出: data from database
}

单例与原型作用域

// 默认是单例
_, _ = di.RegisterBean("singletonService", di.BeanConstructor(func() (interface{}, error) {
	return &MyService{}, nil
}))

// 设置为原型作用域(每次获取新实例)
_, _ = di.RegisterBean("prototypeService", 
	di.BeanConstructor(func() (interface{}, error) {
		return &MyService{}, nil
	}),
	di.BeanScope(di.ScopePrototype),
)

后置处理器

type MyPostProcessor struct{}

func (p *MyPostProcessor) PostProcessBean(beanName string, bean interface{}) error {
	fmt.Printf("Processing bean: %s\n", beanName)
	// 可以对bean进行修改
	return nil
}

func main() {
	di.Init()
	
	// 注册后置处理器
	_, _ = di.RegisterBean("myPostProcessor", di.BeanConstructor(func() (interface{}, error) {
		return &MyPostProcessor{}, nil
	}))
	
	// 注册其他bean...
}

最佳实践

  1. 接口与实现分离:依赖注入最适合与接口一起使用,这样更容易替换实现

  2. 合理划分组件:将应用程序划分为逻辑组件,每个组件专注于单一职责

  3. 避免循环依赖:设计时要避免组件间的循环依赖关系

  4. 合理使用作用域:大多数服务应该是单例的,只有有状态的组件才考虑使用原型作用域

  5. 集中配置:将所有bean的注册放在应用程序初始化阶段

与原生Golang的比较

相比原生Golang的手动依赖管理,goioc/di提供了以下优势:

  1. 更松散的耦合:组件不需要知道如何创建它们的依赖项
  2. 更易于测试:可以轻松替换依赖项的模拟实现
  3. 更清晰的组件生命周期管理
  4. 更声明式的编程风格
  5. 更易于重构和修改

总结

goioc/di为Golang带来了类似Spring的依赖注入体验,虽然Golang本身不鼓励过度使用DI容器,但在大型项目中,合理的依赖注入可以显著提高代码的可维护性和可测试性。使用时应当权衡其带来的好处和复杂性,避免过度设计。

回到顶部