
Go 里用全局变量模拟单例,为什么并发读写会出问题
因为 Go 的全局变量本身不带同步语义,var instance *Service 这种声明只是分配了内存地址,多个 goroutine 同时读写它,既没锁也没原子操作,直接触发数据竞争(fatal error: concurrent map writes 或更隐蔽的脏读)。
常见错误现象:
- 程序偶发 panic,报
concurrent map iteration and map write - 初始化后
instance有时是nil,有时又非空,尤其在init()和main()之间有 goroutine 启动时 - 用
go run -race能稳定复现Data Race报告
实操建议:
- 别手动写
if instance == nil { instance = &Service{} }—— 这不是线程安全的双重检查 - 优先用
sync.Once配合指针变量,它内部用原子操作+互斥锁保证只执行一次 - 如果单例要支持热重载或重置,就别用全局变量,改用依赖注入或 context 传递
sync.Once 是怎么保证单例初始化只执行一次的
sync.Once 不是靠“判断变量是否为 nil”,而是靠一个 uint32 状态字段 + 原子操作 atomic.LoadUint32 和 atomic.CompareAndSwapUint32 来实现状态跃迁:从 0(未执行)→ 1(执行中)→ 2(已完成)。即使十个 goroutine 同时调 once.Do(),也只有一个能抢到“执行权”,其余阻塞等待,等它完成再一起返回。
立即学习“go语言免费学习笔记(深入)”;
典型用法:
var (
instance *DB
once sync.Once
)
func GetDB() *DB {
once.Do(func() {
instance = &DB{conn: connectToDB()}
})
return instance
}
注意点:
-
once.Do()里的函数不能 panic,否则once状态卡在 1,后续调用永远阻塞 - 不要把
once和instance定义在不同包里并导出——一旦被外部误复用,就失去单例语义 - 如果初始化逻辑耗时长,考虑提前在
init()或main()开头调一次GetDB(),避免首次请求慢
为什么不用 init() 函数做单例初始化
init() 确实只运行一次,但它发生在包加载期,不可控、不可重试、无法传参、也无法捕获错误。更关键的是:它无法解决「跨包可见性」和「初始化顺序依赖」问题。
使用场景限制:
- 若单例依赖配置文件、环境变量或网络服务(比如连 Redis),
init()里硬编码会直接导致包导入失败或 panic - 两个包 A 和 B 都定义了
init()初始化各自的单例,但 B 依赖 A 的实例,Go 的初始化顺序只保证 import 依赖链,不保证变量级依赖 - 测试时无法替换 mock 实例,因为
init()在测试启动前就跑完了
替代思路:
- 把初始化逻辑拆成纯函数,如
NewDB(cfg Config) (*DB, error),由上层统一调用 - 用
sync.Once包一层懒加载,兼顾延迟和安全 - 真要静态初始化,至少用
var _ = initDB()显式调用,并让initDB()返回 error 供主流程校验
全局变量在 CGO 或 plugin 场景下的内存隔离风险
Go 的全局变量在 main 包和 plugin(plugin.Open)之间不共享;CGO 导出的 C 函数访问 Go 全局变量时,实际访问的是当前 goroutine 所属 Go runtime 的副本——如果 C 代码在非 Go 线程里回调(比如 pthread 创建的线程),那它根本看不到 Go 的堆,instance 对它来说是未定义行为。
容易踩的坑:
- 用
//export暴露函数给 C,然后在 C 层反复调这个函数试图复用 Go 单例,结果每次都是新对象 - plugin 里定义同名全局变量,和 main 包的变量完全无关,看似“单例”,实则每个 plugin 实例都有一份
- 用
unsafe.Pointer把 Go 全局变量地址传给 C,C 线程直接解引用 → 程序随机 crash,GC 可能已回收该内存
可行做法:
- CGO 场景下,所有状态通过参数传递,避免隐式依赖全局变量
- plugin 如需共享状态,走 RPC 或 channel,而不是变量地址
- 实在要跨语言共享状态,用 mmap 文件或系统级共享内存(如 POSIX shm),由双方按协议读写
真正麻烦的从来不是“怎么写单例”,而是“谁在什么时候、以什么方式、在哪个 goroutine 或线程里第一次访问它”。初始化时机、执行上下文、内存模型边界——这三个点漏掉任何一个,全局变量就会从便利变成隐患。










