news 2026/8/30 5:43:14

Go 1.21 sync.OnceValue/OnceFunc 实战:三行搞定懒加载单例,告别手写双重检查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Go 1.21 sync.OnceValue/OnceFunc 实战:三行搞定懒加载单例,告别手写双重检查

Go 1.21 sync.OnceValue/OnceFunc 实战:三行搞定懒加载单例,告别手写双重检查

「配置只加载一次」「数据库连接池全局初始化一次」「正则只编译一次」——这类「用到时才初始化、且只初始化一次、且并发安全」的需求,Go 里几乎天天遇到。老办法是sync.Once配一个包级变量,能用,但样板代码一堆,还容易踩坑。Go 1.21 新增的sync.OnceValuesync.OnceFunc把它压成了三行。

先看手写 sync.Once 的样板

假设要懒加载一份配置(读文件、解析,开销不小,想用到时才做):

var(cfgOnce sync.Once cfg*Config cfgErrerror)funcGetConfig()(*Config,error){cfgOnce.Do(func(){cfg,cfgErr=loadConfig("app.yaml")// 只会执行一次})returncfg,cfgErr}

这段没错,但暴露了几个烦人的点:

  1. 三个包级变量(Once、结果、错误)散在外面,污染包命名空间,别的函数也能误改cfg
  2. 样板重复:每加一个懒加载单例,这套var + Once + Do就得抄一遍。
  3. 容易漏并发保护:新人常直接写if cfg == nil { cfg = load() },在并发下是数据竞争 + 可能初始化多次的经典 bug。

Go 1.21 的思路是:把「只跑一次」这件事封装成一个普通函数值,你调用它就自动享受「首次执行、后续复用、并发安全」,变量全藏进闭包里。

sync.OnceValue:懒加载一个返回值

sync.OnceValue(f)接收一个无参、返回一个值的函数f,返回一个新函数。这个新函数无论被调用多少次、被多少 goroutine 同时调用,f都只执行一次,之后每次调用都直接返回那一次的缓存结果:

import"sync"// loadConfig 开销大,只想在第一次真正用到时执行一次varGetConfig=sync.OnceValue(func()*Config{returnloadConfig("app.yaml")})funcmain(){c1:=GetConfig()// 第一次:真正执行 loadConfigc2:=GetConfig()// 第二次:直接返回缓存,不再执行// c1 == c2,同一个实例}

对比手写版,三个包级变量没了,Once没了,Do没了。GetConfig就是个普通函数,调用它就是「拿单例」。并发安全是内建的:一百个 goroutine 同时首次调用,loadConfig也只会跑一次,其余的会阻塞等第一次跑完再拿结果。

编译正则、初始化连接这类同理:

varemailRe=sync.OnceValue(func()*regexp.Regexp{returnregexp.MustCompile(`^[\w.\-]+@[\w.\-]+\.\w+$`)// 只编译一次})funcIsEmail(sstring)bool{returnemailRe().MatchString(s)// 每次调用 emailRe() 拿的是同一个已编译对象}

sync.OnceValues:带 error 的初始化

OnceValue只返回一个值,但初始化十有八九会失败(读文件、连数据库都可能出错)。Go 1.21 同时给了sync.OnceValues,返回两个值,专门伺候(T, error)这个 Go 里最常见的组合:

vargetDB=sync.OnceValues(func()(*sql.DB,error){db,err:=sql.Open("postgres",os.Getenv("DSN"))iferr!=nil{returnnil,err}iferr=db.Ping();err!=nil{returnnil,err}returndb,nil})funcQuery(ctx context.Context,qstring)(*sql.Rows,error){db,err:=getDB()// 首次建连并 Ping;之后直接复用同一个 *sql.DB 和同一个 erriferr!=nil{returnnil,fmt.Errorf("db 初始化失败: %w",err)}returndb.QueryContext(ctx,q)}

这就等价于开头那个手写GetConfig,但把三个包级变量全收进了闭包,外部一个都碰不到。注意语义:错误也会被缓存。如果第一次初始化失败,后续每次调用都会拿到同一个失败的 err,不会重试——这对「配置错了就该一直报错」是对的,但对「网络抖动导致的临时失败」可能不是你想要的(见下文坑点)。

sync.OnceFunc:只跑一次、不要返回值

有时候你只想让某段副作用代码执行一次,不需要返回值——比如注册指标、初始化日志、打一次埋点。用sync.OnceFunc:

varinitMetrics=sync.OnceFunc(func(){prometheus.MustRegister(requestCounter)// 重复注册会 panic,必须保证只跑一次log.Println("metrics registered")})funchandleRequest(){initMetrics()// 无论多少请求进来,注册只发生一次// ...}

OnceFunc返回的函数无参无返回值,调多少次里面的逻辑都只执行第一次。它就是sync.Once.Do的等价物,但不用你手动声明和管理那个sync.Once变量。

三个必须知道的坑

坑一:错误被永久缓存,不会重试。OnceValues把首次的error也缓存了。如果初始化失败是暂时的(如启动时数据库还没起来),它会让你永远拿到那个失败,直到进程重启。如果你需要「失败可重试」,OnceValues就不合适,得退回手写逻辑或用带重试的初始化——别指望它帮你重试。

坑二:初始化函数里 panic 会怎样?如果传入的f内部 panic 了,OnceValue/OnceFunc的语义是:这次算「已执行」,panic 向上抛出;之后再调用,会直接重新抛出同一个 panic 值,而不会再跑f也就是说初始化崩了就一崩到底,后续调用不会「重试初始化」。这和sync.Once.Do的行为一致——Do里 panic 后Once也认为已执行过。所以初始化函数里该处理的错误用 error 返回,别用 panic 表达「可恢复的失败」。

坑三:别在初始化函数里再调用它自己(死锁)。因为首次调用会持锁执行f,如果f内部又调了这个OnceValue返回的函数,就会自己等自己,死锁:

vargetA=sync.OnceValue(func()int{returngetA()+1// 死锁!首次执行时又调自己,永远等不到锁释放})

有相互初始化依赖时,把依赖抽成独立的 Once 函数,别让它们环状调用。

什么时候还用手写 sync.Once

OnceValue系列覆盖了绝大多数「懒加载 + 缓存结果」的场景,但下面两种还是手写sync.Once更合适:

  • 需要失败重试:如前所述,新 API 会缓存错误。要重试就别用它。
  • 初始化逻辑要访问多个外部状态、或有复杂条件:闭包能装,但如果逻辑很长、还要读写别的字段,直接once.Do(func(){...})反而更清楚。

日常「读一次配置/编译一次正则/建一次连接池」这类,直接上OnceValue/OnceValues/OnceFunc,别再抄那套三变量样板了。

小结

  • 手写sync.Once+ 包级变量做懒加载单例,样板多、污染命名空间、易漏并发保护。
  • Go 1.21 三兄弟:OnceValue(f)懒加载一个值、OnceValues(f)懒加载(T, error)OnceFunc(f)只跑一次副作用,变量全藏进闭包,并发安全内建。
  • 用法就是var Get = sync.OnceValue(func() T {...}),之后Get()拿单例,首次执行、后续复用。
  • 三个坑:错误会被永久缓存(不重试)、f里 panic 会一崩到底(后续重抛不重试)、别在f内递归调用自己(死锁)。
  • 需要失败重试或初始化逻辑复杂时,仍用手写sync.Once

一句话记忆点:懒加载单例别再抄三变量样板了,sync.OnceValue一行就是并发安全的「只算一次」。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/30 5:39:40

【亲测有效】VS Code 已安装中文语言包却仍是英文的解决办法

VS Code 已安装中文语言包却仍是英文:一次由语言包文件损坏引发的排查记录 文章目录VS Code 已安装中文语言包却仍是英文:一次由语言包文件损坏引发的排查记录一、问题现象描述二、排查过程1. 确认显示语言配置2. 确认语言包是否已安装3. 检查日志与翻译…

作者头像 李华
网站建设 2026/8/30 5:37:58

Python从入门到精通完整学习路线(2026最新版)

凌晨1点的时候, 前同事老张给我发来了消息, 内容是“我被裁了”。他以前是大厂的后端人员, 写了5年的业务代码。但是公司全面引入AI编程助手之后, 那些依靠复制粘贴、调调参数就能够完成的工作, 在一夜之间就被替代了。他想要自救, 却发觉连一个完整的多线程爬虫都写不清楚。这…

作者头像 李华
网站建设 2026/8/30 5:37:55

Hackertab.dev(极客页面) v1.26.13 免费安装版

极客页面被你弄成了新标签页, 身为开发人员, 能随时知晓最新技术新闻、libs、作业以及事件, 有前端、后端、全栈、移动、数据科学家 , 简而言之, 通用于所有开发人员, 感兴趣便赶紧来下载用一用。插件介绍新的趋势, 接连不断地, 出现在了技术世界之中, 因此, 保持更新, 已然是成…

作者头像 李华
网站建设 2026/8/30 5:36:50

C++单元测试实战:GoogleTest从入门到CI落地

在 C 项目里引入单元测试,很多团队都会经历同一个阶段:想写,但不知道从哪个文件开始;知道有 GoogleTest 这个框架,结果配置 CMake 时一头雾水;好不容易把第一个用例跑起来,又发现用例之间相互拖…

作者头像 李华
网站建设 2026/8/30 5:36:46

深度学习算法岗笔试复盘:核心考点与复习路线全拆解

最近不少读者私信我聊校招笔试的事,尤其深度学习算法岗,大家都觉得“题海战术”不好使,刷了一堆 LeetCode 却不知道重点在哪。这让我想起自己当年参加网易 2018 实习生招聘笔试的场景——那次笔试给我留下的印象特别深,不是因为它…

作者头像 李华
网站建设 2026/8/30 5:36:09

Transformer前置概念详解:从RNN到自注意力与位置编码

Transformer 的论文你多半翻过,但真正卡住的地方往往不是框架图,而是公式背后那些前置概念。比如你记住了 Attention 的公式是softmax(QK^T / sqrt(d_k))V,但 Q、K、V 到底从哪来,为什么除以sqrt(d_k),为什么还需要位置…

作者头像 李华