<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Go on Zampo Blog</title><link>https://blog.cpdd.fyi/tags/go/</link><description>Recent content in Go on Zampo Blog</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Fri, 22 May 2026 16:40:00 +0800</lastBuildDate><atom:link href="https://blog.cpdd.fyi/tags/go/index.xml" rel="self" type="application/rss+xml"/><item><title>ready=true 不是发布：Go 内存模型真正要你保证什么</title><link>https://blog.cpdd.fyi/posts/go-memory-model/</link><pubDate>Fri, 22 May 2026 16:40:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-memory-model/</guid><description>&lt;p&gt;线上偶现一个很烦的 bug：配置已经加载了，日志也打印了“ready”，但另一个 goroutine 读到的还是旧值。&lt;/p&gt;
&lt;p&gt;代码看起来没什么毛病：先写数据，再把 &lt;code&gt;ready&lt;/code&gt; 置成 &lt;code&gt;true&lt;/code&gt;。读的一侧先等 &lt;code&gt;ready&lt;/code&gt;，再读数据。人脑看这段逻辑，很容易得出一个结论：既然 &lt;code&gt;ready&lt;/code&gt; 已经是 &lt;code&gt;true&lt;/code&gt;，前面的数据当然也该准备好了。&lt;/p&gt;
&lt;p&gt;问题就在这里。&lt;/p&gt;
&lt;p&gt;在 Go 里，&lt;code&gt;ready=true&lt;/code&gt; 不等于发布。普通变量上的“先写后读”，也不等于 goroutine 之间有可见性保证。&lt;/p&gt;
&lt;p&gt;并发安全不是让代码看起来有先后，而是让规范承认它有先后。&lt;/p&gt;
&lt;p&gt;这篇是 Go 内存模型系列的第一篇。它不打算把你拖进 CPU 指令、缓存一致性协议和汇编细节里。我们先解决一个更常见、更工程化的问题：goroutine A 写了变量，goroutine B 什么时候保证能看到？&lt;/p&gt;
&lt;p&gt;答案只有一个：两边要存在 happens-before 关系。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-memory-model/happens-before.svg" alt="happens-before 关系示意图"&gt;&lt;/p&gt;
&lt;h2 id="最危险的代码通常看起来最顺"&gt;最危险的代码，通常看起来最顺&lt;/h2&gt;
&lt;p&gt;先看这段很多人都写过的代码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;payload&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;直觉上，写入顺序很清楚：&lt;code&gt;data&lt;/code&gt; 先被赋值，&lt;code&gt;ready&lt;/code&gt; 后变成 &lt;code&gt;true&lt;/code&gt;。读的一侧又是看到 &lt;code&gt;ready&lt;/code&gt; 才打印 &lt;code&gt;data&lt;/code&gt;，似乎不会出错。&lt;/p&gt;</description></item><item><title>Go 服务被 OOMKilled，别先怪 GC</title><link>https://blog.cpdd.fyi/posts/go-gc-debugging/</link><pubDate>Fri, 22 May 2026 00:01:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-gc-debugging/</guid><description>&lt;p&gt;服务又被 OOMKilled 了。&lt;/p&gt;
&lt;p&gt;Pod 刚重启，群里已经有人开始翻 GC 日志。有人说 &lt;code&gt;GC CPU&lt;/code&gt; 到了 20%，有人说把 &lt;code&gt;GOGC&lt;/code&gt; 调低一点，还有人建议直接上 &lt;code&gt;GOMEMLIMIT&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这些动作不一定错，但顺序错了。&lt;/p&gt;
&lt;p&gt;OOMKilled 是结果，不是根因。它只说明容器被内核杀掉了，不说明是谁把内存吃掉了。可能是 Go heap 真的在涨，可能是 goroutine 泄漏把栈和引用一起拖大，可能是 &lt;code&gt;alloc_space&lt;/code&gt; 很高导致 GC 忙，可能是 RSS 里有 cgo、mmap、tmpfs，也可能只是容器 limit 给得太紧。&lt;/p&gt;
&lt;p&gt;Go 服务的内存问题，最怕一句话定性。&lt;/p&gt;
&lt;p&gt;一旦你把所有问题都叫“GC 问题”，排查就会变成调参赌博：今天调 &lt;code&gt;GOGC&lt;/code&gt;，明天加 limit，后天又开始怀疑 pprof 没抓准。真正稳定的做法，是先把几条线拆开：Go 堆、Go runtime 管理的内存、RSS、容器 limit，以及业务生命周期。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-gc-debugging/debug-flow.png" alt="Go GC 排查流程"&gt;&lt;/p&gt;
&lt;h2 id="第一步不是调参数是确认谁杀了你"&gt;第一步不是调参数，是确认谁杀了你&lt;/h2&gt;
&lt;p&gt;线上看到内存上涨，第一件事不是打开 pprof，也不是改 &lt;code&gt;GOGC&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;先确认事故事实。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl describe pod &amp;lt;pod&amp;gt; -n &amp;lt;ns&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl logs &amp;lt;pod&amp;gt; -n &amp;lt;ns&amp;gt; --previous
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl top pod &amp;lt;pod&amp;gt; -n &amp;lt;ns&amp;gt; --containers
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl get pod &amp;lt;pod&amp;gt; -n &amp;lt;ns&amp;gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{.status.containerStatuses[*].lastState.terminated.reason}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;你要先回答几个问题：&lt;/p&gt;</description></item><item><title>Go GC 占 CPU 5%，到底要不要调 GOGC？</title><link>https://blog.cpdd.fyi/posts/go-gc-tuning/</link><pubDate>Thu, 21 May 2026 23:30:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-gc-tuning/</guid><description>&lt;p&gt;服务 p99 每隔几十秒抖一下。&lt;/p&gt;
&lt;p&gt;监控里 GC CPU 大概 5%，不算夸张，但曲线很准：GC 一来，延迟就往上冒。群里很快有人提议：把 &lt;code&gt;GOGC&lt;/code&gt; 从 100 调到 200，先让 GC 少跑几次。&lt;/p&gt;
&lt;p&gt;这个动作看起来很合理。&lt;/p&gt;
&lt;p&gt;CPU 下来了，GC 日志没那么密了，p99 也安静了几天。然后另一个问题来了：容器内存开始逼近 limit，某个高峰期被 OOMKilled。大家又把 &lt;code&gt;GOGC&lt;/code&gt; 调回去，延迟抖动也跟着回来。&lt;/p&gt;
&lt;p&gt;这就是很多 Go GC 调优的真实状态：不是不会调参数，而是没想清楚自己到底在买什么。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GOGC&lt;/code&gt; 调的是账期，不是魔法。&lt;/p&gt;
&lt;p&gt;调高它，相当于允许堆多长一段时间，用内存换 CPU；调低它，相当于让 GC 更勤快，用 CPU 换内存。至于 &lt;code&gt;GOMEMLIMIT&lt;/code&gt;，它不是另一个版本的 &lt;code&gt;GOGC&lt;/code&gt;，而是在告诉 runtime：这个进程由 Go 管的内存，最好别越过这条线。&lt;/p&gt;
&lt;p&gt;所以这篇不打算把 GC 参数逐个解释一遍。线上真正需要的是一条决策路径：要不要调，调什么，调多少，什么时候停手。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-gc-tuning/gogc-heap-curve.png" alt="GOGC 与堆目标关系"&gt;&lt;/p&gt;
&lt;h2 id="先别问-gogc-设多少先问谁在付账"&gt;先别问 GOGC 设多少，先问谁在付账&lt;/h2&gt;
&lt;p&gt;Go 官方 GC Guide 给过一个很关键的公式：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Target heap memory = Live heap + (Live heap + GC roots) × GOGC / 100
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这句话不用背，抓住意思就行。&lt;/p&gt;</description></item><item><title>内存一直涨，不一定是泄漏：Go GC 真正在做什么</title><link>https://blog.cpdd.fyi/posts/go-gc-three-color-marking/</link><pubDate>Thu, 21 May 2026 20:25:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-gc-three-color-marking/</guid><description>&lt;p&gt;服务内存从 800MB 慢慢涨到 2.6GB。&lt;/p&gt;
&lt;p&gt;曲线不陡，不像雪崩。它只是每天往上爬一点，重启以后掉下来，过几天又回到老地方。告警响了，群里第一句话通常是：是不是内存泄漏？&lt;/p&gt;
&lt;p&gt;这个判断不算错，但太急。&lt;/p&gt;
&lt;p&gt;在 Go 服务里，内存持续上涨可能是真泄漏，也可能是 live heap 变大了，可能是 &lt;code&gt;GOGC&lt;/code&gt; 目标让下一轮回收来得更晚，也可能是短命对象太多，把 GC 拖进了频繁工作。还有一种更容易误判：heap 已经回收了，但 RSS 没有马上按你想象的方式降下来。&lt;/p&gt;
&lt;p&gt;内存涨，不等于内存泄漏。&lt;/p&gt;
&lt;p&gt;要把这件事查清楚，不能只盯 RSS。你得知道 Go GC 什么时候启动、怎么判断对象还活着、为什么需要写屏障、哪里会停顿，以及哪些参数是真的能调。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-gc-three-color-marking/gc-flow.svg" alt="Go GC 流程图"&gt;&lt;/p&gt;
&lt;h2 id="gc-不是保洁员是成本调度器"&gt;GC 不是保洁员，是成本调度器&lt;/h2&gt;
&lt;p&gt;很多人对 GC 的直觉是“内存不用了，运行时帮我扫掉”。这个说法只对了一半。&lt;/p&gt;
&lt;p&gt;真正重要的是另一半：GC 每扫一次，都要花 CPU；扫得太勤，CPU 被回收器吃掉，服务吞吐和延迟会受影响。扫得太晚，内存峰值又会涨上去，容器可能先把你杀掉。&lt;/p&gt;
&lt;p&gt;所以 Go GC 不是等地上脏了才来的保洁员。它更像一个拿着预算表的人：上一轮还有多少对象活着？再往后拖一点，内存能不能接受？现在开始扫，CPU 能不能接受？&lt;/p&gt;
&lt;p&gt;Go 官方 GC Guide 讲得很清楚：GC 主要消耗 CPU 和物理内存。想省 CPU，就得允许堆多长一会儿；想省内存，就得让 GC 更频繁地工作。&lt;/p&gt;
&lt;p&gt;这就是 &lt;code&gt;GOGC&lt;/code&gt; 的本质。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GOGC=100&lt;/code&gt; 不是“内存达到 100MB 触发 GC”。它的意思更接近：在上一轮存活堆的基础上，允许新分配对象按约 100% 的比例增长，再触发下一轮 GC。Go 1.19 之后，目标堆计算还会把 GC roots 的成本纳入进来，可以简化理解为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;下一轮目标 ≈ live heap + (live heap + GC roots) × GOGC / 100
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;GOGC&lt;/code&gt; 调的是成本，不是情绪。&lt;/p&gt;</description></item><item><title>加 buffer 不是修 bug：Go channel 阻塞排查实战</title><link>https://blog.cpdd.fyi/posts/go-channel-practice-debugging/</link><pubDate>Thu, 21 May 2026 18:30:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-channel-practice-debugging/</guid><description>&lt;p&gt;goroutine 数量从 200 慢慢涨到 2 万。&lt;/p&gt;
&lt;p&gt;监控图上那条线，不陡，但一直在爬。就像水龙头没拧紧——不喷，但也不会停。&lt;/p&gt;
&lt;p&gt;你的第一反应是什么？&lt;/p&gt;
&lt;p&gt;加大 buffer。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;make(chan int, 100)&lt;/code&gt; 改成 &lt;code&gt;make(chan int, 1000)&lt;/code&gt;。重启。观察。好像好了。&lt;/p&gt;
&lt;p&gt;三天后，又涨回来了。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-channel-practice-debugging/cover.svg" alt="Go channel 阻塞排查封面"&gt;&lt;/p&gt;
&lt;p&gt;这不是运气差。这是 Go 服务最常见的线上问题之一：channel 阻塞导致 goroutine 泄漏。而&amp;quot;加 buffer&amp;quot;是社区里最流行、也最没用的解法。&lt;/p&gt;
&lt;p&gt;buffer 只是把崩溃推迟了。问题的根因——谁在等谁、谁该退出、谁该 close——一个都没解决。&lt;/p&gt;
&lt;p&gt;这篇文章做一件事：从&amp;quot;goroutine 涨了&amp;quot;这个信号出发，走一遍完整的排查路径，把 channel 的六个最常见陷阱逐个拆开。&lt;/p&gt;
&lt;p&gt;不是背 API。&lt;/p&gt;
&lt;p&gt;是下次线上出问题的时候，你知道先看什么。&lt;/p&gt;
&lt;h2 id="发现怎么知道-channel-卡住了"&gt;发现：怎么知道 channel 卡住了&lt;/h2&gt;
&lt;p&gt;排查 channel 阻塞的第一步不是看代码，是看数字。&lt;/p&gt;
&lt;h3 id="runtimenumgoroutine最简单的基线"&gt;runtime.NumGoroutine()：最简单的基线&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;runtime&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ticker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewTicker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;C&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;goroutines: %d&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NumGoroutine&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这段代码放到任何 Go 服务的 &lt;code&gt;init()&lt;/code&gt; 里，每 30 秒打一行日志。goroutine 数量只升不降，大概率有泄漏。&lt;/p&gt;</description></item><item><title>Go channel 不是高级锁，它是一种组织并发的语言</title><link>https://blog.cpdd.fyi/posts/go-channel-csp-design-philosophy/</link><pubDate>Thu, 21 May 2026 16:02:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-channel-csp-design-philosophy/</guid><description>&lt;p&gt;有人为了写得“更 Go”，把一个简单 cache 包成了 channel 协议。&lt;/p&gt;
&lt;p&gt;每次 &lt;code&gt;Get&lt;/code&gt;，先构造 request，发给 owner goroutine，再等 response channel 返回。还要处理 context、close、退出顺序、goroutine 泄漏。&lt;/p&gt;
&lt;p&gt;最后代码确实没有显式 &lt;code&gt;sync.RWMutex&lt;/code&gt; 了。&lt;/p&gt;
&lt;p&gt;但问题也来了：本来一把锁能讲清楚的事，被写成了一套小型 RPC。&lt;/p&gt;
&lt;p&gt;这不是优雅。&lt;/p&gt;
&lt;p&gt;这是绕远。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-channel-csp-design-philosophy/cover.png" alt="Go channel 设计哲学封面"&gt;&lt;/p&gt;
&lt;p&gt;Go 社区有一句被反复引用的话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Do not communicate by sharing memory; instead, share memory by communicating.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;很多人把它理解成：Go 不喜欢锁，channel 更高级，能用 channel 就别用 mutex。&lt;/p&gt;
&lt;p&gt;这个理解太粗了。&lt;/p&gt;
&lt;p&gt;channel 的价值不是把锁藏起来，而是把通信显式写出来。mutex 也不是“不 Go”，它就是保护共享状态的直接工具。&lt;/p&gt;
&lt;p&gt;真正要问的不是：channel 和 mutex 谁更高级？&lt;/p&gt;
&lt;p&gt;真正要问的是：你现在到底是在传东西，还是在护东西？&lt;/p&gt;
&lt;h2 id="channel-首先讲的是结构不是性能"&gt;channel 首先讲的是结构，不是性能&lt;/h2&gt;
&lt;p&gt;Rob Pike 在那场很有名的演讲《Concurrency is not Parallelism》里，一直在压一个区分：concurrency 是 structure，parallelism 是 execution。&lt;/p&gt;
&lt;p&gt;并发不是“同时跑得更快”的同义词。&lt;/p&gt;
&lt;p&gt;并发先是一种组织程序的方式：把一件事拆成多个可以独立执行的单元，再让它们通过通信协调。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-channel-csp-design-philosophy/rob-pike.jpg" alt="Rob Pike"&gt;&lt;/p&gt;</description></item><item><title>Go channel 源码不是在讲队列，而是在讲 goroutine 怎么排队</title><link>https://blog.cpdd.fyi/posts/go-channel-hchan-runtime-source/</link><pubDate>Thu, 21 May 2026 15:17:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-channel-hchan-runtime-source/</guid><description>&lt;p&gt;goroutine dump 里满屏 &lt;code&gt;chan send&lt;/code&gt;，你知道它卡住了。&lt;/p&gt;
&lt;p&gt;但它到底卡在哪里？&lt;/p&gt;
&lt;p&gt;是缓冲区满了？没有 receiver？被 &lt;code&gt;select&lt;/code&gt; 挂进了等待队列？还是 channel 被 close 之后才醒过来，然后 panic？&lt;/p&gt;
&lt;p&gt;如果只停留在“channel 是 goroutine 之间通信的管道”，这些问题永远说不清。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ch &amp;lt;- v&lt;/code&gt; 这一行代码，在 Go runtime 里不是一句“把数据塞进队列”。它会走过 &lt;code&gt;hchan&lt;/code&gt;、环形缓冲区、&lt;code&gt;sendq&lt;/code&gt; / &lt;code&gt;recvq&lt;/code&gt;、&lt;code&gt;sudog&lt;/code&gt;、&lt;code&gt;gopark&lt;/code&gt;、&lt;code&gt;goready&lt;/code&gt;，最后决定一个 goroutine 是继续跑，还是挂起来等别人。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-channel-hchan-runtime-source/cover.png" alt="Go channel hchan 运行时封面"&gt;&lt;/p&gt;
&lt;p&gt;Go 的 channel 设计长期受 CSP 思想影响。Rob Pike 在 Go 早期设计和并发模型传播里反复强调：不要通过共享内存来通信，而要通过通信来共享内存。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-channel-hchan-runtime-source/rob-pike.jpg" alt="Rob Pike"&gt;&lt;/p&gt;
&lt;p&gt;这句话听起来像哲学，但到了 runtime 里，它会变成很具体的结构：谁在等谁，数据放在哪里，哪个 goroutine 应该被唤醒。&lt;/p&gt;
&lt;p&gt;这篇只做一件事：沿着 Go 1.25.4 的 &lt;code&gt;runtime/chan.go&lt;/code&gt; 和 &lt;code&gt;runtime/select.go&lt;/code&gt;，把 channel 的几条关键路径走一遍。&lt;/p&gt;
&lt;p&gt;不是为了背源码。&lt;/p&gt;
&lt;p&gt;是为了让你下次看到 &lt;code&gt;chan send&lt;/code&gt;、&lt;code&gt;chan receive&lt;/code&gt;、&lt;code&gt;close&lt;/code&gt;、&lt;code&gt;select&lt;/code&gt; 的时候，脑子里有一张清楚的运行时地图。&lt;/p&gt;
&lt;p&gt;channel 不是无锁队列，它是带调度语义的协作原语。&lt;/p&gt;
&lt;h2 id="hchanchannel-在运行时到底长什么样"&gt;hchan：channel 在运行时到底长什么样&lt;/h2&gt;
&lt;p&gt;Go 代码里你写的是：&lt;/p&gt;</description></item><item><title>Go 服务 goroutine 涨了，别先猜：按这条流程查 context 泄漏</title><link>https://blog.cpdd.fyi/posts/go-context-goroutine-leak-debugging/</link><pubDate>Thu, 21 May 2026 13:45:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-context-goroutine-leak-debugging/</guid><description>&lt;p&gt;线上 goroutine 数开始往上爬，最怕的不是它涨。&lt;/p&gt;
&lt;p&gt;最怕的是你盯着监控看了十分钟，然后开始猜：是不是 &lt;code&gt;context&lt;/code&gt; 泄漏了？是不是超时没生效？是不是某个 channel 卡住了？&lt;/p&gt;
&lt;p&gt;这些猜法都有可能对，也都有可能错。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;runtime.NumGoroutine()&lt;/code&gt; 只能告诉你“现在有多少 goroutine”。它不会告诉你这些 goroutine 停在哪里，也不会告诉你是谁创建的，更不会告诉你为什么没有退出。&lt;/p&gt;
&lt;p&gt;排查这类问题，第一步不是解释 &lt;code&gt;context&lt;/code&gt; 原理。&lt;/p&gt;
&lt;p&gt;第一步是采样。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-context-goroutine-leak-debugging/cover.png" alt="Go context goroutine 泄漏排查封面"&gt;&lt;/p&gt;
&lt;p&gt;这篇是 Go &lt;code&gt;context&lt;/code&gt; 系列第三篇。前两篇讲过 &lt;code&gt;context&lt;/code&gt; 的生命周期、&lt;code&gt;cancel&lt;/code&gt; 的边界，以及 Go 为什么坚持显式传 &lt;code&gt;ctx&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这一篇换一个角度：不从源码开始，从排查现场开始。&lt;/p&gt;
&lt;p&gt;一套最小流程就够了：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;goroutine 涨了
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;先采样，确认趋势
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;用 pprof 找等待点
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;用 go vet 查静态路径
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;按创建路径、取消路径、响应路径回到代码
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;修复，再验证
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-context-goroutine-leak-debugging/inline-01.png" alt="Go context 泄漏最小排查流程"&gt;&lt;/p&gt;</description></item><item><title>Go 为什么宁可让你多传一个 ctx 参数</title><link>https://blog.cpdd.fyi/posts/go-context-explicit-cost/</link><pubDate>Wed, 20 May 2026 22:20:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-context-explicit-cost/</guid><description>&lt;p&gt;很多 Go 开发者第一次系统用 &lt;code&gt;context&lt;/code&gt;，都会嫌它啰嗦。&lt;/p&gt;
&lt;p&gt;一个请求从 handler 进来，service 要传 &lt;code&gt;ctx&lt;/code&gt;，repo 要传 &lt;code&gt;ctx&lt;/code&gt;，RPC client 要传 &lt;code&gt;ctx&lt;/code&gt;，连中间那些根本不关心超时和取消的函数，也要机械地把 &lt;code&gt;ctx&lt;/code&gt; 往下递。&lt;/p&gt;
&lt;p&gt;看多了以后，很自然会冒出一个问题：为什么不能像 Java、Node.js、Python 那样，把上下文放进运行时里？&lt;/p&gt;
&lt;p&gt;Java 有 &lt;code&gt;ThreadLocal&lt;/code&gt;。Node.js 有 &lt;code&gt;AsyncLocalStorage&lt;/code&gt;。Python 有 &lt;code&gt;contextvars&lt;/code&gt;。它们都能让深层函数不改签名，也拿到当前请求的 trace id、用户信息、日志上下文。&lt;/p&gt;
&lt;p&gt;Go 偏不。&lt;/p&gt;
&lt;p&gt;它宁可让你在函数签名里多写一个参数：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;DoSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;arg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这不是因为 Go 不知道隐式上下文更省事。恰好相反，这正是 Go 的取舍：它愿意把“不优雅”摆在台面上，换调用链里的生命周期可见。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-context-explicit-cost/cover.png" alt="Go context 显式代价封面"&gt;&lt;/p&gt;
&lt;h2 id="显式传参最先付出的就是签名噪音"&gt;显式传参，最先付出的就是签名噪音&lt;/h2&gt;
&lt;p&gt;先承认代价。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ctx context.Context&lt;/code&gt; 作为首参，会污染函数签名。尤其在分层很深的服务里，中间层经常只是 pass-through：自己不用 &lt;code&gt;ctx&lt;/code&gt;，但必须接住它，再传给下一层。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CreateReq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CreateReq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UserID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;userID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`insert into ...`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这段代码里，&lt;code&gt;Handler&lt;/code&gt; 和 &lt;code&gt;Service&lt;/code&gt; 可能都没有真正“使用” &lt;code&gt;ctx&lt;/code&gt;。它们只是把一段调用生命周期交给更底层的数据库调用。&lt;/p&gt;</description></item><item><title>Go context 最容易被误用的地方：以为 cancel 会替你收拾现场</title><link>https://blog.cpdd.fyi/posts/go-context-timeout-source/</link><pubDate>Wed, 20 May 2026 17:50:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-context-timeout-source/</guid><description>&lt;p&gt;线上接口开始超时，goroutine 数一路往上涨。&lt;/p&gt;
&lt;p&gt;你第一反应可能是：不是已经传了 &lt;code&gt;context.WithTimeout&lt;/code&gt; 吗？超时到了，goroutine 不就该停了吗？&lt;/p&gt;
&lt;p&gt;这就是 Go &lt;code&gt;context&lt;/code&gt; 最容易被误用的地方。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;context&lt;/code&gt; 不负责替你停掉 goroutine。它只负责把“该停了”这句话传到门口。门里的人听不听、什么时候退场，要看你自己的代码。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-context-timeout-source/cover.png" alt="Go context 调用链生命周期封面"&gt;&lt;/p&gt;
&lt;p&gt;这篇是 Go &lt;code&gt;context&lt;/code&gt; 系列的第一篇。先不急着讲 HTTP、gRPC、database/sql 这些框架怎么接入，也不急着把所有 API 列一遍。&lt;/p&gt;
&lt;p&gt;我们先把最底下这层讲清楚：&lt;code&gt;context&lt;/code&gt; 到底是什么，&lt;code&gt;cancel&lt;/code&gt; 到底做了什么，为什么 &lt;code&gt;WithTimeout&lt;/code&gt; 后面那句 &lt;code&gt;defer cancel()&lt;/code&gt; 不是仪式感，而是资源释放动作。&lt;/p&gt;
&lt;h2 id="事故通常不是从源码开始的"&gt;事故通常不是从源码开始的&lt;/h2&gt;
&lt;p&gt;真实排查里，很少有人一上来就打开 &lt;code&gt;context.go&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;更常见的是这样的场景：&lt;/p&gt;
&lt;p&gt;一个接口偶发超时。压测一上来，P99 开始抖，错误日志里出现 &lt;code&gt;context deadline exceeded&lt;/code&gt;。你看监控，CPU 没打满，内存也没爆，但 goroutine 数不太对，一路往上爬。&lt;/p&gt;
&lt;p&gt;然后你打开 pprof：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -s &lt;span class="s1"&gt;&amp;#39;http://127.0.0.1:6060/debug/pprof/goroutine?debug=1&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;采样里有一批 goroutine 卡在业务 worker 上，等某个 channel，或者等 &lt;code&gt;&amp;lt;-ctx.Done()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这时候最危险的判断是：&lt;/p&gt;
&lt;p&gt;“是不是 Go 的 context 泄漏了？”&lt;/p&gt;
&lt;p&gt;大多数时候，不是。&lt;/p&gt;
&lt;p&gt;更可能是你的调用链里少了三件事之一：创建了 timeout context 但没及时 &lt;code&gt;cancel&lt;/code&gt;；启动了 goroutine 但没监听 &lt;code&gt;ctx.Done()&lt;/code&gt;；或者监听了 &lt;code&gt;Done&lt;/code&gt;，却没有确保取消路径真的跑到。&lt;/p&gt;</description></item><item><title>TypeScript 押注 Go：10 倍提速背后，不是语言胜负，是工程约束</title><link>https://blog.cpdd.fyi/posts/typescript-go-native-port/</link><pubDate>Tue, 19 May 2026 20:30:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/typescript-go-native-port/</guid><description>&lt;p&gt;TypeScript 的 10 倍提速，最容易被讲歪。&lt;/p&gt;
&lt;p&gt;一歪成“Go 打赢 Rust”。&lt;/p&gt;
&lt;p&gt;再歪成“以后 TypeScript 业务代码快 10 倍”。&lt;/p&gt;
&lt;p&gt;这两个说法都抓人，也都危险。前者把工程决策讲成语言饭圈，后者直接把性能场景讲错了。&lt;/p&gt;
&lt;p&gt;真正有意思的地方不是 Go 有多神，也不是 Rust 或 C# 有多不行，而是 TypeScript 团队这次面对的目标非常特殊：他们不是要从零设计一个新编译器，而是要把一个成熟、庞大、被整个前端生态依赖的工具链，迁到 native 实现里。&lt;/p&gt;
&lt;p&gt;这件事一旦说清楚，Go 为什么胜出，就没那么玄学了。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/typescript-go-native-port/cover.svg" alt="TypeScript 与 Go native port 封面图"&gt;&lt;/p&gt;
&lt;h2 id="10-倍提速先别理解错"&gt;10 倍提速，先别理解错&lt;/h2&gt;
&lt;p&gt;官方博客里的 10 倍，不是说你写出来的 JavaScript 业务代码运行快 10 倍。&lt;/p&gt;
&lt;p&gt;TypeScript 最后还是编译成 JavaScript，线上跑得快不快，主要看 JS 引擎、业务逻辑、网络、渲染、数据结构和运行时环境。TypeScript 这次提速，发生在开发期工具链：&lt;code&gt;tsc&lt;/code&gt;、类型检查、项目加载、语言服务、编辑器响应。&lt;/p&gt;
&lt;p&gt;这区别很重要。&lt;/p&gt;
&lt;p&gt;因为一个错的期待，会毁掉一个本来很有价值的技术升级。&lt;/p&gt;
&lt;p&gt;官方 benchmark 里，VS Code 代码库的 &lt;code&gt;tsc&lt;/code&gt; 检查从 77.8 秒降到 7.5 秒，Playwright 从 11.1 秒降到 1.1 秒，TypeORM 从 17.5 秒降到 1.3 秒。编辑器加载 VS Code 项目，也从约 9.6 秒降到约 1.2 秒。&lt;/p&gt;</description></item><item><title>别再让后端手写脚本了：我搭了一套可复用的企业 CLI 基座，命令扩展靠 YAML</title><link>https://blog.cpdd.fyi/posts/enterprise-cli-yaml-base/</link><pubDate>Sat, 18 Apr 2026 12:00:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/enterprise-cli-yaml-base/</guid><description>&lt;p&gt;内部工具越堆越多，每个新服务都要重新写一遍登录、鉴权、日志、错误处理。&lt;/p&gt;
&lt;p&gt;新来的同事拿到一堆脚本，不知道哪个能跑、哪个已经废了。测试环境和生产环境的 token 混着用，一不小心就把测试数据写到生产。&lt;/p&gt;
&lt;p&gt;这是我搭这套 CLI 基座的真实原因：&lt;strong&gt;不是为了让工程师更极客，而是为了把内部能力收敛成一个人和 Agent 都能调用的统一入口。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个项目我已经用在企业内部，核心代码公开。看完你能照着自己搭一套。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="一我为什么把企业后台能力做成-cli"&gt;一、我为什么把企业后台能力做成 CLI&lt;/h2&gt;
&lt;p&gt;先说判断。&lt;/p&gt;
&lt;p&gt;GUI、API、CLI 这三层，很多企业只做了前两层：后台页面给人点，API 给系统调。但中间那层——&lt;strong&gt;对人足够友好、对脚本足够稳定、对 Agent 足够结构化&lt;/strong&gt;——一直是空的。&lt;/p&gt;
&lt;p&gt;后果就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;业务同学要操作后台，点到手软&lt;/li&gt;
&lt;li&gt;工程师要写脚本，每个脚本都重新处理一遍认证、日志、错误&lt;/li&gt;
&lt;li&gt;Agent 要调用业务，得直接拼 API，参数错了都不知道怎么报错&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CLI 正好卡在中间。&lt;/p&gt;
&lt;p&gt;但企业 CLI 不是把几个 API 包成命令就完了。我搭这套基座时，核心就一个原则：&lt;strong&gt;稳定层先代码化，业务层再规格化。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;什么意思？&lt;/p&gt;
&lt;p&gt;认证、环境、HTTP 请求、Header 注入、打包发布——这些是稳定层，一次搭好，长期复用。&lt;/p&gt;
&lt;p&gt;业务命令——这些是变化层，用 YAML 规格化，扩展时不改核心代码。&lt;/p&gt;
&lt;p&gt;这样搭出来的 CLI，命令越多，扩展越快。而不是命令越多，代码越乱。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="二这套-cli-基座的骨架"&gt;二、这套 CLI 基座的骨架&lt;/h2&gt;
&lt;p&gt;项目结构长这样：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;.
├── cmd/ # Cobra 命令入口
│ ├── root.go # 根命令，加载 YAML 动态命令
│ ├── auth_login.go # 登录回调
│ ├── token_refresh.go # Token 刷新
│ └── oauth_injected.go # OAuth 凭据注入（构建时生成）
├── internal/
│ ├── auth/ # 认证层：登录、token、device_id
│ ├── httpx/ # 请求层：HTTP client、Header 注入、401 refresh
│ ├── runtime/ # 执行层：YAML 解析、Cobra 命令注册、请求执行
│ └── store/ # 存储层：profile、token 本地持久化
├── specs/
│ ├── groups/ # 业务命令规格
│ │ └── order/
│ │ ├── group.yaml # 订单组配置
│ │ └── commands/
│ │ └── list.yaml # 订单查询命令
│ └── embed.go # go:embed 把 YAML 编译进二进制
└── scripts/
 └── build-with-oauth.sh # 多环境、多平台打包脚本
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;核心分层：&lt;/strong&gt;&lt;/p&gt;</description></item></channel></rss>