知其然,知其所以然
Go 内部原理 2026-10-10

PGO:用 profile 引导编译优化,真实能快多少

在 Go 1.27.1(arm64)上真实测 PGO:用五种工作负载的 CPU profile 合并成 default.pgo 重新编译,编译器把代价 146 的 score 在热调用点内联、把接口调用的热目标内联;但 5 种工作负载里只有 regexp 明显变快(471 → 433 ms,8.1%),接口调用、直接调用、compress/flate 几乎没变,接口加较长函数的组合反而慢 2.0%——PGO 不保证变快;-pgo=auto 的默认行为。

Go PGO profile 内联 去虚拟化 真实验证 动画演示
Go 内部原理 2026-10-10

三色标记与写屏障:并发标记期间新指针怎么不丢对象

在 Go 1.27.1 里用 gctrace、源码摘录、汇编与基准验证三色标记与写屏障:一次 GC 由清扫终止 STW、并发标记、标记终止 STW 三段组成(STW 最大 0.045 与 0.120 ms);混合写屏障同时 shade 被覆盖的指针与新指针;指针赋值的汇编里检查 runtime.writeBarrier 标志并调用 gcWriteBarrier2;标记阶段里指针写从 2.37 ns 变成 17.59 ns,整数写不变;默认的 greenteagc 实现比 nogreenteagc 的 GC CPU 时间少约 23%。

Go GC 三色标记 写屏障 gctrace 真实验证 动画演示
Go 内部原理 2026-10-10

GC 触发与 GOGC:堆增长到什么程度会触发下一轮

在 Go 1.27.1 里验证 GC 的触发:刚做完一次 GC 时堆目标 = 存活堆 × (1 + GOGC/100)(6 种 GOGC 的预测误差 ≤ 1.5 MB);运行中分配越快目标越大;GOGC 从 25 到 800 时 GC 次数 313 → 32、吞吐 44.5 → 177.7 M 次/秒、峰值堆 69 → 912 MB;GOGC=off 一秒内堆涨到 5952 MB;debug.SetGCPercent 运行时生效;最小堆目标 4 MB 下每秒 1306 次 GC;分配 goroutine 增多时 mark assist 从 6 ms 涨到 2218 ms。

Go GC GOGC pacer 堆目标 mark assist 真实验证 动画演示
Go 内部原理 2026-10-10

GOMEMLIMIT:软内存上限怎么改变 GC 的节奏

在 Go 1.27.1 里实测 GOMEMLIMIT:存活堆 200 MiB 时不设限制峰值 609 MB、吞吐 109.6 M/s,限制 300 MiB 时峰值 299 MB、吞吐 44.7 M/s、GC 占 52% CPU(GOGC 开不开都一样);限制只比存活堆多一点时吞吐降到 11.2 M/s;限制低于存活堆时进程没被杀、内存仍超限(273 MB)、GC 占 57.6%;GOMEMLIMIT 只认整数与 B/KiB/MiB/GiB/TiB。

Go GC GOMEMLIMIT 软内存上限 真实验证 动画演示
Go 内部原理 2026-10-10

GC 停顿:STW 到底停多久,哪几个阶段要停

在 Go 1.27.1(arm64,10 个 P)里量 GC 停顿:STW 停顿不随堆(10 MB 到 1 GB)或 goroutine 数(最多 50 万)变长,直方图上界都在几百微秒以内;但一个每 1 ms 睡一次的 goroutine 看到的 p99 多余延迟从 0.93 ms 涨到 26–27 ms(存活堆 500 MB 以上),GOMAXPROCS=1 时 p50 就是 30 ms;对照实验显示只有一个 goroutine 在做 GC、没有别的负载时,仍有约 30 ms 的 p99 延迟。

Go GC STW 停顿 延迟 真实验证 动画演示
Go 内部原理 2026-10-10

内存分配器:mcache、mcentral、mheap 与 size class

在 Go 1.27.1 里用 MemStats.BySize、堆增量与基准测试验证内存分配器:68 个 size class(8 到 32768 字节,页 8 KiB);请求大小向上取整到 class(17 字节 → 24,1030 → 1152);小于 16 字节的无指针对象被 tiny 分配器合并(1 字节对象只占 1 字节);含指针的对象大于 512 字节要多 8 字节 malloc header(576 字节含指针占 640);大于 32 KiB 按 8 KiB 页取整;分配成本从 8 字节的 7 ns 到 256 KiB 的 8.4 µs,并且在 128、192、256 字节处出现无法解释的跳变。

