golang 基礎之 errgroup
Golang 的擴展併發庫 golang.org/x/sync/errgroup
提供了對多協程任務進行管理和錯誤處理的便利功能。
與基礎的 sync.WaitGroup
相比,errgroup.Group
在等待所有任務完成的同時,還會自動捕獲第一個非 nil 錯誤並返回。如果通過 WithContext
創建 Group
,當任一子任務返回錯誤時,errgroup
會取消關聯的 Context
,從而通知其他協程提前退出。
簡言之,errgroup
封裝了錯誤傳播、上下文取消和併發控制等功能,使併發編程更加簡潔易用。
基本用法
使用 errgroup
時,首先需要創建一個Group
實例。可以直接用零值初始化:var g errgroup.Group
,或者調用errgroup.WithContext(ctx)
同時獲取一個基於ctx
派生的新Context
。典型的用法是對每個併發任務調用g.Go(func() error)
來啓動 goroutine。Group.Go
方法內部會自動執行WaitGroup.Add(1)
,並在函數返回時執行Done()
。最後使用g.Wait()
等待所有任務完成:若有任務返回了錯誤,Wait
會返回該第一個非 nil 錯誤,否則返回 nil。廢話不多說,先滿上:
import (
"context"
"fmt"
"net/http"
"golang.org/x/sync/errgroup"
"io/ioutil"
)
func main() {
// 創建帶取消功能的 errgroup 和 Context
g, ctx := errgroup.WithContext(context.Background())
urls := []string{
"https://example.com",
"https://example.org",
"https://example.net",
}
// 用於收集響應結果的切片
results := make([]string, len(urls))
for i, url := range urls {
i, url := i, url // 捕獲循環變量
g.Go(func() error {
// 在請求中使用 errgroup 的 Context,以支持取消
req, _ := http.NewRequest("GET", url, nil)
req = req.WithContext(ctx)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
data, _ := ioutil.ReadAll(resp.Body)
results[i] = string(data)
return nil
})
}
// 等待所有併發任務完成
if err := g.Wait(); err != nil {
fmt.Println("執行過程中發生錯誤:", err)
return
}
fmt.Println("所有請求完成,結果:", results)
}
此外,errgroup.Group
提供了併發限制的功能。可以使用 g.SetLimit(n)
設置最多同時運行的協程數,配合 g.TryGo(f)
在不超限時才啓動任務。像這樣:
var g errgroup.Group
g.SetLimit(5) // 限制最多 5 個併發 goroutine
for i := 0; i < 10; i++ {
g.TryGo(func() error {
// 模擬任務
fmt.Println("任務執行")
return nil
})
}
if err := g.Wait(); err != nil {
fmt.Println("執行出錯:", err)
} else {
fmt.Println("所有任務成功完成")
}
示例
-
併發執行多個 HTTP 請求並收集結果: 在多個 API 請求或微服務調用場景下,可以用
errgroup
併發發送請求,並在所有請求完成後統一處理結果。例如上面的代碼示例即同時請求多個 URL,並把結果保存在results
切片中。如果任意一個請求出錯,Wait()
會返回錯誤(並在使用WithContext
時取消其他請求)。 -
遇到錯誤立即取消: 如果需要在一旦發現錯誤就中止其他任務的執行,可通過
WithContext
創建帶取消信號的組。在下面示例中,第一個任務在 2 秒後返回錯誤,errgroup
會取消關聯的ctx
,使第二個任務立刻退出等待並返回ctx.Err()
: -
🌰下酒:
import ( "context" "fmt" "time" "golang.org/x/sync/errgroup" ) func main() { g, ctx := errgroup.WithContext(context.Background()) // 第一個協程 2s 後返回錯誤 g.Go(func() error { time.Sleep(2 * time.Second) return fmt.Errorf("第一個任務失敗") }) // 第二個協程檢查 ctx.Done() g.Go(func() error { select { case <-ctx.Done(): fmt.Println("第二個任務被取消:", ctx.Err()) return ctx.Err() case <-time.After(3 * time.Second): fmt.Println("第二個任務完成") return nil } }) if err := g.Wait(); err != nil { fmt.Println("errgroup 返回錯誤:", err) } }
運行結果中會看到 “第二個任務被取消” 和錯誤信息。通過
WithContext
,一旦有任務返回非 nil 錯誤,關聯的Context
會立即被取消。 -
超時控制和上下文取消:
errgroup
常與Context
組合使用來實現超時或取消。例如,如果將父上下文設置超時,當時間到達限制後,所有正在等待的協程會收到取消信號。下面示例爲兩個任務設置 2 秒超時,其中一個任務故意睡眠 3 秒,會被取消: -
talk is cheap,show me the code ,🌰如下
import ( "context" "fmt" "time" "golang.org/x/sync/errgroup" ) func main() { baseCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() g, ctx := errgroup.WithContext(baseCtx) // 長任務:需 3 秒 g.Go(func() error { time.Sleep(3 * time.Second) return nil }) // 觀察超時信號 g.Go(func() error { select { case <-ctx.Done(): fmt.Println("任務取消:", ctx.Err()) return ctx.Err() } }) if err := g.Wait(); err != nil { fmt.Println("errgroup 返回:", err) } }
當超時觸發時,第二個任務檢測到
ctx.Done()
並返回context.DeadlineExceeded
。使用WithContext
和超時上下文可以很方便地控制整個任務組的執行期限。
常見錯誤
-
閉包變量捕獲:
在
for
循環中直接使用迭代變量啓動 goroutine 時,會導致閉包捕獲同一個變量,結果通常不符合預期。正確做法是循環內部重新聲明變量(例如v := v
)或作爲函數參數傳入,避免所有協程共享同一個循環變量。 -
鍛鍊身體,舉個🌰:
for _, url := range urls { url := url // 正確捕獲 g.Go(func() error { // 使用 url return nil }) }
-
Context 使用誤區:
當用
errgroup.WithContext(ctx)
創建組時,不需要也不應再對同一個ctx
額外調用context.WithCancel
等,否則可能導致取消信號影響到外層邏輯。正確的做法是直接傳入已有的上下文,WithContext
會返回派生的新Context
和關聯的取消函數。 -
SetLimit 使用不當導致死鎖:
Group.SetLimit(n)
限制的是組中 持有 協程的數量(包括正在運行和在隊列等待的),誤用場景下容易出現循環依賴而死鎖。尤其當在同一個
errgroup
中嵌套創建更多任務或組時,很可能死鎖。最保險的方式是避免嵌套使用同一個errgroup
實例。另外,將SetLimit(0)
視爲無效設置,會直接導致死鎖,因爲組裏無法再啓動任何協程。因此應謹慎使用併發限制,並確保不會使 goroutine 無法繼續執行。 -
錯誤返回策略:
errgroup.Group
只返回第一個發生的非 nil 錯誤,後續出現的錯誤會被丟棄。同時,
Group.Wait
會調用sync.WaitGroup.Wait
,在返回前會 等待所有 子任務退出。這意味着即使遇到錯誤,其他未完成的任務也會繼續運行(直到收到取消信號退出)。因此在實際使用時,需要對返回的錯誤進行及時檢查,並在必要時主動響應取消信號,讓 goroutine 提前結束。
errgroup
vs sync.WaitGroup
的區別
-
錯誤處理:
sync.WaitGroup
只能等待所有協程完成,不會傳遞錯誤;而
errgroup.Group
在調用Wait()
時會返回第一個非 nil 錯誤。這樣可以方便地在有任何子任務出錯時處理錯誤而不必自行同步。 -
取消支持:
errgroup
集成了
context.Context
,可以在子任務出錯時自動取消其他任務;而sync.WaitGroup
不涉及上下文和取消邏輯。藉助WithContext
,errgroup
能讓併發任務響應父上下文的取消信號,避免浪費資源。 -
併發控制:
errgroup
提供了
SetLimit
和TryGo
等方法,可限制併發 goroutine 的數量。sync.WaitGroup
則只能由用戶自行Add
來記錄數量,沒有併發池或信號量概念。 -
使用便捷性:
與
sync.WaitGroup
需要手動調用Add
/Done
不同,errgroup.Group
封裝了這些操作,只需調用Go
即可啓動任務。此外,errgroup
還可自動捕獲子任務panic
(將其封裝爲PanicError
拋出),提高了併發代碼的魯棒性。
應用
在真實項目中,errgroup
常用於在業務邏輯中並行執行若干相互獨立的子任務。
例如,在一個 HTTP 服務的處理函數中,可能需要並行調用多個後端服務或查詢不同數據源,然後整合結果返回給客戶端。
可以這樣組織代碼:
在請求對應的 Context
上創建一個 errgroup
,爲每個獨立任務調用 g.Go()
,並在最後通過 g.Wait()
收集結果或錯誤。如果某一任務出錯,關聯的 Context
會自動取消,其它任務會及時退出,從而避免不必要的工作。
舉個🌰:
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
g, ctx := errgroup.WithContext(ctx)
var resA, resB string
// 並行執行兩個調用
g.Go(func() error {
data, err := serviceA(ctx, ...)
if err == nil {
resA = data
}
return err
})
g.Go(func() error {
data, err := serviceB(ctx, ...)
if err == nil {
resB = data
}
return err
})
if err := g.Wait(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "Results: %s, %s", resA, resB)
}
總的來說,errgroup
使得 Go 併發編程更易於編寫健壯的代碼,在出現錯誤時能夠優雅地停止不必要的工作。
注意事項
-
結合
Context
使用:通常通過
g, ctx := errgroup.WithContext(ctx)
來創建任務組,確保在某個子任務出錯或超時後能夠取消其他協程。在每個子任務中,應監聽ctx.Done()
並及時返回,以響應取消信號。 -
捕獲循環變量:
在循環中啓動協程時,務必使用
v := v
或函數參數的方式複製循環變量,避免閉包捕獲陷阱。這能確保每個 goroutine 擁有正確的參數值。 -
及時處理取消信號:
在子任務函數內,最好在合適位置檢查並響應
ctx.Err()
或<-ctx.Done()
,以便在外部取消後能儘快退出,釋放資源。例如,如果任務中有 I/O 操作,可以傳遞ctx
給相關 API 來觸發取消。 -
錯誤檢查與日誌:
調用
g.Wait()
後,應該檢查返回的錯誤並記錄或上報。如果只關注第一個錯誤,就要明確日誌體現是哪一個任務失敗;如果需要所有錯誤信息,則可考慮自行在各個 goroutine 中彙集。總之,不要忽略Wait()
的返回值。 -
不要重複創建
errgroup
實例:每個任務組只應使用一個
errgroup.Group
實例,避免嵌套使用同一個組。若需要多個獨立的併發組合任務,應創建多個組實例,以免發生資源衝突或死鎖。 -
代碼風格:
建議將每個併發子任務封裝爲一個返回
error
的函數,使g.Go()
調用清晰簡潔。
標題:errgroup 詳解
作者:mooncakeee
地址:http://blog.dd95828.com/articles/2025/05/20/1747701267973.html
聯繫:scotttu@163.com
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Ob9ZWC8j0_MpDy5nQSM7-Q