訓練營內部有位學員問:"goroutine和Channel我都搞懂了,但為啥有的例子要加鎖,有的又不用?那個for range在Channel裏到底是啥作用?" 這問題問到了點上,今天咱們就掰開揉碎聊聊。
先説説他卡在哪
概括下來就三個迷糊點:
- 會用sync.WaitGroup,但不清楚啥時候必須用,啥時候只是"保險起見"
- 知道有緩衝無緩衝Channel的區別,但看到for range跟Channel混用就懵,更鬧不明白為啥求和還要加鎖
- for range在切片和Channel裏表現完全兩樣,這個語法糖到底甜在哪?
鎖到底啥時候用?兩個場景一看就懂
場景一:搶火車票——不加鎖就等着超賣
想象就100張票,1000個人同時開搶。核心代碼就這麼幾行:
ticketCount := 100
// 1000個goroutine同時跑:
if ticketCount > 0 {
ticketCount-- // 如果不加鎖,這裏會亂成一鍋粥
}
坑在哪:判斷庫存和減庫存是兩步,中間會被打斷。A看到還剩1張,剛準備扣減,B也看到了那1張,結果兩人都能買,票就變成-1張。鎖的作用就是把這兩步焊死,變成"原子操作,一次只能進一個goroutine。
場景二:並行求和——你以為沒事,其實丟了數據
sum := 0
for _, 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的味。