sync.Once.Do 能保证只执行一次,因其内部用原子标志位 done 和互斥锁实现:首次调用执行函数并置 done 为 true;后续调用直接返回,无论函数是否 panic,done 一旦设为 true 就不可重置。

sync.Once.Do 为什么能保证只执行一次
sync.Once.Do 内部用一个 done 原子标志位 + 互斥锁实现,首次调用时执行传入函数并标记完成;后续调用直接返回,不重复执行。它不关心函数是否 panic——只要函数开始执行(哪怕中途 panic),done 就会被设为 true,之后所有调用都跳过。
注意:sync.Once 不是“成功执行一次”,而是“最多执行一次”,且无法重置。一旦触发,哪怕函数 panic,也无法再补救或重试。
初始化数据库连接的典型用法
全局单例资源(如 DB、Redis 客户端)初始化最常用场景。错误写法是把 Do 放在 init 函数里——init 是包加载时同步执行,而 sync.Once 是运行时机制,两者目的不同,混用反而容易掩盖竞态。
- 把
sync.Once实例声明为包级变量(不是局部变量),确保跨 goroutine 共享 - 初始化函数必须接收零参数、返回 void,不能直接传参;需靠闭包捕获依赖
- 如果初始化失败(比如连接超时),应把错误存到全局变量或 panic,否则调用方无法感知
var dbOnce sync.Once
var db *sql.DB
var dbErr error
func GetDB() (*sql.DB, error) {
dbOnce.Do(func() {
db, dbErr = sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/test")
if dbErr == nil {
dbErr = db.Ping()
}
})
return db, dbErr
}
多个 Once 实例 vs 单个 Once 控制多件事
一个 sync.Once 只管一件事。想让“初始化配置 + 初始化日志 + 初始化 DB”按顺序只做一次?不能共用一个 sync.Once 然后塞三个函数进去——Do 只接受一个 func(),且无法拆分控制流。
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
立即学习“go语言免费学习笔记(深入)”;
- 要么用多个
sync.Once实例分别保护不同资源 - 要么把整套初始化逻辑打包进一个函数,由单个
sync.Once触发 - 不要试图在
Do里加 if 判断来“条件执行”,那会破坏 once 语义
常见误用:if someFlag { once.Do(initA) } else { once.Do(initB) } —— 这会导致 initA 或 initB 都可能不执行,违背“确保执行”的初衷。
测试中如何验证 Once 行为是否正确
并发调用 GetDB() 多次,检查是否真只连一次 DB、是否错误可复现。关键点:
- 用
sync.WaitGroup启多个 goroutine 并发调用目标函数 - 在初始化函数里加日志或计数器(如
atomic.AddInt64(&initCount, 1)) - 验证最终
initCount == 1,且所有调用返回相同实例或错误
注意:测试 panic 场景时,要确保 recover 逻辑不干扰 sync.Once 的状态——once 的 done 标志不受 defer/recover 影响,panic 后仍为 true。
once.Do(init),但 init 里出错就静默了,上层拿到 nil 指针才崩。务必显式暴露错误,别让 sync.Once 成为错误黑洞。

















