Go sync.Map.LoadOrStore 的 loaded 为什么要单独判断
来源:17golang原创
时间:2026-10-13 12:12:33 308浏览 收藏
我在把一个“按 key 惰性创建对象”的缓存从互斥锁改成 sync.Map 时,第一次最容易混淆的并不是并发安全,而是 LoadOrStore 的两个返回值。它返回的 actual 不一定是本次传入的对象,loaded 也不是“调用是否成功”的标志。
判断当前 goroutine 是否真正把 value 放进了 Map,要看loaded == false;如果loaded == true,actual才是已经存在的旧值,当前传入的 value 没有成为结果。
官方文档地址:https://pkg.go.dev/sync#Map.LoadOrStore
先记住返回值:actual 是最终值,loaded 是来源
LoadOrStore(key, value) 做的是一次原子决策:key 已经存在时,返回已有值;key 不存在时,写入传入值并返回它。第一个返回值 actual 表示这次调用最终应当使用的值,第二个返回值 loaded 表示这个值是不是从 Map 中已经加载出来的。
package main
import (
"fmt"
"sync"
)
func main() {
var cache sync.Map
// 第一次调用没有 key,传入的对象会成为最终值。
actual, loaded := cache.LoadOrStore("user:42", "first")
fmt.Printf("actual=%v loaded=%v\n", actual, loaded)
// 第二次调用不会覆盖旧值,actual 是已有值,loaded 表示已命中。
actual, loaded = cache.LoadOrStore("user:42", "second")
fmt.Printf("actual=%v loaded=%v\n", actual, loaded)
}
这个例子的输出逻辑是:第一次得到 first false,第二次得到 first true。第二次传入的 second 只是候选值,并没有覆盖已经存在的内容。

沿着数据生命周期看三种结果
我后来不再把 LoadOrStore 记成“带默认值的 Load”,而是把一次调用拆成候选值、原子决策和最终值三个阶段,这样在并发场景里更不容易读错。
Map 中没有 key:候选值完成写入
当 key 不存在时,方法会把 value 存入 Map,并返回同一个逻辑结果。此时 loaded == false,它告诉调用方:这次调用完成了“占位”或“首次登记”。如果后续还要执行昂贵初始化,这个分支通常就是初始化责任的归属点。
Map 中已有 key:已有值覆盖候选值
当 key 已经存在时,方法不覆盖旧值,返回的 actual 是 Map 里的已有对象,loaded == true。因此不能写成“无论如何都使用本地创建的对象”,否则本地对象可能已经做过构造或打开了资源,却没有进入缓存。
多个 goroutine 同时到达:只有一个调用拿到 false
并发竞争时,多个 goroutine 都可以先准备候选值,但对同一个 key 的原子决策只有一个会走 loaded == false。其他调用会拿到同一个已存入的 actual,并看到 loaded == true。这也是它适合做一次性占位的原因。

把 loaded 接到惰性初始化流程
真正写业务代码时,我会先决定“创建对象是否便宜”。如果只是轻量配置,可以每个 goroutine 都创建候选值;如果创建动作会连接外部服务、打开文件或启动后台任务,就应该把昂贵动作放在只有赢家执行的路径中。
package cache
import (
"fmt"
"sync"
)
type Client struct {
Address string
}
var clients sync.Map
func GetClient(address string) (*Client, error) {
// 先构造候选值;这里只放轻量字段,不在这里建立外部连接。
candidate := &Client{Address: address}
actual, loaded := clients.LoadOrStore(address, candidate)
// loaded 为 true 时必须使用缓存中的 actual,而不是丢弃它。
client, ok := actual.(*Client)
if !ok {
return nil, fmt.Errorf("cache value for %q has unexpected type", address)
}
if loaded {
return client, nil
}
// 只有 loaded 为 false 的调用负责首次登记后的初始化动作。
// 如果初始化可能失败,应配合失败清理或单独的状态对象处理。
return client, nil
}
这里有一个很实际的取舍:LoadOrStore 能保证“哪个值进入 Map”的原子性,但不会自动回滚初始化失败,也不会把“正在初始化”和“初始化完成”区分开。如果初始化失败后不能保留半成品,就需要把状态字段、错误值或一个可等待的 promise 放进 Map,而不是只存一个裸指针。
几个容易把结果读反的边界
- 不要把 loaded 当成成功标志。 两条路径都可能正常返回,
loaded只说明最终值来自已有条目还是本次存入。 - 不要忽略 actual。 当
loaded == true时,真正应该交给调用方的是 actual,而不是本地候选值。 - nil 也要结合类型设计。 如果存入的是接口包装后的 nil 指针,Map 里仍可能存在这个 key;读取后再做类型断言和空指针判断。
- Delete 会开启下一轮竞争。 删除 key 后,下一次 LoadOrStore 可能再次出现
loaded == false,因此初始化代码必须允许生命周期重复发生。 - 它不是普通 map 的通用替代品。 官方文档建议只有在写入一次多次读取,或不同 key 被不同 goroutine 读写等场景中考虑 sync.Map;需要强类型和多个字段一起维护时,普通 map 配合锁通常更清楚。
我的结论:先问“谁拥有最终值”
现在遇到 LoadOrStore,我会先看两个问题:最终要使用哪个对象,以及这次调用是否拥有初始化责任。答案分别由 actual 和 loaded 给出:actual 是最终对象,loaded == false 才代表本次调用完成了首次存入。
如果只是做并发去重或缓存占位,这个 API 很顺手;如果还要处理初始化失败、重试、过期和关闭,就把这些状态显式建模,再让 Map 保存状态对象。这样既保留原子占位,也不会把“已写入”误认为“已完成初始化”。
相关问题
LoadOrStore 会覆盖已经存在的值吗?
不会。key 已存在时返回旧值并把 loaded 设为 true,传入值不会替换旧值。
为什么不能只调用 Load 再决定是否 Store?
因为两个调用之间存在竞争窗口,多个 goroutine 可能同时看到 key 不存在。LoadOrStore 把判断和写入合并成一次原子操作。
LoadOrStore 能保证初始化函数只执行一次吗?
不能直接保证。它只保证最终存入哪个值;如果候选值的构造本身很昂贵,应把初始化状态或一次性执行器作为值来管理。
-
288 收藏
-
232 收藏
-
261 收藏
-
501 收藏
-
256 收藏
-
338 收藏
-
157 收藏
-
394 收藏
-
453 收藏
-
173 收藏
-
423 收藏
-
Golang · Go教程 | 11小时前 | 基准测试 · testing · 性能 · Go教程 · GOMAXPROCS RunParallel Go testing.B.SetParallelism 并行基准 IO吞吐404 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习