结论:goroutine 初始栈只有 2KB(Go 1.4+,早期 4/8KB),存放在堆上、由运行时管理,函数调用深度不够时按倍增策略扩容(检查点插入 morestack),上限 1GB(64 位)。对比 OS 线程默认 1–8MB 固定栈且由内核调度,goroutine 轻三个数量级,百万级 goroutine 内存占用约 2GB 起而同样数量的线程光栈就要 TB 级。

展开:机制演进:1)1.3 前用分段栈(segmented stack)——按需链接新栈段,但"热分裂"问题(循环边界反复分裂合并)性能抖动大;2)之后改连续栈(contiguous stack)——不够用时分配两倍大的新栈,拷贝旧栈内容并调整其中指针(栈上指针重定位,这是 Go 指针精确性的红利),摊销成本可接受。扩容时机:编译器在每个函数序言插入栈检查(SP 与 stackguard 比较),不足则调 morestack;1.21 起 goroutine 创建即按历史用量预分配,减少早期扩容抖动。为什么敢说百万:除了小栈,GMP 调度让 M 个线程跑 N 个 G(N>>M),阻塞在 channel/IO 的 G 不占线程。易错点:1)无限递归在 Go 不是 stack overflow 崩溃而是栈涨到上限后 fatal error(不可 recover);2)CGO 调用中 Go 栈增长对 C 代码不透明,传给 C 的 Go 指针受 cgo pointer 规则约束;3)goroutine 泄漏时栈内存不回收,百万 goroutine 的前提是没有泄漏。

// runtime/debug.SetMaxStack 可调上限
// 查看当前 goroutine 数:runtime.NumGoroutine()

追问方向:stackguard 与抢占调度的复用(栈检查同时承担抢占点)、栈收缩的时机(GC 扫描时减半回收)。