sync.Once只能执行一次,因用atomic.CompareAndSwapUint32原子检查done标志位,首次成功后置1,后续调用直接返回;panic后done仍为1,不重试,需上层自行处理失败。

延迟加载不是“加个 sync.Once 就完事”,线上项目里它最容易在并发、错误恢复和资源生命周期上翻车。
为什么 sync.Once 在线上扛不住重试和降级
它只做“一次”——成功就锁死,失败就永远卡住。线上服务重启后上游还没就绪,Once.Do() 里 sql.Open 失败,后续所有请求都直接返回 initErr,连重试机会都没有。
- 真实场景需要:首次失败 → 返回兜底响应(如空数据或 503)→ 后台异步重试 → 成功后自动切到真实实例
- 用
golang.org/x/sync/lazy.Group替代:Load()返回(T, error),每次调用都可重试 - 别把初始化逻辑写进
Do()闭包里;拆成独立函数,方便单元测试和 mock
延迟加载字段必须是指针,且初始化前要判空
结构体里直接嵌入 RealDB 或 CacheClient 字段,等于启动时就初始化了,延迟加载失效。
- 正确姿势:
db *sql.DB+once sync.Once,所有方法里先p.once.Do(p.init) - 错误写法:
if p.db == nil { p.db = newDB() }—— 并发下可能多次 new,也可能 panic - 如果初始化可能失败,
init()里要存错:p.initErr = err,否则下次调用还是空指针 dereference
HTTP 代理里延迟加载后端,header 和 body 处理顺序不能错
权限检查必须在读 body 前完成,后端发现也得在转发前做完,否则 req.Body 已被消耗,转发失败。
立即学习“go语言免费学习笔记(深入)”;
- 入口处取
r.Header.Get("Authorization")验签,别等RoundTrip阶段 -
getBackend(r.Host)必须返回已初始化的实例,不能返回*http.Client再现场 new - 转发前调用
req.Header.Clone(),否则并发修改 header 会导致脏数据 - 用专用
http.Transport,禁用默认连接池复用,防跨请求泄漏
最常被忽略的一点:延迟加载不是性能优化的银弹。如果初始化本身依赖另一个未延迟的服务(比如配置中心),那只是把阻塞从启动阶段挪到了第一次请求,反而放大了尾部延迟。上线前一定要用真实链路压测首次请求耗时。


















