Go 的 map 并发读写,面试里别上来就答「sync.Map」

Go 的 map 并发读写,面试里别上来就答「sync.Map」

这题看起来简单,实际很容易把自己绕进去。

面试官问「Go 的 map 能不能并发访问」,最短答案是:别这么用。Go 官方的 Maps 博客写得很直白,普通 map 不是并发安全的;并发读写时,运行时可能直接报错。

但下一句如果只答「那我用 sync.Map」,通常就露怯了。sync.Map 不是普通 map 的并发版替身,Go 官方文档甚至专门提醒:大多数代码仍应该用普通 map,再配合独立的锁。

我会先反问自己:这块数据到底是在保护什么?

大多数业务代码,map + RWMutex 就够了

比如一个按用户累计次数的计数器。读操作可以并行,写操作要独占,而且 m[id]++ 这种读改写本身就是一个整体,不能拆。

go 复制代码
type Counter struct {
	mu sync.RWMutex
	m  map[string]int
}

func (c *Counter) Add(userID string) {
	c.mu.Lock()
	defer c.mu.Unlock()

	c.m[userID]++
}

func (c *Counter) Get(userID string) int {
	c.mu.RLock()
	defer c.mu.RUnlock()

	return c.m[userID]
}

这里用 RWMutex 的价值不只是「防 panic」。它把业务不变量也锁住了:同一个用户的计数不会因为两个 goroutine 同时加一而丢更新。

有个很常见的误区:觉得读多写少就一定该换 sync.Map。先别急。普通 map 加 RWMutex 的代码更好读,key 和 value 也都有静态类型。锁的边界还能跟着业务走,比如一次校验余额、扣减额度、写审计记录,需要放进同一个临界区。

这类代码我默认就选它。

sync.Map 适合的是两类很具体的访问模式

Go 官方文档给了两个典型场景。

一个是 key 基本只写一次,之后被大量读取。比如按连接 ID 缓存已经建立好的对象。

另一个是多个 goroutine 操作彼此不重叠的 key 集。比如每个 worker 只维护自己那一小撮会话,不会频繁撞到同一把锁上。

go 复制代码
var connections sync.Map // key: string, value: *Client

func saveClient(id string, client *Client) {
	connections.Store(id, client)
}

func findClient(id string) (*Client, bool) {
	value, ok := connections.Load(id)
	if !ok {
		return nil, false
	}

	client, ok := value.(*Client)
	return client, ok
}

它能减少某些读路径上的锁竞争,不过代价也摆在台面上:value 是 any,取值时得做断言;复杂操作的意图不如一把显式锁清楚。

尤其别把下面这种业务逻辑误以为天然安全:

go 复制代码
if _, exists := connections.Load(id); !exists {
	connections.Store(id, client)
}

这不是「不存在才创建」的原子实现。两个 goroutine 可以同时看到不存在,然后都 Store。要么用 LoadOrStore 这种已经定义好原子语义的操作,要么回到 mutex,把校验和写入放在同一个临界区。

Channel 也不是给 map 兜底的

「不要通过共享内存来通信」这句话经常被背成:map 一并发就上 Channel。其实没必要。

如果数据是在 goroutine 之间流动,比如把任务交给某个 worker,Channel 很自然。要是本质上只是很多请求共同维护一份状态,锁往往更直接。为了躲一把 mutex 把所有读写绕到一个管理协程,代码未必更安全,只是把排队的位置换了。

我自己的判断顺序是:

  • 需要保护一份共享状态,先用 map + Mutex/RWMutex。
  • 确实符合一次写入、多次读取,或者 key 集天然不重叠,再考虑 sync.Map。
  • 数据需要在 goroutine 间传递、顺带控制处理顺序时,用 Channel。

面试时把这个选择过程讲出来,比背「sync.Map 读无锁」稳得多。因为追问通常马上就会落到:复合操作怎么办、重复创建怎么办、value 的类型断言怎么办。

参考资料:Go 官方博客《Maps》;Go 标准库 sync 包文档。

相关推荐
考研保研资料分享2 小时前
2022双非保研华科热能工程:三校考核、传热题与报名复盘
面试
weixin_307779138 小时前
RAG 面试陷阱——混合检索并非无条件优越:一个条件反转实验
人工智能·python·面试·职场和发展
nice_lcj52010 小时前
深入理解 Java 反射:从 Class 对象到方法调用(含性能实测 + 迷你 IOC 实战 + 面试考点)
java·开发语言·后端·面试
D e re k ZH13 小时前
C++ 并发、未定义行为与工程化难点(No 3)
数据结构·c++·算法·面试
君万15 小时前
【LeetCode每日一题】回溯:46. 全排列 78. 子集 39. 组合总和
算法·leetcode·go
香蕉大香蕉(06敬畏市场)16 小时前
MySQL 单点详解(08)
java·数据库·mysql·面试·八股
Thanks_ks17 小时前
常见 HTTP 状态码的坑:301/302、401/403、500/502 之间差的那一个词
计算机网络·nginx·http·面试·状态码·后端开发·浏览器缓存
怕浪猫17 小时前
Super-agent 在各大模型厂商的应用情况
算法·面试·github
滼19 小时前
AI 数据安全面试应答框架:从大框架到小点
大数据·人工智能·安全·面试·安全架构
vipxieliang21 小时前
Go 逃逸分析与内存优化:从读懂 -gcflags=“-m“ 到降低 GC 压力
go