Go 面试题:如何查询当前协程数量,以及这道题真正想考什么

闻溪
闻溪
发布时间:2026-08-25| 151人浏览过
原创

前言

面试里问“如何查询当前有多少个协程”,看起来只是一个函数调用题,真正考的却是你对 Go runtime 调度模型和并发排障思路的理解。只会回答 `runtime.NumGoroutine()` 还不够,关键在于说清它统计的范围、与线程的区别,以及它在协程泄漏监控中的实际价值。

面试里问“怎么查当前有多少个协程在执行”,表面是在考 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 协程数量统计示意图,以便签卡片展示 NumGoroutine 统计范围与常见状态。
NumGoroutine 统计范围图`runtime.NumGoroutine()` 统计的是存活 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 数量的变化过程:

Go 协程数量变化示意图,用时间顺序卡片展示主协程、创建 5 个子协程、执行结束后的数量回落。
示例程序中的协程数量变化从 1 到 6 再回到 1,能直观看到 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() 就很适合做基础监控:

Go 协程泄漏排查示意图,用监控与堆栈两组卡片展示 NumGoroutine 和 pprof 的配合方式。
协程泄漏的发现与定位数量监控适合发现异常,堆栈信息适合定位卡点,两者结合才是排查协程泄漏的常见路径。
// 定期打印协程数量,用于监控
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 是否存活”。

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。