

新闻资讯
技术学院Go中单例+依赖注入靠sync.Once延迟初始化和包级指针变量实现,依赖由外部传入而非硬编码,支持测试替换;需避免init初始化、全局直接赋值及内部new依赖。
在 Go 中实现“单例 + 依赖注入”不是靠语言特性(Go 没有类、构造器或自动 DI 容器),而是靠设计约定和包级变量 + 显式初始化控制。核心目标有两个:一是确保某个结构体全局唯一;二是让它的依赖不硬编码,支持替换(比如测试时用 mock)。关键在于“延迟初始化”和“依赖由外部传入”,而不是在结构体内 new 出来。
避免包初始化时就创建实例(可能依赖未就绪),也避免每次调用都加锁。标准做法是结合 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:
service.GetInstance(db, cfg) 获取实例例如:
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{},而不必动真实数据库。
var svc = NewService(...))—— 无法延迟、无法注入、测试难svc.db = sql.Open(...))—— 违反控制反转不复杂但容易忽略。单例只是手段,真正价值在于依赖清晰、可测、可换。