Go 依赖注入:编写更好代码的实用技巧
提示 1:接口,接口,接口
第一个提示至关重要。结合后续提示,它将显著提高代码库的可维护性。
始终为每个具有函数的结构体创建一个接口。这很重要,原因如下:
- 定义接口 封装了与结构体相关的行为。
- 简化测试,可以使用像 GoMock 这样的库。
- 如果代码中没有接口,就无法充分利用 Go 的特性,也无法使用 mock,这对有效测试至关重要。
示例
让我们创建一个具有两个函数的 SQL 仓库:CreateUser
和 GetUserLastname
。
package main
import "database/sql"
type IUserRepository interface {
CreateUser(firstname string, lastname string) error
GetUserLastname(firstname string) (*string, error)
}
type userRepository struct {
db *sql.DB
}
func (r *userRepository) CreateUser(firstname string, lastname string) error {
// TODO: 与数据库交互,创建用户
return nil
}
func (r *userRepository) GetUserLastname(firstname string) (*string, error) {
// TODO: 与数据库交互,检索信息
return nil, nil
}
func InitUserRepository(db *sql.DB) IUserRepository {
return &userRepository{db: db}
}
这看起来很简单,但这是至关重要的。
从上到下:
- 定义了包含所有函数签名的接口,并且它是公开的(首字母大写)。
- 定义了结构体并将一些函数链接到它。结构体定义是私有的(小写),DB 实例也是。这确保了仓库只能在当前包内定义,DB 变量只能设置一次。
- 创建了一个初始化结构体的函数,该函数公开并返回接口类型。
遵循这种模式,强制开发人员调用 InitUserRepository
来初始化 userRepository
,确保接口得到遵守。
提示 2:合并结构体和接口以提高可维护性
如果你的用户仓库实现了 20 或 30 个函数,文件和测试文件可能会变得庞大且难以管理。
在 Go 中,可以合并结构体和接口。这样可以为每个函数创建一个单独的文件。虽然文件数量会增加,但好处显著:
- 减少认知负荷:处理小文件,专注于当前函数。
- 简化代码审查:团队中的开发人员可以更快速地理解代码。
- 测试文件 只包含特定函数的测试,更容易编写和维护。
实践示例
// file createUser.go
package main
import "database/sql"
type ICreateUser interface {}
type createUser struct {
db *sql.DB
}
func (r *createUser) CreateUser(firstname string, lastname string) error {
// TODO: 与数据库交互,创建用户
return nil
}
func InitCreateUser(db *sql.DB) ICreateUser {
return &createUser{db: db}
}
// file getUser.go
package main
import "database/sql"
type IGetUser interface {
GetUserLastname(firstname string) (*string, error)
}
type getUserLastname struct {
db *sql.DB
}
func (r *getUserLastname) GetUserLastname(firstname string) (*string, error) {
// TODO: 与数据库交互,获取信息
return nil, nil
}
func InitGetUserLastname(db *sql.DB) IGetUser {
return &getUserLastname{db: db}
}
// file UserRepository.go
package main
import "database/sql"
type IUserRepository interface {
ICreateUser
IGetUser
}
type UserRepository struct {
ICreateUser
IGetUser
}
func InitUserRepository(db *sql.DB) IUserRepository {
createUser := InitCreateUser(db)
getUserLastname := InitGetUserLastname(db)
return &UserRepository{
ICreateUser: createUser,
IGetUser: getUserLastname,
}
}
// main.go
package main
func main() {
db := FAKE_init_db()
userRepository := InitUserRepository(db)
userRepository.GetUserLastname()
userRepository.CreateUser()
}
提示 3:在依赖注入中只注入接口
依赖注入的概念可以被视为一个“套娃”类或结构体,每一层都依赖于下一层。
示例
仓库示例
// userRepository.go
package main
import "database/sql"
type IUserRepository interface {
GetUserLastname(firstname string) (*string, error)
}
type UserRepository struct {
db *sql.DB
}
func (r *UserRepository) GetUserLastname(firstname string) (*string, error) {
// 实现逻辑
return nil, nil
}
func InitUserRepository(db *sql.DB) IUserRepository {
return &UserRepository{db: db}
}
服务示例
// userService.go
package main
type IUserService interface {
GetUserLastname(firstname string) (*string, error)
}
type userService struct {
repo IUserRepository
}
func (s *userService) GetUserLastname(firstname string) (*string, error) {
return s.repo.GetUserLastname(firstname)
}
func InitUserService(repo IUserRepository) IUserService {
return &userService{repo: repo}
}
处理程序示例
// userHandler.go
package main
type IUserHandler interface {
GetUserLastnameHandler(firstname string) string
}
type UserHandler struct {
service IUserService
}
func (h *UserHandler) GetUserLastnameHandler(firstname string) string {
// 处理逻辑
return ""
}
func InitUserHandler(service IUserService) IUserHandler {
return &UserHandler{service: service}
}
主程序示例
// main.go
package main
func main() {
config := GetConfig()
db, err := InitDb(config.DbConfig)
if err != nil {
log.Fatal(err)
}
// 初始化依赖
userRepository := InitUserRepository(db)
userService := InitUserService(userRepository)
userHandler := InitUserHandler(userService)
// 启动应用
app := InitApp(userHandler)
app.Run()
}
结论
这些提示将显著改善你的 Go 开发工作流程,使你的代码更具可维护性、可测试性和灵活性。依赖注入是一种强大的工具,若有效使用,可以提升项目质量。如果你有其他提示或经验,请在评论中分享,我很乐意听到你的见解!别忘了订阅以获取我最新的文章更新。你的支持对我来说意义重大。