Go 内存分配器 size class tiny 分配器 malloc header 真实验证 动画演示
Go 内部原理 2026-10-10

goroutine 栈增长:从 2KB 到复制整个栈的过程

在 Go 1.27.1 里实测 goroutine 栈:初始约 2 KiB(10 万个阻塞的 goroutine 每个约 2050 字节),递归时栈大约每次翻倍并被整体拷贝(20 万层递归搬动 14 次、共 20.57 ms);函数返回后栈不立即变小,每次 GC 约减半(32.3 MiB → 0.53 MiB);第一次深递归 5.5–7.8 ms,栈已够大时 0.73 ms;无限递归在 1 GB 上限处 fatal error;函数序言的栈检查与 //go:nosplit 链的链接限制。

Go goroutine 栈增长 栈收缩 morestack 真实验证 动画演示
Go 内部原理 2026-10-10

goroutine 的成本:创建一个要多少内存、多少时间

在 Go 1.27.1(arm64 macOS)里量 goroutine 的成本:一个阻塞的 goroutine 约 2.7–2.8 KB(栈 2050 字节 + 堆里的 g 结构体等约 650 字节),50 万个共 1320 MiB;创建并跑完一个约 346 ns(一次 channel 收发 131 ns);两个 goroutine 经 channel 来回一次 189–254 ns;对照 OS 线程创建 22–122 µs、每个约 57 KiB RSS,本机上限 4096;Go 的 SetMaxThreads 超限会 fatal error: thread exhaustion。

Go goroutine 内存 调度 线程 真实验证 动画演示
Go 内部原理 2026-10-10

goroutine 泄漏:怎么发现、怎么定位到那一行

用 Go 1.27.1 复现七种 goroutine 泄漏(发送无人接收、接收无人发送、nil channel、重复 Lock、WaitGroup 永不 Done、空 select、等全局 channel)各 100 个,NumGoroutine 从 1 变成 701;goroutine profile(debug=1)把它们聚合到 main.go 的具体行,debug=2 给出每个的等待原因;Go 1.27.1 新增的 goroutineleak profile 通过 GC 报告其中 598 个(等全局 channel 的不报);两种修好的写法(缓冲 channel、context 取消)不留下 goroutine。

Go goroutine 泄漏 pprof goroutineleak 真实验证 动画演示
Go 内部原理 2026-10-10

工作窃取:空闲的 P 怎么从别的 P 偷 goroutine

在 Go 1.27.1 里验证调度器的工作窃取:同一个 goroutine 创建的 200 个任务,GOMAXPROCS 从 1 到 10 几乎线性加速(1000 → 105 ms,9.52 倍);用 GODEBUG=schedtrace=1,scheddetail=1 看到创建者所在 P 的本地队列放不下的部分进入全局队列(774 个),空闲 P 一次窃取约 127 个(约一半);并摘录 runqgrab 的 n = n - n/2 与 stealTries = 4 等源码。

Go 调度器 工作窃取 GMP schedtrace 真实验证 动画演示
Go 内部原理 2026-10-10

网络轮询器:阻塞的网络 IO 为什么不占线程

在 Go 1.27.1(macOS,kqueue)里验证网络轮询器:2000 条 TCP 连接各有一个 goroutine 阻塞在 Read 上,线程数只从 5 增加到 12,goroutine 状态是 [IO wait],进程里只有一个 KQUEUE 描述符;同样 200 个阻塞读,走 os.Pipe 只多 1 个线程,直接 syscall.Read 阻塞描述符多 198 个线程([syscall] 状态);读超时由轮询器的定时器实现。

Go netpoller 网络轮询器 kqueue IO 真实验证 动画演示
Go 内部原理 2026-10-10

系统调用阻塞时的 P 移交:sysmon 在什么时候介入

在 Go 1.27.1 里测系统调用阻塞时 P 的移交:只有 1 个 P 时,一个 goroutine 阻塞在 syscall.Syscall6 的系统调用里(哪怕长达 100 ms),另一个 goroutine 只等 13–65 µs 就拿到 P;改用不通知运行时的 RawSyscall6,另一个 goroutine 要等到系统调用结束;有空闲 P 时等待更短(4–36 µs);并摘录 sysmon 的 20 µs 起步、至多 10 ms 的睡眠间隔与 retake 的条件。

Go 调度器 系统调用 sysmon handoff 真实验证 动画演示
京ICP备2026067502号-1京公网安备11010502063487号