Go Goroutine
Goroutine 是 Go 语言并发模型的核心。它是一种由 Go 运行时调度的轻量级线程,创建成本极低(初始栈仅 2KB),可以轻松创建数十万个并发执行。通过 go 关键字即可启动一个 goroutine,让函数在后台并发执行。
并发 vs 并行
理解并发和并行的区别是学习 goroutine 的前提:
- 并发(Concurrency):同时处理多件事(关注结构设计),是任务的逻辑组合
- 并行(Parallelism):同时执行多件事(关注运行时执行),需要多核 CPU 支持
Go 的设计理念是"并发编程更容易"。通过 goroutine 和 channel,Go 让开发者可以用简洁的方式构建并发程序,运行时会自动将并发任务映射到可用的 CPU 核心上并行执行。
go 关键字启动 goroutine
在函数调用前加上 go 关键字,即可在新的 goroutine 中执行该函数。
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
for i := 0; i < 3; i++ {
fmt.Printf("Hello from %s (goroutine)\n", name)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// 启动 goroutine
go sayHello("goroutine-1")
go sayHello("goroutine-2")
// 主 goroutine 也在执行
for i := 0; i < 3; i++ {
fmt.Println("Hello from main")
time.Sleep(100 * time.Millisecond)
}
// 等待 goroutine 完成(实际项目中应使用 WaitGroup)
time.Sleep(500 * time.Millisecond)
}每个 goroutine 都是独立执行的。主 goroutine(main 函数)退出时,程序结束,其他 goroutine 也会被终止。
goroutine 的栈空间
goroutine 的栈初始只有 2KB(OS 线程通常是 1-8MB),并且可以动态增长和收缩。这使得 Go 可以轻松创建大量 goroutine。
package main
import (
"fmt"
"runtime"
"sync"
)
func printGoroutineInfo(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Goroutine %d 正在执行\n", id)
}
func main() {
// 查看当前 goroutine 数量
fmt.Printf("初始 goroutine 数: %d\n", runtime.NumGoroutine())
var wg sync.WaitGroup
// 创建 100 个 goroutine
for i := 0; i < 100; i++ {
wg.Add(1)
go printGoroutineInfo(i, &wg)
}
fmt.Printf("创建后 goroutine 数: %d\n", runtime.NumGoroutine())
wg.Wait()
fmt.Printf("完成后 goroutine 数: %d\n", runtime.NumGoroutine())
}100 个 goroutine 仅占用约 200KB 的栈空间,而相同数量的 OS 线程则需要数百 MB。
runtime.GOMAXPROCS
GOMAXPROCS 控制可以同时执行用户代码的 OS 线程数量,默认等于 CPU 核心数。
package main
import (
"fmt"
"runtime"
"time"
)
func worker(id int, duration time.Duration) {
start := time.Now()
for time.Since(start) < duration {
// 模拟计算密集型工作
}
fmt.Printf("Worker %d 完成\n", id)
}
func main() {
fmt.Printf("CPU 核心数: %d\n", runtime.NumCPU())
fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0)) // 0 表示查询当前值
// 设置 GOMAXPROCS(通常不需要手动设置)
runtime.GOMAXPROCS(4)
fmt.Printf("设置后 GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
// 启动 8 个 worker
for i := 0; i < 8; i++ {
go worker(i, 100*time.Millisecond)
}
time.Sleep(200 * time.Millisecond)
}在大多数情况下,保持默认的 GOMAXPROCS 即可。Go 运行时会自动管理线程调度。
GMP 调度模型
Go 的 goroutine 调度使用 GMP 模型:
- G(Goroutine):用户态的轻量级线程,包含执行栈、状态等信息
- M(Machine):操作系统线程,由 OS 调度,负责执行 G
- P(Processor):逻辑处理器,维护一个本地 goroutine 队列。P 的数量等于
GOMAXPROCS
调度流程:
- 每个 P 维护一个本地 G 队列
- M 必须绑定 P 才能执行 G
- 当 M 执行 G 发生阻塞(如系统调用)时,M 和 P 分离,P 会绑定新的 M
- 工作窃取:空闲的 P 会从其他 P 的队列中"窃取" G 来执行,实现负载均衡
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
// 查看调度器统计信息
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
fmt.Printf("Goroutine 数量: %d\n", runtime.NumGoroutine())
fmt.Printf("CPU 核心数: %d\n", runtime.NumCPU())
fmt.Printf("CGo 调用次数: %d\n", stats.NumCgoCall)
// 通过 trace 包可以查看调度细节
// go tool trace 可以可视化 GMP 调度过程
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Goroutine %d 在运行\n", id)
}(i)
}
wg.Wait()
}sync.WaitGroup 等待 goroutine 完成
sync.WaitGroup 是等待一组 goroutine 完成的标准方式。
package main
import (
"fmt"
"sync"
"time"
)
func downloadFile(id int, wg *sync.WaitGroup) {
defer wg.Done() // 函数结束时计数器减 1
fmt.Printf("开始下载文件 %d\n", id)
time.Sleep(time.Duration(id*100) * time.Millisecond) // 模拟下载
fmt.Printf("文件 %d 下载完成\n", id)
}
func main() {
var wg sync.WaitGroup
// 启动多个下载任务
for i := 1; i <= 5; i++ {
wg.Add(1) // 计数器加 1
go downloadFile(i, &wg)
}
// 等待所有 goroutine 完成
wg.Wait()
fmt.Println("所有文件下载完成!")
}WaitGroup 有三个方法:
Add(delta int):计数器增加 deltaDone():等价于Add(-1)Wait():阻塞直到计数器归零
注意:Add 应在 goroutine 启动之前调用,避免竞态条件。
goroutine 泄漏问题
goroutine 泄漏是指 goroutine 永远无法退出,持续占用资源。常见原因包括:
package main
import (
"fmt"
"runtime"
"time"
)
// 泄漏示例 1:channel 阻塞(发送后无人接收)
func leakByChannel() {
ch := make(chan int)
go func() {
ch <- 1 // 永远阻塞,无人接收
}()
}
// 泄漏示例 2:无限循环无退出条件
func leakByLoop() {
go func() {
for {
// 没有退出条件,永远运行
time.Sleep(time.Second)
}
}()
}
// 正确做法:使用 context 或 done channel 控制退出
func safeGoroutine(done <-chan struct{}) {
go func() {
for {
select {
case <-done:
fmt.Println("goroutine 安全退出")
return
default:
// 执行工作
time.Sleep(100 * time.Millisecond)
}
}
}()
}
func main() {
leakByChannel()
leakByLoop()
done := make(chan struct{})
safeGoroutine(done)
time.Sleep(300 * time.Millisecond)
fmt.Printf("当前 goroutine 数: %d\n", runtime.NumGoroutine())
close(done) // 通知 goroutine 退出
time.Sleep(200 * time.Millisecond)
fmt.Printf("退出后 goroutine 数: %d\n", runtime.NumGoroutine())
}防止 goroutine 泄漏的最佳实践:
- 为每个 goroutine 设计明确的退出机制(done channel 或 context)
- 使用
context.WithTimeout设置超时 - 使用
runtime.NumGoroutine()监控 goroutine 数量 - 避免在 goroutine 中向无接收者的 channel 发送数据
总结
Goroutine 是 Go 并发的基石。它由 Go 运行时调度,初始栈仅 2KB,可以轻松创建数十万个。通过 go 关键字启动,使用 sync.WaitGroup 等待完成。Go 的 GMP 调度模型将 goroutine 高效映射到 OS 线程上执行,支持工作窃取实现负载均衡。使用 goroutine 时需注意避免泄漏,始终为 goroutine 提供退出机制。goroutine 配合 channel 构成了 Go 强大的并发编程能力。