极简工具箱

免费在线工具 · 本地计算 · 无需上传

Go 并发入门:goroutine、channel 与 context 取消

Go 的并发很容易写,也很容易写出泄漏。goroutine 不会因为函数返回而结束,channel 不关就可能永远阻塞——这两点是排查并发问题的起点。

广告位(百度联盟)

一、goroutine 有多轻

go func() {
    fmt.Println("running")
}()

go 关键字启动一个 goroutine,初始栈只有 2KB 左右(可动态增长),所以起几万个也不稀奇。但「轻」不等于「免费」:每个 goroutine 都要调度、占内存,必须有明确的退出路径,否则就是泄漏。

主函数 main 返回时,所有 goroutine 立即终止,不会等待。

二、channel:无缓冲 vs 有缓冲

ch := make(chan int)        // 无缓冲
ch := make(chan int, 10)    // 缓冲 10
ch <- 1        // 发送
v := <-ch      // 接收
v, ok := <-ch  // ok 为 false 表示 channel 已关闭且无数据
close(ch)      // 关闭(只能由发送方关)

四条铁律:

  1. 向已关闭的 channel 发送 → panic。
  2. 重复关闭 → panic。
  3. 从已关闭的 channel 接收 → 立即返回零值,ok 为 false。
  4. 只由发送方关闭,接收方不要关。

三、select:多路等待

select {
case v := <-ch1:
    fmt.Println("ch1", v)
case ch2 <- 42:
    fmt.Println("sent to ch2")
case <-time.After(3 * time.Second):
    fmt.Println("timeout")
default:
    fmt.Println("no ready channel")
}

四、WaitGroup:等一组任务

var wg sync.WaitGroup
for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(n int) {
        defer wg.Done()
        work(n)
    }(i)
}
wg.Wait()

要点:

五、context:取消与超时

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

select {
case <-ctx.Done():
    return ctx.Err()      // context deadline exceeded
case res := <-resultCh:
    return res
}

约定:

context.WithValue 只放请求级的元数据(trace id、用户身份),不要放业务参数。

六、数据竞争

多个 goroutine 同时读写同一个变量且没有同步,就是数据竞争(race)。表现可能是「偶尔算错」「偶尔 panic」,极难复现。

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

-race 检测器会在运行时报告竞争位置(两个 goroutine 的调用栈)。并发代码必须跑一次 -race,这是最划算的投入。

保护共享数据的两种方式:

var mu sync.Mutex
mu.Lock(); counter++; mu.Unlock()

// 或原子操作
atomic.AddInt64(&counter, 1)

七、三个常见错误

1. goroutine 泄漏

// 消费者提前退出,生产者永远阻塞在发送上
ch := make(chan int)
go func() { for i := 0; ; i++ { ch <- i } }()

解决:用带缓冲的 channel + ctx 控制,或确保消费端始终读到关闭。

2. 循环里起 goroutine 没限流

一万个任务起一万个 goroutine,可能把下游打挂。用信号量模式限流:

sem := make(chan struct{}, 10)
for _, t := range tasks {
    sem <- struct{}{}
    go func(t Task) {
        defer func() { <-sem }()
        handle(t)
    }(t)
}

3. 用 channel 做锁

channel 能做同步,但简单互斥用 sync.Mutex 更清晰、更快。别为了「Go 风格」把什么都塞进 channel。

小结

goroutine 要能退出;无缓冲 channel 是同步握手,有缓冲是削峰;只由发送方 close;Add 在启动前调用;cancel 必须 defer;并发代码必须跑 -race。

本文相关工具(本地计算,无需上传)

更多使用教程