
上一篇主要整理了 Go 中函數、struct、指針和 interface 的基礎用法。這一篇繼續記錄 Go 中非常有特色的一部分并發編程。本篇主要整理目前已經學習過的幾個知識點goroutine channel 緩沖 channel close WaitGroup defer Mutex select timeout這些內容以后在 Go 后端、網絡編程以及并發任務處理中都會經常遇到。一、goroutine 是什么goroutine 可以先簡單理解成讓一個函數并發執行。普通調用func work() { fmt.Println(working) } func main() { work() }這里work()會按照正常順序執行。如果加上go work()就表示啟動一個新的 goroutine 來執行work()。例如package main import fmt func work() { fmt.Println(working) } func main() { go work() }需要注意如果main()太快結束新的 goroutine 可能還沒來得及執行整個程序就已經退出了。所以 goroutine 啟動之后通常還需要配合其他機制進行等待或同步。二、channel 是什么channel 可以理解成goroutine 之間傳遞數據的通道。創建一個傳遞整數的 channelch : make(chan int)發送數據ch - 10接收數據x : -ch例如package main import fmt func main() { ch : make(chan int) go func() { ch - 20 }() x : -ch fmt.Println(x) }輸出20這里ch - 20表示把20放進 channel。而x : -ch表示從 channel 取出一個值。三、channel 還可以用于等待如果x : -ch執行時 channel 里暫時沒有數據那么當前 goroutine 會在這里等待。直到另一個 goroutine執行ch - 100程序才會繼續。所以 channel 不僅能傳遞數據還能起到一定的同步作用。四、無緩沖 channel下面這種ch : make(chan int)叫做無緩沖 channel。它的特點是發送和接收通常需要配合進行。例如ch : make(chan int) ch - 10如果沒有其他 goroutine 來接收數據那么ch - 10可能會一直阻塞。正確示例ch : make(chan int) go func() { ch - 10 }() fmt.Println(-ch)輸出10五、有緩沖 channel還可以創建有緩沖的 channelch : make(chan int, 2)這里的2表示 channel 最多可以暫時存放 2 個整數。例如ch : make(chan int, 2) ch - 10 ch - 20 fmt.Println(-ch) fmt.Println(-ch)輸出10 20可以先理解為[10, 20]先放進去的值先被取出來。如果緩沖區已經滿了再繼續發送數據就會阻塞直到有人先取走數據。六、關閉 channel可以使用close(ch)關閉 channel。需要注意關閉 channel 并不會立即清空里面已經存在的數據。例如ch : make(chan int, 2) ch - 10 ch - 20 close(ch) fmt.Println(-ch) fmt.Println(-ch)仍然可以得到10 20close(ch)更準確的意思是告訴接收方以后不會再發送新的數據了。七、判斷 channel 是否已經讀取完可以這樣接收value, ok : -ch其中value - 讀到的數據 ok - 是否還能正常讀取例如ch : make(chan int, 1) ch - 8 close(ch) value, ok : -ch此時value 8 ok true如果數據已經全部讀取完再讀取一次value 0 ok false八、使用 range 讀取 channelchannel 也可以配合rangefor value : range ch { fmt.Println(value) }例如ch : make(chan int, 3) ch - 1 ch - 2 ch - 3 close(ch) for value : range ch { fmt.Println(value) }輸出1 2 3這個循環會一直讀取直到channel 已經關閉并且里面的數據全部讀取完。所以如果使用for value : range ch一般要注意 channel 是否會被關閉否則循環可能一直等待。九、WaitGroup 是什么如果啟動了多個 goroutine有時希望主程序等它們全部執行結束。這時候可以使用sync.WaitGroup例如var wg sync.WaitGroup常見三個操作wg.Add(1) wg.Done() wg.Wait()可以理解為Add(n) - 登記 n 個任務 Done() - 當前任務完成 Wait() - 等待所有任務完成例如package main import ( fmt sync ) func main() { var wg sync.WaitGroup wg.Add(2) go func() { fmt.Println(任務1) wg.Done() }() go func() { fmt.Println(任務2) wg.Done() }() wg.Wait() fmt.Println(全部完成) }任務1和任務2的先后順序不一定固定。但全部完成一定會在兩個任務都結束之后輸出。十、defer 的基礎用法defer可以先理解成把某個操作延遲到當前函數結束之前執行。例如func test() { defer fmt.Println(B) fmt.Println(A) }輸出A B因為defer fmt.Println(B)不會馬上執行而是在函數結束之前執行。十一、多個 defer 的執行順序如果有多個deferfunc test() { defer fmt.Println(A) defer fmt.Println(B) defer fmt.Println(C) fmt.Println(D) }輸出D C B A多個defer是后進先出。也就是最后登記的defer最先執行。十二、defer 和 WaitGroup 配合實際代碼里常寫go func() { defer wg.Done() fmt.Println(working) }()這樣可以保證當前 goroutine 結束時執行wg.Done()相比把wg.Done()手動放在函數最后更不容易忘記。十三、Mutex 是什么多個 goroutine 同時修改同一個變量時可能出現數據競爭。例如count看起來只有一行但實際可以理解成讀取 count 加 1 寫回 count如果多個 goroutine 同時進行就可能導致結果不準確。這時候可以使用sync.Mutex也就是互斥鎖。基本寫法mu.Lock() count mu.Unlock()可以理解為Lock() - 我現在開始修改其他 goroutine 先等一下 Unlock() - 我修改完成了其他 goroutine 可以繼續十四、WaitGroup 和 Mutex 綜合使用例如啟動 5 個 goroutine每個都讓count加 1package main import ( fmt sync ) func main() { var wg sync.WaitGroup var mu sync.Mutex count : 0 for i : 0; i 5; i { wg.Add(1) go func() { defer wg.Done() mu.Lock() count mu.Unlock() }() } wg.Wait() fmt.Println(count) }最終輸出5這里WaitGroup負責等待 5 個 goroutine 全部完成。而Mutex負責保證count不會被多個 goroutine 同時修改。十五、goroutine 把結果通過 channel 返回例如func square(n int, ch chan int) { ch - n * n }調用ch : make(chan int) go square(5, ch) result : -ch fmt.Println(result)輸出25這里square()在新的 goroutine 中進行計算。然后通過ch - n * n把結果傳回主 goroutine。十六、并發任務完成順序不固定例如go square(2, ch) go square(3, ch) a : -ch b : -ch最終得到的兩個結果一定是4 9但a 4, b 9和a 9, b 4都有可能。原因是goroutine 的啟動順序不代表它們一定按照這個順序完成。這是學習并發時比較重要的一點。十七、select 是什么select用來處理多個 channel。例如select { case msg1 : -ch1: fmt.Println(msg1) case msg2 : -ch2: fmt.Println(msg2) }可以理解為同時等待多個 channel哪個先準備好就先處理哪個。如果ch1先有數據就執行第一個case。如果ch2先有數據就執行第二個。十八、select 中的 default例如select { case value : -ch: fmt.Println(value) default: fmt.Println(no data) }如果當前 channel 沒有數據那么不會一直等待而是直接執行no data所以default可以理解為如果當前沒有任何 case 可以立即執行就執行 default。需要注意default不是最后一定會執行。只要有其他case可以執行就不會進入default。十九、select timeout 超時控制實際開發中經常不能一直等待某個任務。這時可以使用time.After()例如select { case msg : -ch: fmt.Println(msg) case -time.After(2 * time.Second): fmt.Println(timeout) }意思是等待 channel 的數據但最多等待 2 秒。如果 2 秒內收到數據就處理數據。如果 2 秒仍然沒有收到就輸出timeout二十、超時控制完整例子package main import ( fmt time ) func worker(ch chan string) { time.Sleep(1 * time.Second) ch - 任務完成 } func main() { ch : make(chan string) go worker(ch) select { case msg : -ch: fmt.Println(msg) case -time.After(2 * time.Second): fmt.Println(timeout) } }因為任務只需要等待 1 秒而超時時間是 2 秒所以最終輸出任務完成如果把time.Sleep(1 * time.Second)改成time.Sleep(3 * time.Second)那么超時時間會先到最終輸出timeout二十一、目前容易混淆的幾個地方1. goroutine 和 channel 不是同一個東西goroutine 是執行任務的并發單元。channel 是goroutine 之間傳遞數據的通道。可以簡單理解成goroutine - 干活的人 channel - 傳遞數據的通道2. goroutine 不是進程一開始容易把go func()理解成“創建新進程”。實際上更準確的說法是啟動新的 goroutine。goroutine 是 Go 運行時管理的輕量級并發任務并不是操作系統進程。3. 啟動順序不等于完成順序例如go task1() go task2()并不能保證task1 一定先完成 task2 一定后完成這也是并發程序需要特別注意的地方。4. WaitGroup 和 Mutex 作用不同WaitGroup等待多個 goroutine 全部完成。Mutex防止多個 goroutine 同時修改共享數據。它們解決的是兩個不同的問題。二十二、學習總結這一階段主要學習了 Go 的并發基礎。目前我對幾個核心概念的理解是goroutine - 并發執行任務channel - goroutine 之間傳遞數據WaitGroup - 等待多個 goroutine 完成Mutex - 保護共享數據避免數據競爭defer - 函數結束前執行指定操作select - 同時處理多個 channeltime.After - 實現簡單的超時控制Go 的并發部分比前面的基礎語法更抽象目前還只是掌握了基本概念和簡單用法。后面還需要通過實際項目繼續鞏固。下一階段準備開始學習 Go 的工程基礎包括package go mod 項目目錄結構 JSON 文件讀寫 go test之后再進入 HTTP、Gin 和 MySQL開始真正編寫 Go 后端項目。