当前位置:首页 > 文章列表 > Golang > Go教程 > Go sync.Map.LoadOrStore 的 loaded 为什么要单独判断

Go sync.Map.LoadOrStore 的 loaded 为什么要单独判断

来源:17golang原创 2026-10-13 12:12:33 0浏览 收藏

我在把一个“按 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 只是候选值,并没有覆盖已经存在的内容。

sync.Map LoadOrStore 从空键到已存在键的返回值流程说明图
图1:LoadOrStore 在空键和已存在键两条数据路径上的返回值说明图,不是运行截图。

沿着数据生命周期看三种结果

我后来不再把 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。这也是它适合做一次性占位的原因。

多个 goroutine 竞争同一 sync.Map 键时的候选值与最终值关系图
图2:并发初始化中多个候选值汇聚到一个 actual 的关系图,不是运行截图。

把 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 能保证初始化函数只执行一次吗?

不能直接保证。它只保证最终存入哪个值;如果候选值的构造本身很昂贵,应把初始化状态或一次性执行器作为值来管理。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go http.Request.PathValue 为什么会返回空字符串Go http.Request.PathValue 为什么会返回空字符串
上一篇
Go http.Request.PathValue 为什么会返回空字符串
Go gob 数据对应的结构体增加导出字段后还能不能继续解码
下一篇
Go gob 数据对应的结构体增加导出字段后还能不能继续解码
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    457次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    521次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    536次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    484次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    306次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码