goroutine 基础
本教程共 80 篇 · 第 55 篇 · 更新于 2026-07-27 · 约 8 分钟阅读
55. goroutine 基础
本节目标:学会用
go关键字启动 goroutine,理解它为什么轻量,搞懂 GMP 调度模型和 GOMAXPROCS 的作用。
什么是 goroutine
goroutine(协程)是 Go 里的轻量级线程。你可以把它想成一段「可以和别人同时跑」的代码。普通函数调用是排队执行,goroutine 则可以并发执行。
创建 goroutine 简单到离谱,一个 go 关键字就行:
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("hello from goroutine")
}
func main() {
go sayHello() // 启动一个 goroutine
go func() { // 也可以用匿名函数
fmt.Println("hello from anonymous")
}()
time.Sleep(time.Second) // 等一下子协程
fmt.Println("main done")
}
go 后面必须跟一个函数调用。函数的参数会在当前 goroutine 里先算好,函数体在新的 goroutine 里跑。
为什么说它轻量
操作系统线程创建一个要几 MB 栈内存,切换要进内核。goroutine 不一样:
- 初始栈只有 2KB 左右,按需增长
- 创建和切换都在用户态,由 Go 运行时调度
- 一台机器开几十万个 goroutine 都不费劲
这也是 Go 并发性能好的关键原因之一。你不需要心疼 goroutine 的数量,该开就开。
一个常见的坑
把上面例子的 time.Sleep 去掉试试:
func main() {
go sayHello()
fmt.Println("main done")
}
很可能 hello from goroutine 一行都打印不出来。原因是主 goroutine(main 函数)跑完就退出了,子 goroutine 还没来得及执行就被一并干掉。
Warning主 goroutine 退出时,所有子 goroutine 会立即终止。
time.Sleep只是临时凑合的办法,正式代码请用 channel 或sync.WaitGroup来同步,后面几节会讲。
goroutine 的执行顺序
goroutine 是并发执行的,调度器决定谁先跑谁后跑,顺序不可预测:
func main() {
for i := 0; i < 5; i++ {
go fmt.Println(i)
}
time.Sleep(time.Second)
}
输出可能是 2 0 4 1 3,也可能每次都不一样。别依赖 goroutine 的执行顺序,需要顺序就得显式同步。
NoteGo 1.22 起,
for循环变量每次迭代都会新建一份。所以上面的i在每个 goroutine 里都是独立的值,不会再出现旧版本「全是 5」的问题。但执行顺序仍然不确定。
GMP 调度模型
goroutine 这么多,谁来安排它们在 CPU 上跑?答案是 Go 的 GMP 调度器。三个字母代表三个角色:
- G(Goroutine):你写的协程
- M(Machine):操作系统线程,真正干活的
- P(Processor):逻辑处理器,持有可运行的 G 队列
简单理解:M 是工人,P 是工人的工位,G 是工位上的任务。工人必须坐在工位上才能干活。
G G G G G G G G G
| | |
P --- P --- P
| | |
M M M
(OS线程) (OS线程) (OS线程)
P 的数量决定了能同时执行 goroutine 的线程数上限。M 的数量可以比 P 多(比如遇到系统调用时会新开 M),但同一时刻只有 GOMAXPROCS 个 M 在跑用户代码。
GOMAXPROCS
GOMAXPROCS 就是 P 的数量,默认值是机器的 CPU 逻辑核数。
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) // 传 0 表示只查询不修改
runtime.GOMAXPROCS(2) // 设置为 2
fmt.Println("now:", runtime.GOMAXPROCS(0))
}
也可以用环境变量设置:
GOMAXPROCS=4 go run main.go
Tip一般不需要手动改
GOMAXPROCS。Go 1.25 起在容器环境下会自动识别 CPU 限制(之前默认取宿主机核数,容器里会偏大)。只有在特殊调优场景才手动设置。
什么时候会切换
goroutine 不是「同时」跑的,而是轮流占用 CPU。以下情况会触发调度切换:
- channel 发送或接收时阻塞
- 主动调用
runtime.Gosched()让出 - 系统调用(如文件 IO)阻塞
- 时间片用完被抢占(Go 1.14 起支持异步抢占)
理解这点很重要:并发不等于并行。单核机器上 goroutine 也能并发(交替执行),但谈不上并行。并行需要多核加上 GOMAXPROCS > 1。
小结
go+ 函数调用 = 启动 goroutine- goroutine 轻量,初始 2KB 栈,可大量创建
- 主 goroutine 退出会带走所有子 goroutine
- GMP 模型:G 是协程、M 是线程、P 是处理器
GOMAXPROCS控制 P 数量,默认等于 CPU 核数
下一节我们学 channel,它是 goroutine 之间通信和同步的主力工具。