欢迎您访问新疆栾骏商贸有限公司,公司主营电子五金轴承产品批发业务!
全国咨询热线: 400-8878-609

新闻资讯

技术学院

如何在Golang中实现单例+依赖注入模式_保证全局实例和解耦

作者:P粉6029986702025-12-31 00:00:00
Go中单例+依赖注入靠sync.Once延迟初始化和包级指针变量实现,依赖由外部传入而非硬编码,支持测试替换;需避免init初始化、全局直接赋值及内部new依赖。

在 Go 中实现“单例 + 依赖注入”不是靠语言特性(Go 没有类、构造器或自动 DI 容器),而是靠设计约定和包级变量 + 显式初始化控制。核心目标有两个:一是确保某个结构体全局唯一;二是让它的依赖不硬编码,支持替换(比如测试时用 mock)。关键在于“延迟初始化”和“依赖由外部传入”,而不是在结构体内 new 出来。

用 once.Do 实现线程安全的懒加载单例

避免包初始化时就创建实例(可能依赖未就绪),也避免每次调用都加锁。标准做法是结合 sync.Once 和指针变量:

var (
    instance *Service
    once     sync.Once
)

type Service struct {
    db  *sql.DB
    cfg Config
}

func NewService(db *sql.DB, cfg Config) *Service {
    return &Service{db: db, cfg: cfg}
}

// GetInstance 返回全局唯一 *Service,首次调用时初始化
func GetInstance(db *sql.DB, cfg Config) *Service {
    once.Do(func() {
        instance = NewService(db, cfg)
    })
    return instance
}

注意:GetInstance 接收依赖参数,不自己创建 db 或读配置 —— 这就是解耦的第一步。

把依赖注入逻辑上移到应用启动层

单例本身不负责“找依赖”,而是由 main 或 cmd 层统一组装并注入。这样测试时可轻松传入 mock:

  • main.go 中读取配置、打开数据库连接、初始化日志等
  • 然后调用 service.GetInstance(db, cfg) 获取实例
  • 再把 service 实例传给 HTTP handler、gRPC server 等组件

例如:

func main() {
    cfg := loadConfig()
    db := connectDB(cfg)
    svc := service.GetInstance(db, cfg) // 注入依赖

    http.HandleFunc("/api/user", userHandler(svc))
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func userHandler(svc *service.Service) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // 使用 svc.DoSomething()
    }
}

接口抽象 + 构造函数参数化,为替换留出口

如果直接暴露 *Service,调用方就和具体类型耦合了。更推荐定义接口,并让构造函数接受接口依赖:

type DBInterface interface {
    QueryRow(query string, args ...any) *sql.Row
}

type Service interface {
    GetUser(id int) (*User, error)
}

type serviceImpl struct {
    db  DBInterface
    cfg Config
}

func NewService(db DBInterface, cfg Config) Service {
    return &serviceImpl{db: db, cfg: cfg}
}

这样单元测试时可以传入 &mockDB{},而不必动真实数据库。

避免常见陷阱

  • 不要在 init() 函数里初始化单例 —— 依赖可能还没准备好,且无法传参
  • 不要用全局 var 直接赋值(如 var svc = NewService(...))—— 无法延迟、无法注入、测试难
  • 不要在单例方法里 new 其他服务(如 svc.db = sql.Open(...))—— 违反控制反转
  • 如果需要多个配置变体的单例(如 dev/test/prod 不同 db),考虑用 map + key 区分,或改用工厂函数

不复杂但容易忽略。单例只是手段,真正价值在于依赖清晰、可测、可换。