ELI5 · 说人话 · GO 面试

goroutine 是什么

go 干活() 45 个 goroutine ≈ 90KB

一个 go 字,就多一条自己往下跑的活儿。
它不是操作系统给的线程,是 Go 自己造、自己派活的。

goroutine = Go 自己管的迷你线程

先分清

它跟线程差一层

进程
一整间工厂

自己一块地,跟隔壁工厂互不相干。

线程
厂里一条产线

共用这块地。开几条、谁先开工,全听操作系统排班。

goroutine
轮着干的一份活

产线上排队的活儿。Go 自己排班,哪条产线空了就往哪条上放。

所以 goroutine 不是线程的替代品,是盖在线程上面的一层。真正占着 CPU 干活的还是线程,goroutine 只是被 Go 挪来挪去地往线程上放。

最直观的差别

一个占 8MB,一个占 2KB

线程 goroutine 就这一条 8MB 栈 一开就占满,不管你用不用 开 1000 条 ≈ 8GB 2KB 起,用多少长多少 同样 8MB ≈ 装下 4000 个

线程的栈是操作系统一次划好的一整块,你只用 3KB,剩下的也还给不了别人。goroutine 的栈是 Go 自己在堆上开的,2KB 不够就整块搬到更大的地方去,用完还能缩回来。

一条一条对

七个地方不一样

比什么
线程
goroutine
谁排班
操作系统内核
Go 运行时,全在用户态
起步栈
1~8MB,固定不动
2KB,能长也能缩
创建一个
微秒级,要陷进内核
纳秒级,死了还能回收复用
切一次
约 1 微秒,换一整套上下文
100 纳秒,只存几个寄存器
能开多少
几千条就开始吃力
几十万个很轻松
有没有 ID
有 tid,能标记能绑定
没有,Go 故意不给
怎么说话
共享内存 + 锁
优先 channel 传消息(锁也能用)

「切一次约 100 纳秒」是这堆数字里最要命的那个:线程切换要陷进内核、保存整套上下文;goroutine 切换就是在同一条线程里改几个寄存器,连门都没出

Go 怎么办到的

G、M、P 三个字母

几十万个 goroutine,最后都是被塞进几条线程里轮流跑的。管这件事的,就是 GMP。

全局队列 G G G G G P 本地队列 G G G P 本地队列 G 快空了 偷一半 M OS 线程 M OS 线程 CPU 核 CPU 核
G
goroutine

一份待办的活儿。写一个 go f() 就多一个 G。

M
machine

一条真的操作系统线程。只有它能真正占着 CPU 干活。

P
processor

一张「准你干活」的号牌,自带一个本地待办队列。号牌几张,就最多几条 M 同时在跑 Go 代码。

号牌数量就是 GOMAXPROCS,默认等于 CPU 核数。自己队列干完的 P 不会闲着——先去全局队列拿,还没有就去别的 P 那儿偷一半

拆开看

便宜在哪几处

栈是长出来的

2KB 起。不够了就整块搬到更大的地方,用完还能缩回去。不像线程一上来就划走 8MB。

切换不出门

换一个 goroutine,就是在同一条线程里改几个寄存器,不用陷进内核——虚线那一层根本没跨过去。

多对少地挤

N 个 goroutine 挤在少数几条线程上跑。goroutine 涨到十万,线程数基本不动。

还会被叫停

Go 1.14 起有异步抢占:一个 goroutine 连着跑满 10 毫秒,运行时发个信号把它踢下来,没人能霸着不走。

走一遍

有一个卡住了怎么办

你的某个 goroutine 去读了一个文件,磁盘慢,卡住了 30 毫秒。

  1. 1

    卡住这个 G 发起系统调用,它所在的那条线程 M 也跟着被内核扣住了。

  2. 2

    摘牌运行时把号牌 P 从这条 M 上摘走——线程可以卡,号牌不能跟着陪葬。

  3. 3

    换人P 挂到另一条 M 上:有睡着的就叫醒,没有就新建一条。

  4. 4

    继续本地队列里排着的其它 G 照跑不误,一个都没被这 30 毫秒拖住。

  5. 回来文件读完,那个 G 重新排队等号牌,接着从断的地方往下跑。

如果卡住的原因是 channel 或者锁,那连换线程都省了:G 直接被挂起,M 立刻转头去跑队列里的下一个,一次内核调用都没发生。
网络 I/O 也一样——Go 偷偷把它换成了非阻塞 + 轮询器,所以你写着「阻塞式」的代码,底下其实没有线程真的在阻塞。

别忘了

便宜也会闯祸

开出去容易,收不回来

往一个没人接的 channel 里写,这个 goroutine 就永远挂在那儿——它的栈和它引用的对象一个都回收不掉。所以:开之前先想好它怎么退出,context、close、超时,三选一。查泄漏靠 pprof 看 goroutine 数,-race 是查不出来的。

main 一走,全场散伙

main 函数返回的那一刻,所有 goroutine 当场被砍:不等、不通知、defer 也不跑。所以:用 WaitGroup 或 errgroup 收口

便宜不等于免费

一百万个 × 2KB 起步就是 2GB,栈还会往上长;GC 每一轮都得把所有栈扫一遍。所以:该限流就限流——worker pool,或者拿带缓冲的 channel 当信号量。

换成 goroutine 不等于线程安全

调度归 Go 管,数据竞争还是你的事。两个 goroutine 同时写一个 map,照样直接崩。所以:该加锁加锁,提交前跑一遍 go test -race

面试就这么答

三十秒版本

goroutine 是 Go 运行时自己调度的用户态轻量线程。运行时用 GMP 模型,把 N 个 goroutine 复用到少量 OS 线程上,号牌 P 的数量就是 GOMAXPROCS。

跟线程比,差别集中在三点:栈从 2KB 起、还能伸缩,线程是固定的 1~8MB;创建和切换全在用户态,不用陷内核,切一次约 100 纳秒,线程要 1 微秒;遇到系统调用阻塞,运行时会把 P 从卡住的 M 上摘走换条线程接着跑,channel 阻塞更是连线程都不用换。

所以开几十万个 goroutine 很正常,开几十万条线程不可能。代价是泄漏更隐蔽——每一个 goroutine 都得有明确的退出路径。

线程贵,因为归内核管。
goroutine 便宜,因为归 Go 自己管。