面试里问“怎么查当前有多少个协程在执行”,表面是在考 API,实质上是在看你是否理解 Go runtime 的调度方式,以及你能不能把这个函数放到排障场景里解释清楚。本文先给出直接答案,再结合调度模型、示例代码和常见追问,梳理 `runtime.NumGoroutine()` 到底统计了什么、能解决什么问题。
直接答案:用 runtime.NumGoroutine()
在 Go 里,查询当前 goroutine 数量的标准方式很直接:
import "runtime"
num := runtime.NumGoroutine()
fmt.Printf("当前协程数量: %dn", num)
runtime.NumGoroutine() 返回的是当前 Go 程序中存活的 goroutine 总数。这里的“存活”不只包含正在 CPU 上运行的 goroutine,还包括已经创建但尚未真正运行的、处于就绪状态的,以及因为 channel、syscall、time.Sleep 等原因暂时阻塞的 goroutine。
所以,严格来说,这个函数查到的不是“此刻正在执行”的精确数量,而是当前运行时里存在的 goroutine 数量。面试时如果能主动补上这一点,回答会明显更完整。
为什么这个问题常被拿来面试
面试官通常不是只想听你说出一个函数名,而是想借这个入口继续判断你对并发模型的理解深度。常见考察点主要有三个:
- 你是否知道 goroutine 的调度由 Go runtime 管理,而不是一创建就对应一个操作系统线程。
- 你是否清楚 goroutine 和线程的区别,尤其是创建成本、切换成本和调度位置的差异。
- 你是否在实际工作里用过 goroutine 数量监控,排查过协程泄漏、阻塞或死锁问题。
也就是说,这类题的关键不在“会不会背答案”,而在于你能不能从 API 往下解释到运行时机制,再往上落到工程实践。
它统计的到底是什么:从 goroutine 调度说起
当你使用 go 关键字启动一个 goroutine 时:

go func() {
// 模拟工作
time.Sleep(2 * time.Second)
}()
这段代码并不意味着 Go 会立刻创建一个新的线程来执行它。更准确地说,Go 调度器会先把这个 goroutine 放进运行队列,随后由 M(Machine,操作系统线程)去取任务执行。
因此,runtime.NumGoroutine() 统计的是整个运行时视角下“还活着的 goroutine”,通常包括几类:
- 正在运行的 goroutine
- 处于可运行状态、等待被调度的 goroutine
- 因为 channel 操作、syscall、
time.Sleep等原因阻塞的 goroutine - 刚创建、还没轮到执行的 goroutine
这一点很重要,因为很多人第一次接触这个函数时,容易把它理解成“当前 CPU 上真正并行执行的协程数”。这两者不是一个概念。前者是 goroutine 总量统计,后者则与调度状态、线程数和 GOMAXPROCS 等因素相关。
用一个例子看清数量变化
下面这个例子能直观看到 goroutine 数量的变化过程:

package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func main() {
fmt.Printf("主函数启动,当前协程数: %dn", runtime.NumGoroutine())
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("协程 %d 开始执行n", id)
time.Sleep(1 * time.Second)
fmt.Printf("协程 %d 执行完成n", id)
}(i)
}
fmt.Printf("启动 5 个协程后,当前协程数: %dn", runtime.NumGoroutine())
wg.Wait()
fmt.Printf("所有协程执行完毕,当前协程数: %dn", runtime.NumGoroutine())
}
运行结果类似这样:
主函数启动,当前协程数: 1
启动 5 个协程后,当前协程数: 6
协程 0 开始执行
协程 2 开始执行
协程 3 开始执行
协程 4 开始执行
协程 1 开始执行
协程 2 执行完成
协程 0 执行完成
协程 4 执行完成
协程 3 执行完成
协程 1 执行完成
所有协程执行完毕,当前协程数: 1
这个结果可以这样理解:
- 程序刚启动时,只有主 goroutine,也就是
main,所以数量是 1。 - 创建 5 个子 goroutine 后,总数变成 6。这里统计的是主 goroutine 加上 5 个新建 goroutine,而不是“同时跑在 CPU 上的 6 个任务”。
- 等到 5 个子 goroutine 全部退出后,数量又回到 1。
如果在面试里配合这个例子解释,通常就能把“会调用函数”和“理解函数语义”区分开来。
面试追问通常会落到哪里
协程泄漏怎么排查
协程泄漏指的是 goroutine 被创建出来后,迟迟无法退出,导致 goroutine 数量持续增长。这个场景下,runtime.NumGoroutine() 就很适合做基础监控:

