方法1
代碼
package mainimport ("fmt""sync""time"
)func main() {allChan := make(chan interface{}, 3)var sendWg, recvWg sync.WaitGroup // 分別同步發送和接收// 發送goroutinesendWg.Add(1)go func() {defer sendWg.Done()for i := 0; i < 10; i++ {time.Sleep(time.Millisecond * 10)allChan <- ifmt.Printf("發送: %d\n", i)}close(allChan) // 發送完成后關閉channel}()// 接收goroutinerecvWg.Add(1)go func() {defer recvWg.Done()for item := range allChan {time.Sleep(time.Millisecond * 20) // 模擬處理耗時fmt.Printf("接收: %v\n", item)}}()// 先等待發送完成,再等待接收完成sendWg.Wait()recvWg.Wait()fmt.Println("所有操作完成")
}
運行結果:
標準輸出:發送: 0
發送: 1
接收: 0
發送: 2
發送: 3
接收: 1
發送: 4
發送: 5
接收: 2
發送: 6
接收: 3
發送: 7
接收: 4
發送: 8
接收: 5
發送: 9
接收: 6
接收: 7
接收: 8
接收: 9
所有操作完成
分析
優化說明
- 雙重同步:
- 使用兩個WaitGroup(sendWg和recvWg),分別等待發送和接收 goroutine 完成。
- 先通過sendWg.Wait()確保所有數據發送完畢并關閉 channel;
- 再通過recvWg.Wait()確保接收 goroutine 處理完所有數據。
- channel 關閉邏輯:
- 發送完成后關閉 channel 是關鍵,這會讓接收 goroutine 的range循環在取完數據后自動退出,避免接收端阻塞。
- 適用場景:
- 這種 “發送 + 接收都異步” 的模式適合需要主程序同時處理其他邏輯的場景(例如同時監控多個任務),但必須做好同步,否則會出現數據丟失。
方法2
代碼
package mainimport ("fmt""time"
)func main() {// 1. 數據通道:傳遞實際數據dataChan := make(chan interface{}, 3)// 2. 發送完成信號通道:發送方結束后通知sendDone := make(chan struct{})// 3. 接收完成信號通道:接收方結束后通知recvDone := make(chan struct{})// 發送goroutinego func() {for i := 0; i < 10; i++ {time.Sleep(time.Millisecond * 10)dataChan <- ifmt.Printf("發送: %d\n", i)}close(dataChan) // 發送完畢,關閉數據通道(通知接收方結束)sendDone <- struct{}{} // 發送完成信號}()// 接收goroutinego func() {for item := range dataChan { // 當dataChan關閉且數據取完后,循環自動退出time.Sleep(time.Millisecond * 20)fmt.Printf("接收: %v\n", item)}recvDone <- struct{}{} // 接收完成信號}()// 主程序等待:先等發送完成,再等接收完成<-sendDone // 阻塞等待發送完成<-recvDone // 阻塞等待接收完成fmt.Println("所有操作完成")
}
運行結果
標準輸出:發送: 0
發送: 1
接收: 0
發送: 2
發送: 3
接收: 1
發送: 4
發送: 5
接收: 2
發送: 6
接收: 3
發送: 7
接收: 4
發送: 8
接收: 5
發送: 9
接收: 6
接收: 7
接收: 8
接收: 9
所有操作完成
那種更好呢
一、效率對比
sync.WaitGroup 略占優勢
- sync.WaitGroup 是 Go 標準庫專門為 “等待一組 goroutine 完成” 設計的同步原語,底層通過原子操作(計數器加減)實現,幾乎沒有額外開銷,執行效率極高。
- 它的邏輯簡單:Add 增加計數、Done 減少計數、Wait 阻塞直到計數歸零,整個過程不涉及復雜的 runtime 調度交互。
多通道同步的開銷稍高 - 通道同步依賴于 channel 的發送 / 接收操作,這些操作需要與 Go runtime 的調度器交互(如檢查通道狀態、喚醒阻塞的 goroutine 等),理論上比原子操作多一些微小的開銷。
- 額外的信號通道(如 sendDone、recvDone)會占用少量內存(每個通道需要維護內部數據結構),但在實際應用中影響可忽略。
二、實用性與適用場景
sync.WaitGroup 更適合大多數場景
- 代碼簡潔:對于 “等待多個 goroutine 完成” 的場景,WaitGroup 的語義更直觀(Add/Done/Wait 直接對應 “注冊 - 完成 - 等待” 邏輯),可讀性更高。
- 擴展性好:如果需要等待多個發送者或接收者(如 10 個發送 goroutine),WaitGroup 只需一次 Add(10) 即可,無需額外定義多個信號通道。
- 通用性強:是 Go 社區的 “標準做法”,團隊協作時更容易被理解。
多通道同步適合特定場景 - 符合 Go 哲學:更貼合 “用通信實現共享內存” 的 Go 設計思想,通過 channel 傳遞信號比直接操作計數器更 “Go 式”。
- 靈活擴展:如果需要更復雜的同步邏輯(如 “先等待 A 完成,再啟動 B,最后等待 B 完成”),通道的阻塞特性可以天然實現流程控制,而 WaitGroup 可能需要配合額外邏輯。
- 無依賴:不依賴 sync 包,純靠語言原生特性實現,適合理解 channel 機制的場景。
三、總結建議
優先選 sync.WaitGroup:在大多數業務代碼中,它更簡潔、高效、易維護,是同步 goroutine 的 “標準答案”。
選多通道同步:當你需要強調 “通信優先” 的設計,或同步邏輯較復雜(需要通過通道傳遞更多狀態)時,它是更優雅的選擇。
本質區別:
WaitGroup 是 “共享狀態” 式同步(通過計數器),多通道是 “通信” 式同步(通過信號傳遞)。兩者效率差異微小,選擇時主要看代碼可讀性和場景適配性。