首页 / Go 语言入门教程 / goroutine 基础

Go 语言入门教程

goroutine 基础

本教程共 80 篇 · 第 55 篇 · 更新于 2026-07-27 · 约 8 分钟阅读

GoGo 入门教程goroutine并发GMPGOMAXPROCS协程

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 的执行顺序,需要顺序就得显式同步。

Note

Go 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 之间通信和同步的主力工具。