
面试官:"说说 Go 语言中如何查询当前有多少个协程在执行?"
这个问题看似简单,实际上考察的是候选人对 Go 运行时调度模型的掌握程度。根据我的理解,这篇文章来聊聊这个面试题背后的知识点。
在 Go 语言中,查询当前正在执行的协程数量非常简单,只需要调用一个函数:
import "runtime"
num := runtime.NumGoroutine()
fmt.Printf("当前协程数量: %d\n", num)
runtime.NumGoroutine() 会返回当前 Go 程序中存在的 goroutine 数量,包括正在运行的和你刚刚创建但还没来得及运行的。
表面上是问函数调用,实际上他想考察的是:
让我们逐一展开。
当你在 Go 中使用 go 关键字启动一个协程时:
go func() {
// 模拟工作
time.Sleep(2 * time.Second)
}()
这段代码并不会立即创建线程来执行。Go 的调度器会将这个协程放入运行队列,等待 M(Machine,操作系统线程)从队列中取出执行。
runtime.NumGoroutine() 返回的是所有存活的 goroutine 总数,包括正在运行的、就绪的、阻塞的(如 channel 操作、syscall、time.Sleep)以及刚启动还没来得及调度的。它并不是单纯指"正在 CPU 上执行"的数量。
让我们通过一个完整的例子来理解:
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func main() {
fmt.Printf("主函数启动,当前协程数: %d\n", runtime.NumGoroutine())
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
gofunc(id int) {
defer wg.Done()
fmt.Printf("协程 %d 开始执行\n", id)
time.Sleep(1 * time.Second)
fmt.Printf("协程 %d 执行完成\n", id)
}(i)
}
fmt.Printf("启动 5 个协程后,当前协程数: %d\n", runtime.NumGoroutine())
wg.Wait()
fmt.Printf("所有协程执行完毕,当前协程数: %d\n", runtime.NumGoroutine())
}
运行结果类似这样:
主函数启动,当前协程数: 1
启动 5 个协程后,当前协程数: 6
协程 0 开始执行
协程 2 开始执行
协程 3 开始执行
协程 4 开始执行
协程 1 开始执行
协程 2 执行完成
协程 0 执行完成
协程 4 执行完成
协程 3 执行完成
协程 1 执行完成
所有协程执行完毕,当前协程数: 1
可以看到:
协程泄漏是指协程被创建后永远无法结束,导致协程数持续增长。这时 runtime.NumGoroutine() 就派上用场了:
// 定期打印协程数量,用于监控
func monitorGoroutines() {
ticker := time.NewTicker(30 * time.Second)
for range ticker.C {
fmt.Printf("[监控] 当前协程数量: %d\n", runtime.NumGoroutine())
}
}
如果发现协程数量持续增长没有下降,就需要检查是否存在死锁、channel 阻塞或 time.Sleep 忘记删除等常见问题。
除了数量,你可能还想知道这些协程都在做什么。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
// 查看所有协程的堆栈信息
}
这是必问的基础题。简单来说:
一个 Go 程序可以轻松创建上万个协程,但线程数通常只有几百个。Go 的调度器会自动将协程分配到有限的线程上执行,实现高效的并发。
如果你能说出下面这些,肯定能给面试官留下深刻印象:
GOMAXPROCS 设置 P 的数量,影响调度器的行为// 查看和设置 GOMAXPROCS
fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
runtime.GOMAXPROCS(4) // 设置为 4
runtime.NumGoroutine() 是 Go 语言中查询协程数量的标准方式,背后反映的是 Go 运行时调度系统的设计理念。掌握这个知识点,不仅是应对面试,更是实际工作中排查并发问题的基本功。
下次面试官问到这个问题,你可以自信地说出:不仅仅是调用一个函数那么简单,它背后是 Go 精心设计的调度模型。
思考题:如果协程中使用了 runtime.Gosched() 让出执行权,会影响 runtime.NumGoroutine() 的返回值吗?