Go goroutine 与 channel

AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。

Go 1.24

go 关键字启动一个 goroutine,channel 在它们之间传值。语法很少,难的是「谁负责关闭」「什么时候会阻塞」这两件事。

goroutine 与 WaitGroup

var wg sync.WaitGroup

for i := 1; i <= 3; i++ {
    wg.Add(1)
    go func(n int) {
        defer wg.Done()
        fmt.Printf("任务 %d 开始\n", n)
    }(i)
}

wg.Wait()

三个细节:

  • wg.Add(1) 必须在 go 之前调用,否则 Wait 可能在计数前返回。
  • 参数通过形参传入,别在闭包里直接引用循环变量(Go 1.22 起循环变量每轮独立,但显式传参仍更清楚)。
  • 主 goroutine 退出时,其他 goroutine 会被直接终止,不会等待——所以需要 Wait。

channel 的基本规则

ch := make(chan int)        // 无缓冲
buf := make(chan int, 3)    // 有缓冲,容量 3

ch <- 1        // 发送
v := <-ch      // 接收
v, ok := <-ch  // ok 为 false 表示已关闭且无数据
close(ch)      // 关闭,只有发送方可以关
  • 无缓冲:发送与接收必须同时就绪,天然同步。
  • 有缓冲:缓冲满时发送阻塞,空时接收阻塞。
  • 关闭:向已关闭的 channel 发送会 panic;从已关闭的 channel 接收立即返回零值。
  • range ch 会在 channel 关闭且数据取完后结束——不关闭就永远等下去。
go func() {
    defer close(ch)
    for _, v := range []int{1, 2, 3} {
        ch <- v
    }
}()

for v := range ch {
    fmt.Println(v)
}

关闭是发送方的责任,接收方永远不该 close。多个发送方时,用额外的 done channel 或 WaitGroup 收尾后再关。

select

select 在多个 channel 操作中等待第一个就绪的:

select {
case v := <-data:
    fmt.Println("收到", v)
case err := <-errc:
    fmt.Println("出错", err)
case <-time.After(2 * time.Second):
    fmt.Println("超时")
case <-ctx.Done():
    return ctx.Err()
}

default 让 select 不阻塞(非阻塞尝试):

select {
case ch <- v:
    // 发送成功
default:
    // 缓冲已满,丢弃或稍后重试
}

注意 time.After 在循环里会不断创建定时器,长期运行应改用 time.NewTimer + Reset。

常见模式

worker pool

func process(ctx context.Context, jobs []Job, workers int) ([]Result, error) {
    jobCh := make(chan Job)
    resCh := make(chan Result)

    var wg sync.WaitGroup
    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobCh {
                resCh <- run(ctx, job)
            }
        }()
    }

    go func() {
        defer close(jobCh)
        for _, job := range jobs {
            select {
            case jobCh <- job:
            case <-ctx.Done():
                return
            }
        }
    }()

    go func() {
        wg.Wait()
        close(resCh)
    }()

    var results []Result
    for res := range resCh {
        results = append(results, res)
    }
    return results, ctx.Err()
}

谁生产谁关闭 jobCh,wg.Wait() 负责在全部 worker 结束后关闭 resCh——这样消费端 range 才能正常退出。

扇出/扇入

多个 goroutine 从同一 channel 读(扇出),结果汇总到一个 channel(扇入)。

超时控制

select {
case res := <-doWork():
    return res
case <-time.After(500 * time.Millisecond):
    return ErrTimeout
}

注意这里 goroutine 仍在跑,若它会写结果 channel,需要带缓冲或可取消,否则泄漏。

Mutex 还是 channel

  • 共享状态(计数器、缓存、配置):sync.Mutex 更直接。
  • 传递所有权、流水线、事件通知:channel 更自然。
type Counter struct {
    mu sync.Mutex
    n  int
}

func (c *Counter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.n++
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.n
}

读多写少用 sync.RWMutex:RLock 可并发,Lock 独占。别在持锁期间做 I/O 或调用可能回调的代码。

sync.Once 适合单次初始化:

var once sync.Once
once.Do(func() { cfg = loadConfig() })

数据竞争

go test -race ./...
go run -race main.go

race detector 只有在实际发生竞争时才报告,所以测试要覆盖并发路径。典型症状:结果偶尔不对、map 并发读写直接 fatal(concurrent map writes)。

// 危险:多个 goroutine 同时写 map
go func() { m["a"] = 1 }()
go func() { m["b"] = 2 }()

// 安全:加锁,或用 sync.Map / 单 goroutine 独占

常见错误与注意事项

1. 主函数提前退出

go doWork()
// main 返回,doWork 被终止

用 WaitGroup、channel 或 select {} 等它完成。

2. 向已关闭的 channel 发送

panic: send on closed channel。只让唯一的发送方关闭,且关闭后不再发送。

3. 重复关闭

同样 panic。用 sync.Once 包住 close。

4. 无缓冲 channel 死锁

ch := make(chan int)
ch <- 1        // 没有接收方,永久阻塞

需要并发接收,或给缓冲。

5. goroutine 泄漏

channel 没人读、select 缺少 ctx.Done() 分支、循环里起 goroutine 但不回收。用 runtime.NumGoroutine() 或 pprof 观察数量是否持续增长。

6. 在循环里忘记传参

for _, v := range items {
    go func() { use(v) }()   // Go 1.22 前全部用最后一个值
}

显式传参最保险:go func(v Item) { use(v) }(v)。

7. 用 channel 传递大对象

channel 传的是副本(指针除外)。大结构体走指针,或重新设计数据流。

8. 认为 close 会等消费者处理完

close 只是标记「不会再有新值」,缓冲里的数据仍可被读取。

小结

  • go + WaitGroup 是最小并发单元,Add 在启动前调用。
  • 无缓冲同步、有缓冲解耦;关闭由发送方负责,接收方用 range 或 ok 判断。
  • select 处理多路等待与超时,循环里别用 time.After。
  • worker pool 的三段式:生产并关闭输入、worker 处理、Wait 后关闭输出。
  • 共享状态用 Mutex,数据流用 channel;CI 必跑 -race。

下一步