4.3 Go 协程:goroutine
目录
- 一、Goroutine 是什么?
- 1. Goroutine vs 操作系统线程
- 2. 并发 (Concurrency) 与并行 (Parallelism)
- 二、如何启动 Goroutine
- 1. go 关键字的使用
- 2. 示例:主协程与子协程
- 3. 匿名函数与参数传递
- 三、Goroutine 的生命周期
- 1. Goroutine 的状态
- 2. Goroutine 的 Panic 处理
- 四、Go 调度器原理
- 1. GMP 模型
- 2. 调度器的工作机制
- 五、常见陷阱与最佳实践
- 1. 循环变量陷阱
- 2. Goroutine 泄漏
- 3. 性能优化建议
- 七、Goroutine 的应用
- 八、总结
Go 语言将
并发作为其核心特性,通过
Goroutine(协程) 机制,让开发者能够轻松编写出高性能且易于理解的并发程序。
一、Goroutine 是什么?
Goroutine 是 Go 语言并发的执行单元。你可以简单地将其视为一个轻量级的线程。
在 Go 语言中,每一个并发的执行单元都称为 Goroutine (协程)。
想象一下您的程序中有两个独立的功能函数,一个负责复杂的计算任务,另一个负责将结果输出。在一个传统的、线性的程序中,这两个函数必须依次执行——先完成计算,再进行输出。
而当您使用 Goroutine 时,这两个函数可以同时开始、独立运行。Go 语言的运行时(Runtime)会自动调度它们,让您能够轻松地实现并发执行。
你可以将 Goroutine 简单地理解为一个“超级轻量级”的线程。 相比于操作系统线程的高开销,Goroutine 的创建和管理成本极低,这使得 Go 程序能够轻松启动成千上万个并发任务。
1. Goroutine vs 操作系统线程

