rwmutex仅在读占比≥70%时显著优于mutex,读占比

Go 里选同步原语不是看“哪个高级”,而是看“谁在等谁、等什么、等多久”。用错一个,轻则性能掉一半,重则死锁或竞态静默崩溃。
什么时候必须用 chan struct{} 而不是 sync.WaitGroup
核心区别在于:是否需要「精确控制执行顺序」。WaitGroup 只回答“都干完了吗?”,而 channel 能回答“你干到哪一步了?现在轮到我了吗?”
- WaitGroup 适合「批量启动 + 统一收尾」:比如并发拉 10 个 API,不关心谁先谁后,只等全部返回
-
chan struct{}适合「流水线式协作」:比如生产者生成数据后,必须等消费者处理完才能发下一条;或者主 goroutine 必须等子 goroutine 初始化完成再继续 - 常见错误:用 WaitGroup 等待「有状态的中间结果」——它没有信号内容,无法传递“已就绪”“已失败”“超时”等语义
- 性能差异不大,但语义错位会导致后期加日志、加重试、加超时时逻辑爆炸
sync.Mutex 和 sync.RWMutex 的读写比例临界点在哪
没有固定数字,但可以观察两个信号:CPU 使用率突增 + go tool trace 显示大量 goroutine 阻塞在 RUnlock 后,大概率是写锁饥饿了。
- RWMutex 不是“读越多越快”,而是“读远多于写且写不频繁”才值得换
- 典型安全阈值:写操作占比
- 别为了“看起来更专业”强行上 RWMutex:Map 类型自带并发安全问题,
sync.Map在读多场景下通常比sync.RWMutex+ 普通 map 更省心 - 一个容易被忽略的坑:
RUnlock必须和RLock成对,漏掉一次就会永久卡住后续所有写操作
sync.Once 为什么不能替代 init() 或简单 if 判断
因为 sync.Once 保证的是「多次调用只执行一次」,而 init() 是编译期单次执行,if 判断则完全不防并发。
- 适用场景非常窄:仅用于运行时首次触发的、带副作用的初始化,比如加载配置、连接数据库、注册回调
- 常见误用:在 HTTP handler 里用
sync.Once做 per-request 缓存——它会把第一次请求的结果锁死给所有后续请求 - 注意:Once.Do 里的函数 panic 会导致后续所有调用直接 panic,不会重试;如果初始化可能失败,得在外层包一层错误处理
- 性能上几乎无开销,但它解决的是「正确性」问题,不是「效率」问题;别把它当缓存用
channel 关闭后继续接收会怎样,select 怎么避免假唤醒
关闭的 channel 接收会立即返回零值 + false,但如果不检查第二个返回值,就等于默认接受零值——这是最隐蔽的 bug 来源之一。
- 永远用
v, ok := 形式接收,<code>ok == false表示 channel 已关闭且无剩余数据 -
select中多个 case 都 ready 时是伪随机的,不是按书写顺序;如果依赖顺序,必须用 if+channel 检查代替 select - 空
select{}会永久阻塞,select{default:}是非阻塞轮询,但高频轮询等于自造 busy-loop,应配合time.Sleep或time.After - 超时场景别手写 timer 控制,直接用
select { case ,避免 goroutine 泄露
真正难的不是记住每个原语怎么写,而是读懂代码里那句没写出来的潜台词:“这个变量,到底被谁改、被谁读、改和读之间有没有先后约束”。同步原语只是把这句话翻译成 Go 能执行的指令而已。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











