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
更多关于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...
}
最佳实践
-
接口与实现分离:依赖注入最适合与接口一起使用,这样更容易替换实现
-
合理划分组件:将应用程序划分为逻辑组件,每个组件专注于单一职责
-
避免循环依赖:设计时要避免组件间的循环依赖关系
-
合理使用作用域:大多数服务应该是单例的,只有有状态的组件才考虑使用原型作用域
-
集中配置:将所有bean的注册放在应用程序初始化阶段
与原生Golang的比较
相比原生Golang的手动依赖管理,goioc/di提供了以下优势:
- 更松散的耦合:组件不需要知道如何创建它们的依赖项
- 更易于测试:可以轻松替换依赖项的模拟实现
- 更清晰的组件生命周期管理
- 更声明式的编程风格
- 更易于重构和修改
总结
goioc/di为Golang带来了类似Spring的依赖注入体验,虽然Golang本身不鼓励过度使用DI容器,但在大型项目中,合理的依赖注入可以显著提高代码的可维护性和可测试性。使用时应当权衡其带来的好处和复杂性,避免过度设计。