Skip to content

for range和锁,终于悟了

训练营内部有位学员问:"goroutine和Channel我都搞懂了,但为啥有的例子要加锁,有的又不用?那个for range在Channel里到底是啥作用?" 这问题问到了点上,今天咱们就掰开揉碎聊聊。

先说说他卡在哪

概括下来就三个迷糊点:

  1. 会用sync.WaitGroup,但不清楚啥时候必须用,啥时候只是"保险起见"
  2. 知道有缓冲无缓冲Channel的区别,但看到for range跟Channel混用就懵,更闹不明白为啥求和还要加锁
  3. for range在切片和Channel里表现完全两样,这个语法糖到底甜在哪?

锁到底啥时候用?两个场景一看就懂

场景一:抢火车票——不加锁就等着超卖

想象就100张票,1000个人同时开抢。核心代码就这么几行:

go
ticketCount := 100   // 1000个goroutine同时跑: if ticketCount > 0 {     ticketCount--  // 如果不加锁,这里会乱成一锅粥 }

坑在哪:判断库存和减库存是两步,中间会被打断。A看到还剩1张,刚准备扣减,B也看到了那1张,结果两人都能买,票就变成-1张。锁的作用就是把这两步焊死,变成"原子操作,一次只能进一个goroutine。

场景二:并行求和——你以为没事,其实丢了数据

go
sum := 0 for _, num := range numbers {     go func(n int) {         sum += n  // 这儿不加锁,结果准不准全凭运气     }(num) }

坑在哪:这不是扣减固定资源,但sum += n本质上是三步:读sum → 做加法 → 写回sum。两个goroutine可能同时读到100,都加了5,最后写回105,但正确结果应该是110。这就是"数据竞争"——不是资源不够,是更新被覆盖了

更地道的写法:用Channel干掉锁

Go的哲学是"别通过共享内存通信,用通信替代共享内存"。改造后的代码