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的哲学是"别通过共享内存通信,用通信替代共享内存"。改造后的代码:

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算一块,最后合并
  • 只读不写:大家都只读,没人改,安全得很

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

go 复制代码
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的味。

相关推荐
梦未18 小时前
Spring控制反转与依赖注入
java·后端·spring
无限大618 小时前
验证码对抗史
后端
用户21903265273519 小时前
Java后端必须的Docker 部署 Redis 集群完整指南
linux·后端
VX:Fegn089519 小时前
计算机毕业设计|基于springboot + vue音乐管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计
bcbnb19 小时前
苹果手机iOS应用管理全指南与隐藏功能详解
后端
用户479492835691519 小时前
面试官:DNS 解析过程你能说清吗?DNS 解析全流程深度剖析
前端·后端·面试
幌才_loong19 小时前
.NET8 实时通信秘籍:WebSocket 全双工通信 + 分布式推送,代码实操全解析
后端·.net
开心猴爷19 小时前
iOS应用发布:App Store上架完整步骤与销售范围管理
后端
JSON_L19 小时前
Fastadmin API接口实现多语言提示语
后端·php·fastadmin