Go 协程常见代码问题 在使用 Go 协程(goroutine)编写并发程序时,容易遇到各种陷阱与问题。本文汇总常见的协程相关代码问题、原因分析与解决方案。
一、循环变量捕获问题 1.1 问题描述 在 for 循环中启动 goroutine 时,闭包捕获的是循环变量的引用 而非值,导致所有 goroutine 使用同一个变量。
1 2 3 4 5 6 7 8 9 func main () { for i := 0 ; i < 5 ; i++ { go func () { fmt.Println(i) }() } time.Sleep(time.Second) }
输出 (可能):5 5 5 5 5
1.2 原因分析 flowchart LR
A[i = 0] --> B[启动 goroutine 1]
A --> C[i = 1]
C --> D[启动 goroutine 2]
C --> E[i = 2]
E --> F[...]
F --> G[i = 5]
G --> H[循环结束]
H --> I[所有 goroutine 打印同一个 i]
循环变量 i 在整个循环中只有一个实例
goroutine 中的闭包捕获的是 i 的地址
当 goroutine 执行时,循环可能已结束,i 的值为 5
1.3 解决方案 方案一:传参(推荐) 1 2 3 4 5 6 7 8 func main () { for i := 0 ; i < 5 ; i++ { go func (n int ) { fmt.Println(n) }(i) } time.Sleep(time.Second) }
方案二:局部变量拷贝 1 2 3 4 5 6 7 8 9 func main () { for i := 0 ; i < 5 ; i++ { i := i go func () { fmt.Println(i) }() } time.Sleep(time.Second) }
方案三:Go 1.22+ 自动修复 Go 1.22 之后,for 循环的变量在每次迭代时会自动创建新实例,无需手动处理。
二、竞态条件(Race Condition) 2.1 问题描述 多个 goroutine 同时读写共享变量,没有正确同步,导致数据不一致。
1 2 3 4 5 6 7 8 9 10 11 12 var count int func main () { for i := 0 ; i < 1000 ; i++ { go func () { count++ }() } time.Sleep(time.Second) fmt.Println(count) }
2.2 检测方法 使用 -race 标志运行程序:
2.3 解决方案 方案一:使用 Mutex 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var ( count int mu sync.Mutex ) func main () { for i := 0 ; i < 1000 ; i++ { go func () { mu.Lock() count++ mu.Unlock() }() } time.Sleep(time.Second) fmt.Println(count) }
方案二:使用 atomic 1 2 3 4 5 6 7 8 9 10 11 var count int64 func main () { for i := 0 ; i < 1000 ; i++ { go func () { atomic.AddInt64(&count, 1 ) }() } time.Sleep(time.Second) fmt.Println(count) }
方案三:使用 Channel 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func main () { ch := make (chan int ) count := 0 go func () { for range ch { count++ } }() for i := 0 ; i < 1000 ; i++ { go func () { ch <- 1 }() } time.Sleep(time.Second) close (ch) time.Sleep(time.Millisecond * 10 ) fmt.Println(count) }
三、Goroutine 泄漏 3.1 问题描述 goroutine 启动后无法正常退出,持续占用资源,导致内存泄漏。
常见场景一:Channel 阻塞 1 2 3 4 5 6 7 8 9 func leak () { ch := make (chan int ) go func () { val := <-ch fmt.Println(val) }() }
常见场景二:无限循环无退出条件 1 2 3 4 5 6 7 8 9 10 func leak () { go func () { for { doSomething() time.Sleep(time.Second) } }() }
3.2 解决方案 使用 Context 控制生命周期 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func worker (ctx context.Context) { for { select { case <-ctx.Done(): return default : doSomething() } } } func main () { ctx, cancel := context.WithCancel(context.Background()) go worker(ctx) time.Sleep(time.Second * 5 ) cancel() }
Channel + done 信号 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func worker (done <-chan struct {}) { for { select { case <-done: return default : doSomething() } } } func main () { done := make (chan struct {}) go worker(done) time.Sleep(time.Second * 5 ) close (done) }
3.3 检测泄漏 1 2 3 4 5 6 7 before := runtime.NumGoroutine() after := runtime.NumGoroutine() if after > before { fmt.Printf("Leaked %d goroutines\n" , after-before) }
四、死锁(Deadlock) 4.1 Channel 死锁 场景一:无缓冲 Channel 自己给自己发送 1 2 3 4 5 6 func main () { ch := make (chan int ) ch <- 1 fmt.Println(<-ch) }
解决 :使用 goroutine 或带缓冲 channel
1 2 3 4 5 func main () { ch := make (chan int , 1 ) ch <- 1 fmt.Println(<-ch) }
场景二:循环等待 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func main () { ch1, ch2 := make (chan int ), make (chan int ) go func () { <-ch1 ch2 <- 1 }() go func () { <-ch2 ch1 <- 1 }() time.Sleep(time.Second) }
4.2 Mutex 死锁 场景:重复加锁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 var mu sync.Mutexfunc foo () { mu.Lock() bar() mu.Unlock() } func bar () { mu.Lock() mu.Unlock() }
解决 :使用 sync.RWMutex 或避免嵌套锁
五、WaitGroup 误用 5.1 Add 与 Done 不匹配 1 2 3 4 5 6 7 8 9 10 11 12 13 var wg sync.WaitGroupfunc main () { for i := 0 ; i < 5 ; i++ { wg.Add(1 ) go func () { fmt.Println("work" ) }() } wg.Wait() }
解决 :确保每个 Add 对应一个 Done
1 2 3 4 go func () { defer wg.Done() fmt.Println("work" ) }()
5.2 Add 在 goroutine 内部 1 2 3 4 5 6 7 8 9 10 11 12 13 var wg sync.WaitGroupfunc main () { for i := 0 ; i < 5 ; i++ { go func () { wg.Add(1 ) defer wg.Done() fmt.Println("work" ) }() } wg.Wait() }
解决 :在启动 goroutine 前 Add
1 2 3 4 5 6 7 for i := 0 ; i < 5 ; i++ { wg.Add(1 ) go func () { defer wg.Done() fmt.Println("work" ) }() }
5.3 WaitGroup 拷贝 1 2 3 4 5 6 7 8 9 10 11 12 func work (wg sync.WaitGroup) { defer wg.Done() fmt.Println("work" ) } func main () { var wg sync.WaitGroup wg.Add(1 ) go work(wg) wg.Wait() }
解决 :传指针
1 2 3 4 5 6 7 8 9 10 11 func work (wg *sync.WaitGroup) { defer wg.Done() fmt.Println("work" ) } func main () { var wg sync.WaitGroup wg.Add(1 ) go work(&wg) wg.Wait() }
六、Channel 使用问题 6.1 重复关闭 Channel 1 2 3 4 ch := make (chan int ) close (ch)close (ch)
解决 :使用 sync.Once 确保只关闭一次
1 2 3 4 var once sync.Onceonce.Do(func () { close (ch) })
6.2 向已关闭 Channel 发送 1 2 3 4 ch := make (chan int ) close (ch)ch <- 1
6.3 nil Channel 1 2 3 4 5 var ch chan int ch <- 1 <-ch close (ch)
在 select 中的应用 (禁用某个 case):
1 2 3 4 5 6 7 8 9 10 var ch1, ch2 chan int ch1 = make (chan int ) select {case v := <-ch1: fmt.Println(v) case v := <-ch2: fmt.Println(v) }
6.4 无缓冲 vs 有缓冲 1 2 3 4 5 ch := make (chan int ) ch := make (chan int , 10 )
七、Panic 处理 7.1 Goroutine 中的 Panic 1 2 3 4 5 6 7 func main () { go func () { panic ("oops" ) }() time.Sleep(time.Second) }
解决 :每个 goroutine 内部 recover
1 2 3 4 5 6 7 8 9 10 11 func main () { go func () { defer func () { if r := recover (); r != nil { fmt.Println("Recovered:" , r) } }() panic ("oops" ) }() time.Sleep(time.Second) }
7.2 统一 Panic 处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func safeGo (fn func () ) { go func () { defer func () { if r := recover (); r != nil { log.Printf("Panic recovered: %v\n%s" , r, debug.Stack()) } }() fn() }() } func main () { safeGo(func () { panic ("oops" ) }) time.Sleep(time.Second) }
八、Select 语句陷阱 8.1 所有 case 都阻塞时无 default 1 2 3 4 5 6 7 8 9 10 11 func main () { ch1 := make (chan int ) ch2 := make (chan int ) select { case <-ch1: case <-ch2: } }
解决 :添加 default 或 timeout
1 2 3 4 5 6 select {case <-ch1:case <-ch2:case <-time.After(time.Second): fmt.Println("timeout" ) }
8.2 空 select
用途 :阻止 main 函数退出
8.3 case 的执行顺序 select 在多个 case 同时就绪时随机选择 ,不按顺序。
1 2 3 4 5 6 7 8 9 10 11 12 ch1 := make (chan int , 1 ) ch2 := make (chan int , 1 ) ch1 <- 1 ch2 <- 2 select {case <-ch1: fmt.Println("ch1" ) case <-ch2: fmt.Println("ch2" ) }
九、Context 使用问题 9.1 不传递 Context 1 2 3 4 5 func work () { time.Sleep(time.Hour) }
解决 :接收并监听 Context
1 2 3 4 5 6 7 func work (ctx context.Context) { select { case <-time.After(time.Hour): case <-ctx.Done(): return } }
9.2 使用错误的父 Context 1 2 3 4 5 6 ctx, cancel := context.WithCancel(context.Background()) ctx2 := context.Background()
9.3 Context 值的滥用 1 2 3 4 5 ctx := context.WithValue(ctx, "userID" , 123 ) ctx := context.WithValue(ctx, "requestID" , "abc-123" )
十、最佳实践 10.1 不要泄漏 Goroutine flowchart TD
A[创建 goroutine] --> B{有退出条件?}
B -->|是| C[使用 Context/Channel 控制]
B -->|否| D[goroutine 泄漏]
C --> E[正常退出]
D --> F[内存泄漏]
style E fill:#ccffcc
style F fill:#ffcccc
每个 goroutine 都应有明确的退出条件
使用 Context 或 Channel 传递取消信号
定期检查 runtime.NumGoroutine()
10.2 避免共享内存,使用 Channel 通信 1 2 3 4 ch := make (chan Data) go producer(ch)go consumer(ch)
10.3 使用 -race 检测竞态 1 2 go test -race ./... go build -race
10.4 Goroutine 数量控制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func workerPool (jobs <-chan Job, results chan <- Result, workers int ) { var wg sync.WaitGroup for i := 0 ; i < workers; i++ { wg.Add(1 ) go func () { defer wg.Done() for job := range jobs { results <- process(job) } }() } wg.Wait() close (results) }
10.5 使用 errgroup 简化错误处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import "golang.org/x/sync/errgroup" func main () { g := new (errgroup.Group) for i := 0 ; i < 10 ; i++ { i := i g.Go(func () error { return work(i) }) } if err := g.Wait(); err != nil { log.Fatal(err) } }
十一、小结
问题类型
原因
解决方案
循环变量捕获
闭包引用循环变量地址
传参或局部变量拷贝
竞态条件
多 goroutine 无同步访问共享变量
Mutex / atomic / Channel
Goroutine 泄漏
无退出条件或 Channel 阻塞
Context / done Channel
死锁
Channel 或 Mutex 循环等待
避免循环依赖、超时机制
WaitGroup 误用
Add/Done 不匹配或拷贝
确保配对、传指针
Channel 问题
重复关闭、nil Channel
sync.Once、检查 nil
Panic
goroutine 内 panic 未 recover
defer recover
Context
不传递或滥用 WithValue
正确传递、合理使用
核心原则 :
不要泄漏 goroutine :明确退出条件
避免竞态 :使用 -race 检测,正确同步
用 Channel 通信 :而非共享内存
控制并发数 :避免创建过多 goroutine
优雅处理错误 :recover panic、检查 error