You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

规避goroutine泄漏时触发panic:竞态条件问题求助

解决Go中goroutine错误传递的竞态条件问题

我来帮你捋捋这个问题——你启动多个goroutine,用error channel返回错误,父goroutine通过select取第一个错误或等待所有完成,但延迟关闭errc时触发了竞态条件,核心问题其实是关闭channel的时机和goroutine发送操作不同步导致的。

先明确你遇到的竞态场景

你原来的代码逻辑大概是这样(补全你没写完的部分):

package main

import (
	"log"
	"sync"
	"time"
)

func f(errc chan<- error, wg *sync.WaitGroup) {
	defer wg.Done()
	time.Sleep(1 * time.Second)
	// 模拟错误返回
	errc <- fmt.Errorf("goroutine failed")
}

func main() {
	var wg sync.WaitGroup
	errc := make(chan error)

	for i := 0; i < 3; i++ {
		wg.Add(1)
		go f(errc, &wg)
	}

	defer close(errc) // 问题出在这里

	done := make(chan struct{})
	go func() {
		wg.Wait()
		close(done)
	}()

	select {
	case err := <-errc:
		if err != nil {
			log.Fatal("got error:", err)
		}
	case <-done:
		log.Println("all goroutines done")
	}
}

当父goroutine通过select拿到第一个错误后,会执行defer close(errc)关闭错误通道,但此时剩下的goroutine可能还在运行,正准备往已经关闭的errc发送数据,直接触发send on closed channel的panic——这就是典型的竞态条件:你无法保证关闭channel时,所有发送操作都已经完成。

正确的解决方案:同步关闭时机+取消信号

要解决这个问题,我们需要做到两点:

  1. 确保只有当所有goroutine都不再发送数据时,才关闭errc
  2. 当父goroutine拿到第一个错误后,及时通知剩余goroutine停止工作,避免无效的发送操作

这里提供一个标准的实现方式:

package main

import (
	"context"
	"fmt"
	"log"
	"sync"
	"time"
)

func worker(ctx context.Context, idx int, errc chan<- error, wg *sync.WaitGroup) {
	defer wg.Done()

	// 先检查是否已经被取消,避免做无用功
	select {
	case <-ctx.Done():
		log.Printf("worker %d canceled before starting", idx)
		return
	default:
	}

	// 模拟业务操作,可能出错
	time.Sleep(time.Duration(idx) * 500 * time.Millisecond)
	err := fmt.Errorf("worker %d failed", idx) // 模拟错误

	// 尝试发送错误,但如果context已取消,就放弃发送(避免阻塞)
	select {
	case errc <- err:
		log.Printf("worker %d sent error", idx)
	case <-ctx.Done():
		log.Printf("worker %d canceled before sending error", idx)
	}
}

func main() {
	// 创建可取消的context,用于通知所有goroutine停止工作
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel() // 确保程序退出时取消所有goroutine

	var wg sync.WaitGroup
	// 给errc加1个缓冲,确保第一个错误能被接收,避免发送阻塞
	errc := make(chan error, 1)

	workerCount := 3
	for i := 0; i < workerCount; i++ {
		wg.Add(1)
		go worker(ctx, i, errc, &wg)
	}

	// 单独启动goroutine等待所有worker完成,再关闭errc
	go func() {
		wg.Wait()
		close(errc)
		log.Println("all workers exited, closed errc")
	}()

	// 等待第一个错误或所有worker完成
	select {
	case err := <-errc:
		if err != nil {
			log.Printf("received first error: %v", err)
			cancel() // 通知剩余worker停止工作
			wg.Wait() // 等待所有worker退出,避免goroutine泄漏
		}
	case <-ctx.Done():
		log.Println("context canceled")
	}
}

关键优化点说明

  • 用context取消信号:当父goroutine拿到第一个错误后,调用cancel()让所有剩余goroutine尽快退出,避免它们继续执行无效操作或尝试往errc发送数据。
  • 延迟关闭errc的正确姿势:通过单独的goroutine等待wg.Wait()完成后再关闭errc,确保此时所有worker都已经退出,不会再有发送操作,彻底避免竞态。
  • 非阻塞发送错误:每个worker发送错误时用select监听ctx.Done(),如果已经被取消就放弃发送,避免goroutine因发送到无接收者的channel而阻塞泄漏。
  • 带缓冲的errc:缓冲大小设为1足够接收第一个错误,避免第一个发送操作因父goroutine还没准备好接收而阻塞。

内容的提问来源于stack exchange,提问作者yangmillstheory

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 10:17:09