控制反转
IoC 简介
控制反转和依赖注入的主要目标是移除应用程序的依赖关系。这使得系统更加解耦和可维护。

依赖注入是一个适用于任何编程语言的概念。依赖注入背后的通用概念称为控制反转。根据这个概念,结构体不应该静态配置其依赖项,而应该从外部进行配置。
依赖注入设计模式允许我们移除硬编码的依赖关系,使我们的应用程序松散耦合、可扩展和可维护。
依赖注入
Hiboot 最重要的特性之一是依赖注入。Hiboot 实现了 JSR-330 标准。
依赖注入提供对象所需的依赖项。依赖项不是自己构造的,而是通过某种外部方式注入的。
基础示例
package service
import "github.com/hidevopsio/hiboot/pkg/app"
type UserRepository interface {
FindById(id uint64) (*User, error)
}
type UserService struct {
repository UserRepository
}
func init() {
app.Register(newUserService)
}
func newUserService(repository UserRepository) *UserService {
return &UserService{
repository: repository,
}
}
func (s *UserService) GetUser(id uint64) (*User, error) {
return s.repository.FindById(id)
}
构造函数注入(推荐)
构造函数注入是 Hiboot 推荐的方式。它有以下优点:
- 可测试性:易于使用 Mock 实现单元测试
- 语法验证:IDE 可以验证类型并避免拼写错误
- 不可变性:依赖项在构造后无法更改
- 完整性:确保所有必需的依赖项都已设置
示例
package controller
import (
"github.com/hidevopsio/hiboot/pkg/app"
"github.com/hidevopsio/hiboot/pkg/at"
"github.com/hidevopsio/hiboot/pkg/model"
"myapp/service"
)
type userController struct {
at.RestController
at.RequestMapping `value:"/user"`
userService *service.UserService
}
func init() {
app.Register(newUserController)
}
// 依赖项通过构造函数参数注入
func newUserController(userService *service.UserService) *userController {
return &userController{
userService: userService,
}
}
func (c *userController) GetById(id uint64) (model.Response, error) {
user, err := c.userService.GetUser(id)
response := new(model.BaseResponse)
response.SetData(user)
return response, err
}
字段注入
字段注入通过 inject:"" 结构体标签触发。当字段上存在此标签时,Hiboot 会尝试根据字段类型解析要注入的对象。
基础字段注入
type userController struct {
at.RestController
UserService *service.UserService `inject:""`
}
func newUserController() *userController {
return &userController{}
}
func init() {
app.Register(newUserController)
}
命名注入
如果同一接口有多个实现,可以使用字段名或标签值来区分:
type AuthService interface {
Authenticate(username, password string) bool
}
// AuthService 的两个实现
type basicAuthService struct{}
type oauth2AuthService struct{}
type authController struct {
at.RestController
// 通过字段名注入(匹配 "basicAuthService")
BasicAuthService AuthService `inject:""`
// 通过字段名注入(匹配 "oauth2AuthService")
Oauth2AuthService AuthService `inject:""`
// 通过显式名称注入
PrimaryAuth AuthService `inject:"basicAuthService"`
}
值注入
使用 value:"" 标签注入配置值:
type MyService struct {
AppName string `value:"${app.name}"`
Port int `value:"${server.port}"`
LogLevel string `value:"${logging.level:info}"` // 带默认值
}
默认值注入
使用 default:"" 标签指定默认值:
type Config struct {
Timeout int `default:"30"`
Host string `default:"localhost"`
Enabled bool `default:"true"`
}
方法注入
方法注入用于自动配置。依赖项通过方法参数注入:
package myconfig
import "github.com/hidevopsio/hiboot/pkg/app"
type configuration struct {
app.Configuration
}
func init() {
app.Register(newConfiguration)
}
func newConfiguration() *configuration {
return &configuration{}
}
// Database 通过方法参数注入
func (c *configuration) UserRepository(db *Database) UserRepository {
return &mysqlUserRepository{db: db}
}
// UserService 依赖于 UserRepository
func (c *configuration) UserService(repo UserRepository) *UserService {
return &UserService{repository: repo}
}
注册
所有可注入的组件必须使用 app.Register() 注册:
func init() {
// 注册构造函数
app.Register(newUserService)
app.Register(newUserController)
// 一次注册多个
app.Register(newFoo, newBar, newBaz)
}
注入规则
- 指针类型:依赖项必须是指针类型或接口
- 必须注册:组件必须通过
app.Register()注册 - 无循环依赖:避免循环依赖链
- 接口解析:如果存在多个实现,使用命名来区分
有效
type Foo struct{}
type Bar struct {
foo *Foo // 指针 - 会被注入
}
无效
type Foo struct{}
type Bar struct {
foo Foo // 非指针 - 不会被注入
}
使用依赖注入进行测试
构造函数注入使测试变得简单:
package controller
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// Mock 实现
type mockUserService struct {
mock.Mock
}
func (m *mockUserService) GetUser(id uint64) (*User, error) {
args := m.Called(id)
return args.Get(0).(*User), args.Error(1)
}
func TestUserController_GetById(t *testing.T) {
// 创建 Mock
mockService := new(mockUserService)
mockService.On("GetUser", uint64(1)).Return(&User{Name: "Test"}, nil)
// 通过构造函数注入 Mock
controller := newUserController(mockService)
// 测试
response, err := controller.GetById(1)
assert.NoError(t, err)
assert.NotNil(t, response)
mockService.AssertExpectations(t)
}