如何在Golang中实现单例模式_Golang单例模式实现方法

Go单例靠包级变量+sync.Once实现,线程安全且延迟初始化;不用init因无法按需、不支持错误返回、难测试;禁用if-nil手动实现以防竞态。

Go 语言里没有“类”和“构造函数”,所以单例不是靠私有化构造器实现的,而是靠包级变量 + sync.Once 控制初始化时机 —— 这是最安全、最常用的方式。

sync.Once 保证全局唯一实例

直接声明一个包级指针变量,配合 sync.OnceDo 方法确保 init 函数只执行一次。这是 Go 官方推荐的单例写法,线程安全且无竞态风险。

  • sync.Once 内部使用原子操作和互斥锁,比手写 if instance == nil + mutex.Lock() 更可靠
  • 初始化逻辑放在闭包或独立函数里,避免在变量声明时就执行(防止 init 循环或依赖未就绪)
  • 返回指针类型,方便后续方法定义为值接收者或指针接收者
package singleton

import "sync"

type Config struct {
    Timeout int
    Env     string
}

var (
    instance *Config
    once     sync.Once
)

func GetConfig() *Config {
    once.Do(func() {
        instance = &Config{
            Timeout: 30,
            Env:     "prod",
        }
    })
    return instance
}

为什么不用 init() 函数?

init() 确实只执行一次,但它在包加载时就运行,无法按需延迟初始化,也不支持带参数或错误返回 —— 实际项目中单例常需读配置、连数据库、校验权限,这些都可能失败。

  • init() 不能返回 error,出错只能 panic,不可控
  • 无法在测试中重置或替换单例(比如 mock 数据库连接)
  • 如果单例依赖其他尚未 init 的包,会触发隐式依赖顺序问题

带错误返回的单例(如初始化 DB 连接)

当初始化可能失败(比如打开文件、连接 Redis),需要把 error 暴露给调用方,并缓存失败状态避免重复尝试。

  • 用两个包级变量:一个存实例,一个存 error
  • 首次调用时初始化,之后直接返回缓存的结果(无论成功或失败)
  • 不建议在 GetXXX() 中 panic,应由上层决定如何处理 erro

    r
package db

import (
    "database/sql"
    "sync"
)

var (
    instance *sql.DB
    err      error
    once     sync.Once
)

func GetDB(dsn string) (*sql.DB, error) {
    once.Do(func() {
        instance, err = sql.Open("mysql", dsn)
        if err == nil {
            err = instance.Ping()
        }
    })
    return instance, err
}

注意:不要用全局变量 + if 判断模拟单例

这种写法看似简洁,但存在竞态风险,尤其在高并发场景下可能创建多个实例:

// ❌ 危险!可能创建多个实例
var instance *Config

func GetConfig() *Config {
    if instance == nil { // 多个 goroutine 同时通过判断
        instance = &Config{} // 多次赋值
    }
    return instance
}

即使加了 mutex,也容易漏锁或死锁;而 sync.Once 是标准库专为此设计的原语,无需自己造轮子。真正要注意的是:别在单例方法里做耗时操作(比如每次调用都查一次 etcd),单例只管“实例创建”,不负责“每次调用逻辑”。