03_并发与Context
goroutine、channel、Context 取消、共享状态和数据竞争排查。
Go 并发与 Context
学习目标:能启动、取消并等待并发任务,避免 goroutine 泄漏与数据竞争。
1. goroutine 和 channel
goroutine 是 Go 的轻量并发执行单元;channel 用于在 goroutine 间传递值并协调执行。启动任务不等于任务会在主程序退出前完成,必须明确等待或取消机制。
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
result := make(chan string, 1)
go func() {
select {
case result <- "完成":
case <-ctx.Done():
}
}()
select {
case value := <-result:
fmt.Println(value)
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
select 选择一个已就绪的通信分支;若多个分支同时就绪,不应依赖固定顺序。缓冲通道容量为 1,使示例中的发送方即使主流程取消也不必长期等待接收。
2. Context 传递请求生命周期
使用原则:将 context.Context 作为函数第一个参数,沿调用链传给数据库和外部 HTTP 请求;在创建超时或取消 Context 后调用 cancel() 释放资源。不要把 Context 长期存进结构体,也不要用它传递普通业务参数。
func load(ctx context.Context, id int) error {
// 数据库调用应使用 QueryContext / ExecContext 等可取消方法。
return ctx.Err()
}
上面只是展示签名;真实查询要处理 ctx.Err() 与数据库错误,不能用它代替实际数据访问。
3. 共享状态与退出
- 多任务共同修改同一映射会产生数据竞争,使用
sync.Mutex、单一拥有者 goroutine 或重新设计为消息传递。 - 用
sync.WaitGroup等待已启动任务;生产者负责关闭其写入的 channel,接收方通常不要关闭。 - 每个长期运行的 goroutine 都应有退出条件,尤其是等待 channel 或外部 I/O 的循环。
- 用
go test -race ./...查找运行中触发的数据竞争;它不能证明所有路径都没有问题。
自测
- 只调用
go work()就能保证主函数退出前完成吗?不能。 - 为什么 HTTP 请求的 Context 应传给数据库?客户端取消或超时后,数据库操作才能尽快停止,避免继续占用资源。
4. 关闭 channel 与等待任务
关闭 channel 表示“以后不会再发送值”,接收方可用 for value := range ch 读到关闭为止。通常由发送方关闭,因为它知道何时不再发送;重复关闭会 panic。WaitGroup 负责等待任务结束,channel 负责传数据,两者用途不同。
var wg sync.WaitGroup
jobs := make(chan int)
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
fmt.Println(job)
}
}()
jobs <- 1
close(jobs)
wg.Wait()
这段是函数体片段,需要导入 fmt、sync。若忘记 close(jobs),消费者会一直等待,wg.Wait() 也不会结束。若生产者可能提前失败,要设计 defer close(jobs) 或显式取消路径。
5. 用 Context 管理真实工作
在 HTTP handler 中从 r.Context() 开始,向仓储函数与外部请求继续传递。创建子超时要 defer cancel();子超时不应超过上游请求允许的时间。循环中也应检查 ctx.Done(),否则一个纯计算循环不会因为 Context 被取消就自动停止。
func work(ctx context.Context, jobs <-chan int) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case job, ok := <-jobs:
if !ok { return nil }
_ = job // 此处替换为真正的处理逻辑
}
}
}
多个 select 分支同时就绪时选择不确定;不要把取消分支视为绝对优先。如果业务要求取消后绝不处理下一项,取出任务后还要再次检查 ctx.Err()。
6. 数据竞争与吞吐量
普通 map 和计数器被多个 goroutine 写入时要同步。go test -race 能发现被测试路径实际触发的竞争,但不能替代设计审查。用有界 worker 数限制并发,避免为每个请求无上限地启动任务;数据库连接池、下游限流和内存往往先成为瓶颈。
练习:构造两个 worker 消费 10 个任务,支持超时取消,最后确认所有 goroutine 都退出。