Go 为什么不怕 goroutine 死循环:抢占不是玄学,是一套停机协议
一个没有函数调用的 for 死循环,为什么没有把 Go 程序和 GC 一起拖死?从同步抢占、asyncPreempt、safe point、sysmon retake 到 GC worker,这篇把 Go 调度器的第三个关键问题讲清楚:正在跑的 goroutine,runtime 到底怎么把它停下来。
一个没有函数调用的 for 死循环,为什么没有把 Go 程序和 GC 一起拖死?从同步抢占、asyncPreempt、safe point、sysmon retake 到 GC worker,这篇把 Go 调度器的第三个关键问题讲清楚:正在跑的 goroutine,runtime 到底怎么把它停下来。
阻塞 syscall 为什么不会拖死整个 Go 程序?net.Conn.Read 为什么不是让一条线程傻等?看懂 syscall handoff、netpoller 和 work stealing,关键是把等待、线程和 P 这张执行许可证拆开。
很多程序员一开始都把写代码当成工作的全部。后来才发现,真正耗精力的不是敲键盘,而是把需求、边界、风险和预期讲清楚。代码能跑只是第一步,别人敢不敢把事情交给你,才是更长期的答案。
channel 导致的 goroutine 泄漏通常分两种方向:sender 发了没人接,或者 receiver 等了没人发。排查时要走出三条线才能锁定代码里的真实责任方。
goroutine 涨了就加 buffer,是把问题从第 101 次发送推迟到第 1001 次。真正的根因是 nil channel、closed panic、sender/receiver 不匹配——这些加 buffer 都解决不了。
channel 不只是队列,Mutex 不只是锁。每个同步原语都在建立 happens-before 关系:send 和 receive 之间有、Unlock 和 Lock 之间有、Once.Do 返回和后续调用之间有。没有这些链,可见性就不存在。
普通变量的先写后读不提供跨 goroutine 的可见性。Go 内存模型的要求很硬核:写入和读取之间必须有 happens-before 证明链,否则代码跑得再好也只是碰巧。
Go 服务内存持续涨,不一定是 GC 的锅。goroutine 泄漏修生命周期,无界缓存修淘汰策略,高分配速率修代码本身——三类问题,三类修法,只有一个共同点:都不该先调 GOGC。
OOMKilled 了别急着调参数。先去 kubectl describe 确认事故现场,再抓两次 heap profile 用 diff_base 看谁在涨,最后才决定动 GOGC 还是 GOMEMLIMIT。
线上 GC 调优最怕先有答案再找证据。正确的顺序是:先确认 GC 是不是问题→看 live heap→看分配速率→再决定参数→最后设回滚线。没有基线的调优,只是玄学。