写点什么

for range 和锁,终于悟了

作者:王中阳Go
  • 2025-12-10
    北京
  • 本文字数:1729 字

    阅读完需:约 6 分钟

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

先说说他卡在哪

概括下来就三个迷糊点:


  1. 会用 sync.WaitGroup,但不清楚啥时候必须用,啥时候只是"保险起见"

  2. 知道有缓冲无缓冲 Channel 的区别,但看到 for range 跟 Channel 混用就懵,更闹不明白为啥求和还要加锁

  3. for range 在切片和 Channel 里表现完全两样,这个语法糖到底甜在哪?

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

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

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


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


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

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

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


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

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

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


func sumWithChannel(numbers []int) int {    ch := make(chan int)        for _, num := range numbers {        go func(n int) {            ch <- n  // 各自把结果扔进来,谁也别碰谁的        }(num)    }        sum := 0    for range numbers {  // 收够len(numbers)次就完事        sum += <-ch    }    return sum}
复制代码


关键点:每个 goroutine 只操心自己的数字,主 goroutine 统一汇总。for range 在这里不是遍历切片,而是反复从 Channel 里取值,直到收到指定次数。数据零竞争,代码还清爽。

锁的底线:这三类情况逃不掉

必须用锁的场景:


  • 读写同一个变量:goroutine A 在写,B 要读或写,必须锁

  • 检查再行动:像抢车票,得先判断条件再操作,两步不能拆

  • 多步操作要打包:转账得"扣 A 的钱 + 加 B 的钱",要么全做要么全不做


可以不用锁的替代方案:


  • 各算各的:用 Channel 传结果,别碰共享变量

  • 数据分片:把数组切开,每个 goroutine 算一块,最后合并

  • 只读不写:大家都只读,没人改,安全得很

完整代码对比:一眼看懂差异

package main
import ( "fmt" "sync")
func main() { numbers := []int{1,2,3,4,5,6,7,8,9,10} // 方案一:锁 + WaitGroup(直观但笨重) var mu sync.Mutex sum1, wg := 0, sync.WaitGroup{} for _, n := range numbers { wg.Add(1) go func(x int) { defer wg.Done() mu.Lock() // 进去先上锁 sum1 += x mu.Unlock() // 出来记得开 }(n) } wg.Wait() fmt.Println("加锁求和:", sum1) // 55 // 方案二:Channel(推荐) ch := make(chan int, len(numbers)) for _, n := range numbers { go func(x int) { ch <- x // 只负责发,不用抢 }(n) } sum2 := 0 for i := 0; i < len(numbers); i++ { sum2 += <-ch // 主线程统收 } close(ch) // 好习惯,用完关通道 fmt.Println("Channel求和:", sum2) // 55}
复制代码


for range 的两种面孔


  • for _, v := range numbers:遍历切片,v是元素值

  • for v := range ch:从通道一直读,直到通道关闭且已读空

总结:一个自问就够了

写并发代码时,心里默念: "如果两个 goroutine 同时跑这行代码,会掐架吗?"


  • 会?上锁或改用 Channel

  • 不会?大胆写


记住 Go 的黄金法则:Share memory by communicating, don't communicate by sharing memory. 优先用 Channel 把数据流理清楚,实在理不清再考虑锁。这样写出来的代码,不仅安全,还自带 Go 的味。

发布于: 刚刚阅读数: 3
用户头像

王中阳Go

关注

靠敲代码在北京买房的程序员 2022-10-09 加入

【微信】wangzhongyang1993【公众号】程序员升职加薪之旅【成就】InfoQ专家博主👍掘金签约作者👍B站&掘金&CSDN&思否等全平台账号:王中阳Go

评论

发布
暂无评论
for range和锁,终于悟了_Go_王中阳Go_InfoQ写作社区