-
数据竞争 (Data Race)
-
说明: 这是最常见也是最危险的问题。当多个 Goroutine 并发地访问(至少一个是写入操作)同一个共享变量,并且没有使用同步机制(如 Mutex、Channel)来保护时,就会发生数据竞争。结果是不可预测的,可能导致程序崩溃、数据损坏或逻辑错误。
-
检测: Go 提供了强大的工具来检测数据竞争:
go run -race main.go
或go build -race
。强烈建议在开发和测试阶段始终开启-race
检查! -
示例 (错误):
gopackage main import ( "fmt" "sync" "time" ) var counter int // 共享变量 func main() { var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter++ // <- 这里存在数据竞争! }() } wg.Wait() fmt.Println("Final Counter (unreliable):", counter) // 结果通常不是 1000 time.Sleep(1 * time.Second) // 等待可能的竞争日志输出 } // 使用 go run -race main.go 运行会报告 DATA RACE
-
示例 (正确 - 使用 Mutex):
gopackage main import ( "fmt" "sync" ) var counter int var mu sync.Mutex // 互斥锁 func main() { var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() mu.Lock() // 获取锁 counter++ mu.Unlock() // 释放锁 }() } wg.Wait() fmt.Println("Final Counter (Mutex):", counter) // 结果稳定为 1000 }
-
示例 (正确 - 使用 Atomic): 对于简单的数值操作,
sync/atomic
包提供了更高效的原子操作。gopackage main import ( "fmt" "sync" "sync/atomic" ) var counter int64 // 使用 atomic 需要特定类型 func main() { var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() atomic.AddInt64(&counter, 1) // 原子地增加 }() } wg.Wait() fmt.Println("Final Counter (Atomic):", counter) // 结果稳定为 1000 }
-
-
Goroutine 泄漏 (Goroutine Leak)
-
说明: 如果一个 Goroutine 启动后,因为某种原因(如等待一个永远不会关闭或写入数据的 Channel)而永久阻塞,它就永远不会退出,占用的内存和其他资源也无法释放。随着时间推移,泄漏的 Goroutine 累积可能耗尽系统资源。
-
检测: 可以通过
runtime.NumGoroutine()
监控 Goroutine 数量,或者使用net/http/pprof
工具分析 Goroutine 的堆栈信息。 -
示例 (泄漏):
gopackage main import ( "fmt" "runtime" "time" ) func leakyWorker() { ch := make(chan int) go func() { // 这个 Goroutine 等待从 ch 读取数据,但永远不会有数据写入 val := <-ch fmt.Println("Received:", val) // 永远不会执行 }() fmt.Println("leakyWorker finished its main logic") // 函数返回了,但内部启动的 Goroutine 阻塞了 -> 泄漏 } func main() { fmt.Println("Initial Goroutines:", runtime.NumGoroutine()) // 通常是 1 或 2 leakyWorker() time.Sleep(1 * time.Second) // 给点时间看看 Goroutine 数量 fmt.Println("Goroutines after leakyWorker:", runtime.NumGoroutine()) // 数量会增加 1 // 在实际应用中,如果 leakyWorker 被反复调用,Goroutine 数量会持续增长 }
-
避免泄漏:
- 确保所有 Channel 都有明确的关闭或写入逻辑。
- 使用
context
包来传递取消信号,让 Goroutine 可以在不再需要时主动退出。 - 使用
select
语句配合default
或超时case
来避免无限期阻塞。
-
-
闭包 (Closure) 中的循环变量问题
-
说明: 在
for
循环中直接启动 Goroutine 并引用循环变量时,Goroutine 捕获的是变量的引用 ,而不是当前迭代的值。由于 Goroutine 的执行是异步的,当它们真正开始运行时,循环可能已经结束,此时所有 Goroutine 都会看到循环变量的最终值。 -
示例 (错误):
gopackage main import ( "fmt" "sync" "time" ) func main() { var wg sync.WaitGroup values := []string{"a", "b", "c"} for _, v := range values { wg.Add(1) go func() { // 错误的闭包用法 defer wg.Done() // v 是共享的,所有 Goroutine 运行时 v 很可能都是 "c" fmt.Println("Processing (incorrect):", v) }() } wg.Wait() time.Sleep(100*time.Millisecond) // 等待输出完成 } // 输出通常是多个 "Processing (incorrect): c"
-
示例 (正确 - 通过参数传递):
gopackage main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup values := []string{"a", "b", "c"} for _, v := range values { wg.Add(1) go func(val string) { // 将 v 作为参数传递 defer wg.Done() // val 是当前 Goroutine 的局部副本 fmt.Println("Processing (param):", val) }(v) // 传递当前迭代的 v 的值 } wg.Wait() } // 输出是 "Processing (param): a", "Processing (param): b", "Processing (param): c" (顺序不定)
-
示例 (正确 - 内部变量):
gopackage main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup values := []string{"a", "b", "c"} for _, v := range values { wg.Add(1) v := v // 在循环内部创建一个新的变量副本 (Shadowing) go func() { defer wg.Done() // 这里引用的 v 是内部副本 fmt.Println("Processing (internal var):", v) }() } wg.Wait() } // 输出是 "Processing (internal var): a", "Processing (internal var): b", "Processing (internal var): c" (顺序不定)
-
-
使用
sync.WaitGroup
等待 Goroutine 完成- 说明: 如果主 Goroutine 需要等待其他子 Goroutine 完成后再继续执行或退出,必须使用同步机制。
sync.WaitGroup
是最常用的方法。 - 关键点:
Add(n)
: 在启动 Goroutine 之前 调用,增加计数器。Done()
: 在 Goroutine 内部 ,通常使用defer
来确保在 Goroutine 退出前调用,减少计数器。Wait()
: 在主 Goroutine 中调用,阻塞直到计数器归零。
- 常见错误:
- 在 Goroutine 内部 调用
Add(1)
: 可能导致Wait()
在Add(1)
执行前就返回(如果主 Goroutine 运行得快)。 - 忘记调用
Done()
: 导致Wait()
永久阻塞。
- 在 Goroutine 内部 调用
- 示例 (正确使用 WaitGroup): (见上面数据竞争的正确示例)
- 说明: 如果主 Goroutine 需要等待其他子 Goroutine 完成后再继续执行或退出,必须使用同步机制。
-
Channel 的正确使用
-
说明: Channel 是 Goroutine 间通信和同步的主要方式。
-
关键点:
- 阻塞: 无缓冲 Channel 的发送和接收都会阻塞,直到另一方准备好。有缓冲 Channel 在缓冲区满(发送时)或空(接收时)时阻塞。
- 关闭:
close(ch)
用于告知接收方不会再有数据发送。对已关闭的 Channel 发送会 panic,但接收会立即返回零值和false
。range ch
会在 Channel 关闭后自动结束循环。 - 死锁: 所有 Goroutine 都在等待某个 Channel 操作,但没有其他 Goroutine 能满足这个操作(如所有 Goroutine 都在等待接收,但没有 Goroutine 在发送)。
-
示例 (关闭 Channel 和 range):
gopackage main import ( "fmt" "time" ) func producer(ch chan int, count int) { defer close(ch) // 完成后关闭 channel for i := 0; i < count; i++ { ch <- i time.Sleep(10 * time.Millisecond) } fmt.Println("Producer finished") } func consumer(id int, ch chan int) { // 使用 range 读取 channel,会在 channel 关闭后自动退出循环 for val := range ch { fmt.Printf("Consumer %d received: %d\n", id, val) } fmt.Printf("Consumer %d finished (channel closed)\n", id) } func main() { ch := make(chan int, 3) // 使用带缓冲的 channel go producer(ch, 5) go consumer(1, ch) go consumer(2, ch) // 可以有多个 consumer time.Sleep(1 * time.Second) // 等待所有 Goroutine 完成 }
-
-
使用
context
进行取消、超时和传递值-
说明: 在复杂的应用(尤其是网络服务)中,经常需要在请求处理链或一组相关的 Goroutine 中传递取消信号、截止时间或请求范围的值。
context
包是 Go 的标准解决方案。 -
关键点:
context.Background()
: 通常作为顶层 Context。context.WithCancel(parent)
: 创建可手动取消的 Context。context.WithTimeout(parent, duration)
: 创建到时自动取消的 Context。context.WithDeadline(parent, time)
: 创建到指定时间自动取消的 Context。- Goroutine 内部通过
select
监听ctx.Done()
Channel 来响应取消信号。
-
示例 (超时控制):
gopackage main import ( "context" "fmt" "time" ) func longRunningTask(ctx context.Context) { select { case <-time.After(5 * time.Second): // 模拟耗时操作 fmt.Println("Task finished normally") case <-ctx.Done(): // 检查 context 是否被取消 fmt.Println("Task cancelled:", ctx.Err()) // ctx.Err() 说明取消原因 } } func main() { // 创建一个 2 秒后自动取消的 context ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() // 确保 cancel 函数被调用(即使任务提前完成) fmt.Println("Starting task...") go longRunningTask(ctx) // 等待足够长的时间观察结果 time.Sleep(3 * time.Second) fmt.Println("Main finished") } // 输出会显示 "Task cancelled: context deadline exceeded"
-
-
错误处理
-
说明:
go func()
启动的 Goroutine 不能像普通函数调用那样直接返回error
。需要设计一种机制将错误信息传递回调用方或进行集中处理。 -
常用方法:
- 使用一个专门的 Channel 来传递
error
。 - 将错误存储在共享变量中(需要加锁保护)。
- 使用
sync.WaitGroup
配合一个共享的错误切片(或第一个错误变量)。 - 使用
golang.org/x/sync/errgroup
包,它封装了 Goroutine 组、错误传播和 Context 取消。
- 使用一个专门的 Channel 来传递
-
示例 (使用 error channel):
gopackage main import ( "errors" "fmt" "time" ) func worker(id int, errChan chan error) { fmt.Printf("Worker %d starting\n", id) time.Sleep(time.Duration(id) * 100 * time.Millisecond) if id == 2 { // 模拟 worker 2 出错 fmt.Printf("Worker %d encountered an error\n", id) errChan <- errors.New(fmt.Sprintf("worker %d failed", id)) return } fmt.Printf("Worker %d finished successfully\n", id) errChan <- nil // 成功时发送 nil } func main() { numWorkers := 3 errChan := make(chan error, numWorkers) // 缓冲 channel 避免发送阻塞 for i := 1; i <= numWorkers; i++ { go worker(i, errChan) } // 等待所有 worker 的结果 for i := 0; i < numWorkers; i++ { err := <-errChan if err != nil { fmt.Println("Received error:", err) // 在实际应用中,可能需要取消其他 worker 或做其他处理 } } close(errChan) fmt.Println("All workers finished or reported error.") }
-
-
Panic 处理
-
说明: 一个 Goroutine 中的 panic 如果没有被
recover
,会导致整个程序崩溃。如果希望某个 Goroutine 的失败不影响其他部分,需要在该 Goroutine 内部使用defer
和recover
。 -
示例:
gopackage main import ( "fmt" "time" ) func mayPanic(id int) { defer func() { if r := recover(); r != nil { fmt.Printf("Goroutine %d recovered from panic: %v\n", id, r) } }() fmt.Printf("Goroutine %d running\n", id) if id == 1 { panic("something went wrong in goroutine 1") } fmt.Printf("Goroutine %d finished normally\n", id) } func main() { go mayPanic(0) go mayPanic(1) // 这个会 panic,但会被 recover go mayPanic(2) time.Sleep(1 * time.Second) // 等待 Goroutine 执行 fmt.Println("Main exiting") } // 程序不会崩溃,会打印 recover 的信息
-
一句话:
- 警惕数据竞争: 使用
-race
检测,并通过 Mutex、Channel 或 Atomic 操作进行同步。 - 防止 Goroutine 泄漏: 确保 Goroutine 有明确的退出路径,善用
context
。 - 正确处理闭包变量: 通过参数传递或内部变量副本避免共享循环变量。
- 有效等待: 使用
sync.WaitGroup
或其他同步原语等待 Goroutine 完成。 - 精心设计 Channel 通信: 理解阻塞、关闭和死锁。
- 利用
context
: 实现优雅的取消和超时控制。 - 规划错误处理: 将 Goroutine 中的错误传递出来。
- 考虑 Panic 恢复: 在需要隔离故障时使用
recover
。