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 的安全网。

创建于 2026/6/25 更新于 2026/6/25