饿汉式单例本质是包加载时初始化,依赖Go包初始化机制天然线程安全且唯一;它通过全局变量赋值或init函数实现,无需sync.Once或判断逻辑,启动即创建、无运行时开销,但可能浪费内存或拖慢启动。

饿汉式单例在 Go 中本质就是「包加载时就完成实例化」,它不是靠逻辑判断或延迟调用,而是靠 Go 的包初始化机制天然保证线程安全和唯一性。只要不把初始化逻辑塞进 GetInstance 函数里,就不是懒汉——哪怕你起了个叫 GetInstance 的函数名。
饿汉式单例 = 全局变量 + 包级初始化
Go 的包初始化(包括全局变量赋值和 init 函数)是串行、一次、且发生在 main 执行前的。这意味着:
-
var instance *Config = &Config{...}这行代码在包被 import 时就执行,instance指向的内存已分配、字段已初始化 - 不需要任何锁、
sync.Once或 if 判断,GetInstance()只是单纯返回这个早已存在的指针 - 如果初始化过程包含耗时操作(比如读配置文件、连接数据库、调用远程服务),会拖慢整个程序启动,且即使后续完全没用到该单例也照常执行
两种写法:全局变量赋值 vs init 函数
二者效果等价,但语义和可读性有差异:
- 直接赋值:
var instance = &DatabasePool{connections: make([]*sql.DB, 0, 10)}—— 简洁,适合结构体字段可字面量初始化的场景 -
init函数:var instance *DatabasePool; func init() { instance = NewDatabasePool() }—— 更灵活,能封装复杂初始化逻辑(比如错误处理、重试),也方便单元测试 mock - ⚠️ 注意:
init函数里若调用外部服务并 panic(如 ping 不通就panic("db unreachable")),会导致所有 import 该包的测试或 CLI 工具直接失败,本地调试非常痛苦
为什么不用 sync.Once?它和饿汉无关
sync.Once 是懒汉式的核心工具,它的作用是「确保某段代码只执行一次」,但饿汉式根本不需要「执行一次」——它连「执行」这个动作都发生在包加载期,早于任何 goroutine 启动。如果你看到代码里用了 sync.Once 却还声称是饿汉式,那只是混淆了概念:
立即学习“go语言免费学习笔记(深入)”;
- 用了
sync.Once→ 说明初始化逻辑藏在某个函数里 → 实际是懒汉式 - 包一加载就初始化 → 即使没调用任何函数 → 才是真饿汉
- Go 1.21+ 的
sync.OnceValue同理,只适用于懒加载返回值场景,对饿汉无意义
饿汉式最常踩的坑:隐式初始化开销
开发者容易忽略的是「import 即触发」这个特性。例如:
- 你在
pkg/config里用饿汉式初始化一个*Config,里面做了ioutil.ReadFile("config.yaml") - 另一个包
pkg/utils只想用其中某个工具函数,却 import 了pkg/config—— 结果启动就报错:找不到 config.yaml - 测试时想 mock 配置,却发现
init已执行、无法拦截
这类问题不会在编译时报错,而是在运行或测试阶段突然暴露,且难以定位。真正需要饿汉式的场景其实很窄:轻量、确定必用、初始化无副作用(比如空结构体、预设常量池)。其他情况,宁可选带 sync.Once 的懒汉式,把不确定性控制在第一次调用时,而不是整个程序生命周期的起点。


















