你是否遇到过这样的情况:
这些问题的根本原因通常是 Goroutines 中的错误处理缺失。如果没有正确捕获和处理错误,Go 的并发机制可能会让 Bug 变得难以察觉,甚至导致程序崩溃。本文将深入剖析 Goroutines 中的错误处理,提供最佳实践,帮助你打造更健壮的 Go 并发代码。
在 Go 语言中,Goroutines 运行在独立的执行流中,一旦发生错误,默认情况下:
panic
并未被捕获。return
机制来直接返回错误给调用者。因此,Go 并发编程中,一个核心问题是:如何确保 Goroutine 发生错误时,我们能及时发现并正确处理?
sync.WaitGroup
结合 channel
传递错误sync.WaitGroup
可用于等待多个 Goroutine 完成,而 channel
可以用来收集错误信息。
package main
import (
"errors"
"fmt"
"sync"
)
func worker(id int, errCh chan<- error, wg *sync.WaitGroup) {
defer wg.Done()
if id%2 == 0 {
errCh <- fmt.Errorf("worker %d failed", id)
return
}
fmt.Printf("worker %d completed successfully\n", id)
}
func main() {
var wg sync.WaitGroup
errCh := make(chan error, 5)
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, errCh, &wg)
}
wg.Wait()
close(errCh)
for err := range errCh {
fmt.Println("Error:", err)
}
}
✅ 优势:所有 Goroutine 运行完毕后,可以一次性处理所有错误,防止遗漏。
在实际开发中,我们可以利用这种方法 收集所有 Goroutine 运行中的错误,然后记录到日志系统中,确保问题能被及时发现。
package main
import (
"log"
"os"
"sync"
)
func main() {
logFile, _ := os.OpenFile("errors.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
logger := log.New(logFile, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
var wg sync.WaitGroup
errCh := make(chan error, 10)
for i := 1; i <= 5; i++ {
wg.Add(1)
gofunc(id int) {
defer wg.Done()
if id%2 == 0 {
errCh <- fmt.Errorf("task %d encountered an issue", id)
}
}(i)
}
wg.Wait()
close(errCh)
for err := range errCh {
logger.Println(err)
}
}
context.WithCancel
控制 Goroutine 退出在多个 Goroutine 执行时,如果一个 Goroutine 发生错误,我们可能希望取消所有其他 Goroutine。
package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, id int, wg *sync.WaitGroup, errCh chan<- error) {
defer wg.Done()
select {
case <-time.After(time.Duration(id) * time.Second):
if id == 2 {
errCh <- fmt.Errorf("worker %d failed", id)
}
case <-ctx.Done():
fmt.Printf("Worker %d canceled\n", id)
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
errCh := make(chan error, 1)
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(ctx, i, &wg, errCh)
}
if err := <-errCh; err != nil {
fmt.Println("Error occurred:", err)
cancel()
}
wg.Wait()
}
✅ 优势:一旦检测到错误,立刻取消所有 Goroutine,防止不必要的资源消耗。
在实际业务中,如果有多个 Goroutine 负责并发 HTTP 请求,我们可以在某个请求失败后,立即取消所有其他请求,避免浪费资源。
package main
import (
"context"
"net/http"
"sync"
)
func fetchURL(ctx context.Context, url string, wg *sync.WaitGroup, client *http.Client) {
defer wg.Done()
req, _ := http.NewRequest("GET", url, nil)
req = req.WithContext(ctx)
_, err := client.Do(req)
if err != nil {
fmt.Println("Request failed:", err)
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
client := &http.Client{}
urls := []string{"https://example.com", "https://google.com", "https://github.com"}
for _, url := range urls {
wg.Add(1)
go fetchURL(ctx, url, &wg, client)
}
// 取消所有请求
cancel()
wg.Wait()
}
这些代码示例和实战应用能够帮助你更深入地理解如何正确处理 Go 并发中的错误。🚀