// 定期打印协程数量,用于监控
func monitorGoroutines() {
ticker := time.NewTicker(30 * time.Second)
for range ticker.C {
fmt.Printf("[监控] 当前协程数量: %dn", runtime.NumGoroutine())
}
}
如果一个服务在稳定流量下 goroutine 数持续上涨,且没有明显回落,通常就该怀疑存在泄漏。排查时可以优先看几类常见问题:channel 一直阻塞、协程等待条件永远不成立、死锁、遗留的 time.Sleep 逻辑没有清理掉,或者某些后台任务没有退出路径。
只知道数量还不够,怎么继续看堆栈
数量异常通常只是信号,真正定位问题还需要知道这些 goroutine 卡在什么地方。Go 提供了 pprof 来做这件事:
import _ "net/http/pprof"
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// 访问 http://localhost:6060/debug/pprof/goroutine?debug=1
// 查看所有协程的堆栈信息
}
当你发现 goroutine 数量异常增长时,可以继续查看 goroutine profile,确认大量协程是不是都卡在同一个 channel、网络调用或锁等待上。这才是线上定位问题时真正有价值的下一步。
goroutine 和线程有什么区别
这也是高频追问。可以用最简洁的方式回答:
- 线程:操作系统级别的执行单元,创建和上下文切换成本都更高。
- 协程:用户态的轻量级执行单元,由 Go runtime 负责调度。
也正因为如此,一个 Go 程序往往可以轻松创建上万个 goroutine,而底层线程数仍然保持在相对有限的范围内。Go runtime 会把大量 goroutine 映射到少量线程上执行,从而兼顾并发能力和资源开销。
想答得更深,可以补哪些调度器知识
如果题目继续往下问,或者你想把回答从“会用”提升到“理解机制”,可以补充调度器层面的几个关键词。
G-P-M 模型
Go 调度器常见的描述方式是 G-P-M:
- G:Goroutine,真正要执行的任务。
- P:Processor,调度所需的逻辑处理器资源。
- M:Machine,对应操作系统线程。
goroutine 不是直接挂在线程上跑,而是要先和 P 关联,再由 M 执行。面试时能把这一层说清楚,通常已经超出单纯 API 题的范围了。
工作窃取
当某个 P 的本地运行队列空了,它不会立刻闲着,而是可能从其他 P 的队列里“偷”一部分任务来执行。这就是常说的工作窃取。它的作用是尽量让各个处理器资源保持忙碌,减少调度不均衡带来的浪费。
GOMAXPROCS 的作用
GOMAXPROCS 用来设置 P 的数量,也就是能同时执行 Go 代码的并行度上限。它不会改变 runtime.NumGoroutine() 统计 goroutine 总数的方式,但会影响调度器如何分配执行资源。
// 查看和设置 GOMAXPROCS
fmt.Printf("GOMAXPROCS: %dn", runtime.GOMAXPROCS(0))
runtime.GOMAXPROCS(4) // 设置为 4
面试里如果能补一句“`NumGoroutine` 看的是 goroutine 总量,`GOMAXPROCS` 影响的是并行执行能力”,基本就把两个常见概念分开了。
结论:别把它当成单纯的 API 背诵题
runtime.NumGoroutine() 确实是查询 goroutine 数量的标准答案,但真正有区分度的地方在于,你是否知道它统计的是所有存活 goroutine,而不是此刻占用 CPU 的执行数。
再往前一步,它还是排查 goroutine 泄漏的常用入口;再往深一点,它连接的是 Go runtime 的调度模型、G-P-M 结构和并发设计思路。面试里把这几层讲顺,回答就不再只是“我记住了一个函数”。
思考题:如果协程中使用了 runtime.Gosched() 主动让出执行权,会不会影响 runtime.NumGoroutine() 的返回值?理解这个问题时,关键仍然是区分“调度状态变化”和“goroutine 是否存活”。













