Go 手动依赖注入
Go 中在 main 函数手动组装依赖链的实践:从底向上创建实例、接口解耦、什么时候该引入 DI 框架。
#type / concept
#status / growing
#tech / dev / backend
#resource / go
[!info] related notes
Go 手动依赖注入
核心问题
Go 项目中 handler、service、repository 之间有依赖关系,怎么把它们组装起来?
手动 DI 模式
在 main 函数中按依赖关系从底向上逐个创建:
func main() {
// 1. 基础设施
db, _ := database.Connect(dbCfg)
redis, _ := database.ConnectRedis(redisCfg)
jwtConfig := auth.JWTConfigFromEnv()
// 2. Repository
userRepo := repository.NewUserRepository(db)
sessionRepo := repository.NewSessionRepository(db)
// 3. Service
authService := service.NewAuthService(userRepo, jwtConfig)
sessionService := service.NewSessionService(sessionRepo)
// 4. Handler
authHandler := handler.NewAuthHandler(authService)
sessionHandler := handler.NewSessionHandler(sessionService)
// 5. 路由
r := gin.Default()
// ...
}
优点:
- 打开 main.go 就能看到所有依赖关系
- 编译时类型安全
- IDE 可以追踪引用
- 零额外抽象
缺点:
- main 函数会变长(超过 200 行时考虑重构)
接口解耦
Service 通过接口引用 Repository,而不是直接依赖具体实现:
type userRepository interface {
Create(ctx context.Context, user *User) error
GetByEmail(ctx context.Context, email string) (*User, error)
}
type AuthService struct {
userRepo userRepository // 接口
}
好处:
- 测试时可以用 mock repository
- 换实现不需要改 service 代码
什么时候引入 DI 框架
当出现以下信号时考虑 wire/dig:
- main 函数超过 200 行
- 多个服务共享相似的依赖子图
- 需要按环境切换不同实现
- 团队规模大,需要强制约束依赖方向
对于中小项目,手动 DI 完全够用。
常见错误
循环依赖
// ❌ A 依赖 B,B 依赖 A
serviceA := NewServiceA(serviceB)
serviceB := NewServiceB(serviceA) // 编译错误
解决:提取共同依赖到第三个组件。
忘记传依赖
// ❌
handler := NewHandler(service) // 编译错误:not enough arguments
Go 的类型系统会在编译时捕获,这是手动 DI 的安全网。