总结: Goroutine 相比传统线程更加轻量、高效,使得 Go 语言能以极低的成本实现大规模并发。
2. 并发 (Concurrency) 与并行 (Parallelism)
- 并发 (Concurrency): 指的是
处理多个任务的能力。宏观上看起来是同时在做,但微观上可能在单核 CPU 上通过时间片切换轮流执行。Go 语言的设计目标是实现并发。 - 并行 (Parallelism): 指的是同时做多个任务的能力。这需要多个 CPU 核心同时运行多个任务。
- Go Runtime: Go 语言的运行时会自动将 Goroutine 高效地映射到少量的操作系统线程上,通过调度器实现并发,如果有多核 CPU 也会自动实现并行。
二、如何启动 Goroutine
启动 Goroutine 非常简单,只需在一个函数调用前加上 go 关键字。
1. go 关键字的使用
任何函数或匿名函数,只要在调用前加上 go 关键字,就会在一个新的 Goroutine 中执行。
// 1. 普通函数调用
func myFunction() {// ...
}// 启动一个 Goroutine 执行 myFunction
go myFunction()
2. 示例:主协程与子协程
当一个 Go 程序启动时,main 函数运行在一个单独的 Goroutine 中,我们称之为 main Goroutine。
注意: 当 main Goroutine 结束时,程序会立即终止,不管其他 Goroutine 是否仍在运行。
package mainimport ("fmt""time"
)func worker(id int) {fmt.Printf("Goroutine %d: 开始工作...\n", id)time.Sleep(500 * time.Millisecond) // 模拟耗时操作fmt.Printf("Goroutine %d: 完成工作。\n", id)
}func main() {// 启动两个子协程go worker(1) go worker(2)fmt.Println("Main Goroutine: 已启动所有子协程。")// 阻塞 main Goroutine 一段时间,确保子协程有时间运行// ⚠️ 警告:这是一种临时的、不推荐的同步方式time.Sleep(1 * time.Second) fmt.Println("Main Goroutine: 退出。") // main 函数返回,程序结束,即使 worker 协程尚未完全完成
}
输出示例:
Main Goroutine: 已启动所有子协程。
Goroutine 2: 开始工作...
Goroutine 1: 开始工作...
Goroutine 1: 完成工作。
Goroutine 2: 完成工作。
Main Goroutine: 退出。
重要提示:
在实际项目中,绝对不能使用 time.Sleep 来等待 Goroutine 完成。这是一种不确定的同步方式。
更优雅且推荐的方式是使用 sync.WaitGroup 或 Channel 来进行同步。
3. 匿名函数与参数传递
Goroutine 也可以通过匿名函数启动,通常用于需要传递参数或访问闭包变量的场景。
package mainimport ("fmt""time"
)func main() {for i := 0; i < 5; i++ {// ❌ 错误:i 的值在 Goroutine 执行时可能已经改变(全部输出 5)// go func() {// fmt.Printf("循环变量i: %d\n", i)// }()// ✅ 正确做法:将 i 作为参数传递给匿名函数,保证 Goroutine 捕获到当前 i 的值go func(j int) {fmt.Printf("Goroutine %d: 正在执行...\n", j)}(i) // 立即调用匿名函数并传入 i 的值}time.Sleep(100 * time.Millisecond)fmt.Println("所有 Goroutine 启动完毕。")
}
三、Goroutine 的生命周期
一个 Goroutine 的生命周期从go语句开始,并在以下情况之一结束:
- 函数自然返回: Goroutine 内部执行的函数正常执行完毕,返回。
- 发生未捕获的 panic: 如果 Goroutine 发生 panic 且没有在自身的 defer 中被 recover 捕获,则该 Goroutine 终止,并导致整个程序崩溃。
- 主 Goroutine 结束: 当 main 函数返回时,程序会强制终止所有正在运行的 Goroutine。
1. Goroutine 的状态
package mainimport ("fmt""runtime""time"
)func showGoroutineInfo() {fmt.Printf("当前 Goroutine 数量: %d\n", runtime.NumGoroutine())
}func longRunningTask(id int) {fmt.Printf("Goroutine %d: 开始执行\n", id)// 模拟长时间运行的任务for i := 0; i < 5; i++ {time.Sleep(200 * time.Millisecond)fmt.Printf("Goroutine %d: 执行步骤 %d\n", id, i+1)}fmt.Printf("Goroutine %d: 执行完成\n", id)
}func main() {fmt.Println("程序开始")showGoroutineInfo() // 显示当前只有 main goroutine// 启动多个 goroutinefor i := 1; i <= 3; i++ {go longRunningTask(i)}// 稍等片刻,让 goroutine 开始执行time.Sleep(100 * time.Millisecond)showGoroutineInfo() // 显示增加的 goroutine 数量// 等待所有 goroutine 完成(这里用 sleep 仅作演示)time.Sleep(2 * time.Second)showGoroutineInfo() // 显示 goroutine 完成后的数量fmt.Println("程序结束")
}
2. Goroutine 的 Panic 处理
package mainimport ("fmt""time"
)func safeworker(id int) {// 使用 defer 和 recover 捕获 panicdefer func() {if r := recover(); r != nil {fmt.Printf("Goroutine %d: 捕获到 panic: %v\n", id, r)}}()fmt.Printf("Goroutine %d: 开始工作\n", id)if id == 2 {panic("模拟错误") // 故意触发 panic}time.Sleep(500 * time.Millisecond)fmt.Printf("Goroutine %d: 正常完成\n", id)
}func unsafeWorker(id int) {fmt.Printf("Unsafe Goroutine %d: 开始工作\n", id)if id == 2 {panic("未处理的 panic") // 这会导致整个程序崩溃}time.Sleep(500 * time.Millisecond)fmt.Printf("Unsafe Goroutine %d: 正常完成\n", id)
}func main() {fmt.Println("=== 安全的 Goroutine 示例 ===")// 启动安全的 goroutine(有 panic 处理)for i := 1; i <= 3; i++ {go safeworker(i)}time.Sleep(1 * time.Second)fmt.Println("所有安全的 Goroutine 处理完成")// 注意:下面的代码会导致程序崩溃,仅作演示// fmt.Println("\n=== 不安全的 Goroutine 示例 ===")// for i := 1; i <= 3; i++ {// go unsafeWorker(i)// }// time.Sleep(1 * time.Second)
}
四、Go 调度器原理
1. GMP 模型
Go 语言的调度器采用 GMP 模型:
- G (Goroutine): 协程,用户态的轻量级线程
- M (Machine): 操作系统线程,真正执行计算的资源
- P (Processor): 处理器,调度的上下文,维护 Goroutine 队列
package mainimport ("fmt""runtime""time"
)func showRuntimeInfo() {fmt.Printf("GOMAXPROCS (P的数量): %d\n", runtime.GOMAXPROCS(0))fmt.Printf("NumCPU (CPU核心数): %d\n", runtime.NumCPU())fmt.Printf("NumGoroutine (当前Goroutine数): %d\n", runtime.NumGoroutine())
}func cpuBoundTask(id int) {fmt.Printf("CPU密集型任务 %d 开始\n", id)// 模拟 CPU 密集型计算count := 0for i := 0; i < 1000000000; i++ {count++}fmt.Printf("CPU密集型任务 %d 完成,计算结果: %d\n", id, count)
}func ioBoundTask(id int) {fmt.Printf("IO密集型任务 %d 开始\n", id)// 模拟 IO 等待time.Sleep(1 * time.Second)fmt.Printf("IO密集型任务 %d 完成\n", id)
}func main() {fmt.Println("=== 运行时信息 ===")showRuntimeInfo()fmt.Println("\n=== CPU密集型任务测试 ===")start := time.Now()// 启动多个 CPU 密集型任务for i := 1; i <= 4; i++ {go cpuBoundTask(i)}time.Sleep(3 * time.Second) // 等待完成fmt.Printf("CPU密集型任务耗时: %v\n", time.Since(start))fmt.Println("\n=== IO密集型任务测试 ===")start = time.Now()// 启动多个 IO 密集型任务for i := 1; i <= 10; i++ {go ioBoundTask(i)}time.Sleep(2 * time.Second) // 等待完成fmt.Printf("IO密集型任务耗时: %v\n", time.Since(start))
}
2. 调度器的工作机制
package mainimport ("fmt""runtime""sync/atomic""time"
)var counter int64func increment(id int) {for i := 0; i < 1000; i++ {atomic.AddInt64(&counter, 1)// 主动让出 CPU 时间片if i%100 == 0 {runtime.Gosched()}}fmt.Printf("Goroutine %d 完成\n", id)
}func main() {fmt.Printf("初始 GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))// 启动多个 goroutine 进行计数numGoroutines := 10for i := 1; i <= numGoroutines; i++ {go increment(i)}// 监控计数器变化for i := 0; i < 10; i++ {time.Sleep(100 * time.Millisecond)fmt.Printf("当前计数: %d, Goroutine数: %d\n", atomic.LoadInt64(&counter), runtime.NumGoroutine())}fmt.Printf("最终计数: %d\n", atomic.LoadInt64(&counter))
}
五、常见陷阱与最佳实践
1. 循环变量陷阱
package mainimport ("fmt""time"
)func demonstrateLoopTrap() {fmt.Println("=== 错误示例:循环变量陷阱 ===")// ❌ 错误:所有 goroutine 可能打印相同的值for i := 0; i < 5; i++ {go func() {fmt.Printf("错误示例 - i = %d\n", i) // 可能全部打印 5}()}time.Sleep(100 * time.Millisecond)fmt.Println("\n=== 正确示例:参数传递 ===")// ✅ 正确:通过参数传递当前值for i := 0; i < 5; i++ {go func(val int) {fmt.Printf("正确示例 - val = %d\n", val)}(i)}time.Sleep(100 * time.Millisecond)fmt.Println("\n=== 正确示例:局部变量 ===")// ✅ 正确:创建局部变量for i := 0; i < 5; i++ {i := i // 创建新的局部变量go func() {fmt.Printf("局部变量示例 - i = %d\n", i)}()}time.Sleep(100 * time.Millisecond)
}func main() {demonstrateLoopTrap()
}
2. Goroutine 泄漏
package mainimport ("context""fmt""runtime""time"
)// ❌ 错误:可能导致 goroutine 泄漏
func leakyGoroutine() {go func() {for {// 无限循环,没有退出条件time.Sleep(1 * time.Second)fmt.Println("泄漏的 goroutine 仍在运行...")}}()
}// ✅ 正确:使用 context 控制 goroutine 生命周期
func controlledGoroutine(ctx context.Context) {go func() {ticker := time.NewTicker(1 * time.Second)defer ticker.Stop()for {select {case <-ctx.Done():fmt.Println("受控的 goroutine 正常退出")returncase <-ticker.C:fmt.Println("受控的 goroutine 正在工作...")}}}()
}func main() {fmt.Printf("初始 Goroutine 数量: %d\n", runtime.NumGoroutine())// 演示 goroutine 泄漏(注释掉以避免实际泄漏)// leakyGoroutine()// 使用 context 控制 goroutinectx, cancel := context.WithTimeout(context.Background(), 3*time.Second)defer cancel()controlledGoroutine(ctx)time.Sleep(4 * time.Second)fmt.Printf("最终 Goroutine 数量: %d\n", runtime.NumGoroutine())
}
3. 性能优化建议
package mainimport ("fmt""runtime""sync""time"
)// 测试不同数量的 goroutine 性能
func benchmarkGoroutines(numGoroutines int) time.Duration {var wg sync.WaitGroupstart := time.Now()for i := 0; i < numGoroutines; i++ {wg.Add(1)go func(id int) {defer wg.Done()// 模拟轻量工作time.Sleep(1 * time.Millisecond)}(i)}wg.Wait()return time.Since(start)
}func main() {fmt.Printf("CPU 核心数: %d\n", runtime.NumCPU())fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))// 测试不同数量的 goroutinetestCases := []int{10, 100, 1000, 10000}for _, num := range testCases {duration := benchmarkGoroutines(num)fmt.Printf("%d 个 Goroutine 耗时: %v\n", num, duration)}// 性能建议fmt.Println("\n=== 性能优化建议 ===")fmt.Println("1. 避免创建过多的 goroutine,考虑使用工作池模式")fmt.Println("2. CPU 密集型任务的 goroutine 数量不应超过 CPU 核心数")fmt.Println("3. IO 密集型任务可以创建更多的 goroutine")fmt.Println("4. 使用 sync.Pool 重用对象,减少 GC 压力")fmt.Println("5. 合理设置 GOMAXPROCS,通常等于 CPU 核心数")
}
七、Goroutine 的应用
以下是一个使用协程解决计算密集型任务效率问题的例子:计算多个整数的素数分解。
不使用协程的下载示例:
package mainimport ("fmt""time"
)// factorize 对一个整数进行素数分解
func factorize(num int) []int {factors := []int{}for i := 2; i*i <= num; i++ {time.Sleep(100 * time.Microsecond) // 模拟耗时任务for num%i == 0 {factors = append(factors, i)num /= i}}if num > 1 {factors = append(factors, num)}return factors
}func main() {numbers := make([]int, 10000)for i := range numbers {numbers[i] = i + 1}start := time.Now()for _, num := range numbers {factors := factorize(num)fmt.Printf("Factors of %d: %v\n", num, factors)}elapsed := time.Since(start)fmt.Printf("Total time without goroutines: %v\n", elapsed)
}
使用协程的素数分解:
package mainimport ("fmt""sync""time"
)// factorize 对一个整数进行素数分解
func factorize(num int) []int {factors := []int{}for i := 2; i*i <= num; i++ {time.Sleep(100 * time.Microsecond) // 模拟耗时任务for num%i == 0 {factors = append(factors, i)num /= i}}if num > 1 {factors = append(factors, num)}return factors
}func main() {numbers := make([]int, 10000)for i := range numbers {numbers[i] = i + 1}var wg sync.WaitGroupstart := time.Now()// 创建一个容量为 len(numbers) 的 channel 用于接收素数分解结果resultCh := make(chan []int, len(numbers))for _, num := range numbers {wg.Add(1)go func(n int) {defer wg.Done()factors := factorize(n)resultCh <- factors}(num)}go func() {wg.Wait()close(resultCh)}()for factors := range resultCh {fmt.Printf("Factors of %d: %v\n", numbers[len(resultCh)], factors)}elapsed := time.Since(start)fmt.Printf("Total time with goroutines: %v\n", elapsed)
}
八、总结
Goroutine 是 Go 语言并发编程的核心,掌握其正确使用方法对于编写高性能 Go 程序至关重要。
核心要点
- 轻量级:Goroutine 比传统线程更轻量,可以创建大量实例
- 简单启动:使用 go 关键字即可启动新的 goroutine
- 调度器:Go 运行时自动管理 goroutine 的调度
- 生命周期:理解 goroutine 的创建、运行和结束过程
最佳实践 - 避免 goroutine 泄漏:确保 goroutine 能够正常退出
- 处理 panic:在 goroutine 中使用 defer/recover 处理异常
- 合理数量:根据任务类型控制 goroutine 数量
- 参数传递:注意循环变量的正确传递方式
注意事项 - 主 goroutine 结束时程序会立即终止
- 未处理的 panic 会导致程序崩溃
- 合理使用同步机制(后续章节会详细介绍)
- 通过掌握这些概念和实践,你将能够有效地使用 Goroutine 来构建高并发的 Go 应用程序
