<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Sync.Map on Zampo Blog</title><link>https://blog.cpdd.fyi/tags/sync.map/</link><description>Recent content in Sync.Map on Zampo Blog</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Mon, 13 Jul 2026 14:30:00 +0800</lastBuildDate><atom:link href="https://blog.cpdd.fyi/tags/sync.map/index.xml" rel="self" type="application/rss+xml"/><item><title>你以为 sync.Map 无锁？它的锁藏在三个地方</title><link>https://blog.cpdd.fyi/posts/go-sync-map-mechanism/</link><pubDate>Mon, 13 Jul 2026 14:30:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-sync-map-mechanism/</guid><description>&lt;p&gt;这套对话，我在 code review 里见过很多次。&lt;/p&gt;
&lt;p&gt;A 说：这个 map 读多写少，并发安全得处理一下。
B 说：那把 map + RWMutex 换成 sync.Map 吧，无锁的，更快。&lt;/p&gt;
&lt;p&gt;第一句不完全错，第二句基本错了。&lt;/p&gt;
&lt;p&gt;sync.Map 不是无锁 map。它内部有一把 Mutex，还有一套 read / dirty 双 map 的调度逻辑。它只是努力让你常见的读路径不走锁，但写、删除、miss、dirty 提升这些路径一个都绕不开锁。&lt;/p&gt;
&lt;p&gt;你选 &lt;code&gt;sync.Map&lt;/code&gt;，不是因为它在所有场景下都更快，而是因为你的 workload 正好落在它设计的那条窄路径上。&lt;/p&gt;
&lt;p&gt;但绝大多数人跳过这个前提。&lt;/p&gt;
&lt;h2 id="read-和-dirty一把锁两个桶"&gt;read 和 dirty：一把锁两个桶&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;sync.Map&lt;/code&gt; 的内部结构（Go 1.25.4）可以浓缩成五样东西：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Map&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Mutex&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Pointer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;readOnly&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 无锁读的主流路径&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dirty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 写和 miss 的后备路径&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;misses&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 读 miss 计数&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;其中 &lt;code&gt;readOnly&lt;/code&gt; 又包了一层：&lt;/p&gt;</description></item><item><title>读多写少就用 sync.Map？先问这 5 个问题</title><link>https://blog.cpdd.fyi/posts/go-sync-map-decision/</link><pubDate>Sat, 04 Jul 2026 13:41:44 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-sync-map-decision/</guid><description>&lt;p&gt;线上有一张设备状态表，用 &lt;code&gt;sync.Map&lt;/code&gt; 存着。&lt;/p&gt;
&lt;p&gt;读很多，写很少。平时接口很稳，一到定时刷新那几秒，读 p99 就会抖一下。你看代码，第一反应可能不是怀疑 &lt;code&gt;sync.Map&lt;/code&gt;，而是怀疑刷新逻辑：是不是批量太大？是不是网络抖了？是不是 GC 刚好撞上？&lt;/p&gt;
&lt;p&gt;这种判断很自然。&lt;/p&gt;
&lt;p&gt;因为在不少 Go 后端的直觉里，普通 map 并发不安全，&lt;code&gt;sync.Map&lt;/code&gt; 并发安全；读多写少，正好该用它。这个链条看起来太顺了，顺到没人再问第二个问题：这张 map 只是在存 key/value，还是还牵着过期队列、计数器、最近更新时间和一组业务不变量？&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-sync-map-decision/cover.svg" alt="sync.Map 选型封面"&gt;&lt;/p&gt;
&lt;p&gt;真正的分岔点在这里：&lt;code&gt;sync.Map&lt;/code&gt; 不是普通 map 的并发升级版。它是少数访问模式下的专用优化。&lt;/p&gt;
&lt;p&gt;读多写少只是入口，不是结论。&lt;/p&gt;
&lt;p&gt;真正该问的是：key 稳不稳定？写入是不是一次性的？不同 goroutine 操作的是不是不相交的 key？这个 map 是否需要和其他状态一起保持一致？你是否需要 Range 得到一个稳定快照？&lt;/p&gt;
&lt;p&gt;这些问题不问清楚，把 &lt;code&gt;map+RWMutex&lt;/code&gt; 换成 &lt;code&gt;sync.Map&lt;/code&gt;，经常不是优化，只是把一把看得见的锁，换成了一套更隐蔽的代价。&lt;/p&gt;
&lt;h2 id="官方第一句话就不是用它"&gt;官方第一句话就不是“用它”&lt;/h2&gt;
&lt;p&gt;Go 官方文档对 &lt;code&gt;sync.Map&lt;/code&gt; 的态度很克制。&lt;/p&gt;
&lt;p&gt;它确实说 &lt;code&gt;Map&lt;/code&gt; 像 &lt;code&gt;map[any]any&lt;/code&gt;，可以被多个 goroutine 并发使用，Load、Store、Delete 是摊还常数时间。&lt;/p&gt;
&lt;p&gt;但紧接着那段话更重要：&lt;code&gt;Map&lt;/code&gt; 是 specialized。大多数代码应该使用普通 Go map，再配合单独的锁或协调机制。原因也写得很直：类型安全更好，也更容易维护 map 内容之外的其他不变量。&lt;/p&gt;
&lt;p&gt;这句话经常被忽略。&lt;/p&gt;
&lt;p&gt;因为我们太容易只记住“并发安全”，忘了官方真正给出的优化场景只有两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个 key 的 entry 只写一次，之后读很多次，比如只增长的缓存；&lt;/li&gt;
&lt;li&gt;多个 goroutine 读、写、覆盖的是彼此不相交的 key 集合。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意，不是“所有读多写少”。&lt;/p&gt;</description></item></channel></rss>