Go goroutine 与 channel
AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。
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。