<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>模組三：Runtime 與效能診斷 on Tarragon</title><link>https://tarrragon.github.io/blog/go-advanced/03-runtime-profiling/</link><description>Recent content in 模組三：Runtime 與效能診斷 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 22 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/go-advanced/03-runtime-profiling/index.xml" rel="self" type="application/rss+xml"/><item><title>3.1 GC 與 memory limit</title><link>https://tarrragon.github.io/blog/go-advanced/03-runtime-profiling/gc-memory-limit/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/03-runtime-profiling/gc-memory-limit/</guid><description>&lt;p>GC 與 memory limit 的核心關係是：Go runtime 會根據 heap 成長決定何時執行 GC，而 memory limit 讓 runtime 有一個軟性記憶體目標。Memory limit 不是硬性上限，也不是 leak 修復工具；它是讓 runtime 更早回應記憶體壓力的控制訊號。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>理解 heap growth、GOGC 與 GC 頻率的關係&lt;/li>
&lt;li>判斷 &lt;code>debug.SetMemoryLimit&lt;/code> 能解決什麼、不能解決什麼&lt;/li>
&lt;li>從環境變數設定服務 memory limit&lt;/li>
&lt;li>用 runtime &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> 觀察調整效果&lt;/li>
&lt;li>分辨 GC 壓力、長期保留與真正 leak&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察長時間服務的記憶體問題通常是趨勢問題">【觀察】長時間服務的記憶體問題通常是趨勢問題&lt;/h2>
&lt;p>記憶體診斷的核心觀察是趨勢。Heap 是否持續上升、GC 後是否下降、goroutine 是否增加、某個操作後是否留下無法回收的資料，這些都比「現在用了多少 MB」更重要。&lt;/p>
&lt;p>常見現象：&lt;/p>
&lt;ul>
&lt;li>啟動後 heap 穩定在某個區間：通常正常。&lt;/li>
&lt;li>每次高峰後 heap 都能下降：可能是短暫配置。&lt;/li>
&lt;li>GC 後 heap 仍持續上升：可能有長期保留或 leak。&lt;/li>
&lt;li>GC 次數快速增加且 CPU 升高：可能是 allocation 壓力。&lt;/li>
&lt;li>goroutine 與 heap 同時增加：可能是 goroutine leak 或 send &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a> 堆積。&lt;/li>
&lt;/ul>
&lt;p>Memory limit 可以幫 runtime 更積極控制 heap，但不能替代趨勢判讀。&lt;/p>
&lt;h2 id="判讀gc-控制的是-heap-成長">【判讀】GC 控制的是 heap 成長&lt;/h2>
&lt;p>Go GC 的核心目標是回收不再被引用的 heap 物件。Runtime 會根據 &lt;code>GOGC&lt;/code> 控制下一次 GC 觸發點。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nv">GOGC&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">100&lt;/span> go run ./cmd/server&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>GOGC=100&lt;/code> 大致表示 heap 在上次 GC 後成長約 100% 時觸發下一次 GC。數字越小，GC 越頻繁，記憶體通常較低但 CPU 成本較高；數字越大，GC 較少，記憶體通常較高但 CPU 成本較低。&lt;/p>
&lt;p>這是取捨，不是調大或調小就一定更好。CPU 緊繃的服務可能不能承受過低 &lt;code>GOGC&lt;/code>；記憶體緊繃的服務可能不能承受過高 &lt;code>GOGC&lt;/code>。&lt;/p>
&lt;h2 id="判讀memory-limit-是-runtime-軟目標">【判讀】memory limit 是 runtime 軟目標&lt;/h2>
&lt;p>&lt;code>debug.SetMemoryLimit&lt;/code> 的核心用途是告訴 Go runtime 希望整體記憶體使用量靠近某個目標。當 runtime 接近目標時，會更積極回收 heap。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">configureRuntime&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">const&lt;/span> &lt;span class="nx">limit&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">512&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="mi">20&lt;/span> &lt;span class="c1">// 512 MiB&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">debug&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">SetMemoryLimit&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">limit&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這不是作業系統層級的硬限制。程式仍可能短暫超過這個值，特別是有大量非 Go heap 記憶體、cgo、mmap、大型 byte slice 尖峰或外部 library 配置時。&lt;/p>
&lt;p>Memory limit 適合容器、桌面常駐服務、背景 worker、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> server 這類需要避免吃掉過多資源的服務。若部署平台已有 memory limit，Go runtime 的 limit 通常應略低於平台限制，留給非 Go heap 與系統開銷。&lt;/p>
&lt;h2 id="執行設定值應來自部署環境">【執行】設定值應來自部署環境&lt;/h2>
&lt;p>Memory limit 的核心配置原則是由部署環境決定，而不是寫死在 library 裡。應用入口可以讀取環境變數，解析後設定 runtime。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">ConfigureMemoryLimitFromEnv&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="kt">error&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">raw&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">os&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Getenv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;APP_MEMORY_LIMIT_MB&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">raw&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s">&amp;#34;&amp;#34;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">mb&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">strconv&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Atoi&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">raw&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Errorf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;parse APP_MEMORY_LIMIT_MB: %w&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">mb&lt;/span> &lt;span class="o">&amp;lt;=&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Errorf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;APP_MEMORY_LIMIT_MB must be positive&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="nx">debug&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">SetMemoryLimit&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">int64&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">mb&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>錯誤應在啟動時明確失敗。服務若用錯誤設定悄悄運行，後續記憶體行為會很難解釋。&lt;/p></description><content:encoded><![CDATA[<p>GC 與 memory limit 的核心關係是：Go runtime 會根據 heap 成長決定何時執行 GC，而 memory limit 讓 runtime 有一個軟性記憶體目標。Memory limit 不是硬性上限，也不是 leak 修復工具；它是讓 runtime 更早回應記憶體壓力的控制訊號。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>理解 heap growth、GOGC 與 GC 頻率的關係</li>
<li>判斷 <code>debug.SetMemoryLimit</code> 能解決什麼、不能解決什麼</li>
<li>從環境變數設定服務 memory limit</li>
<li>用 runtime <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 觀察調整效果</li>
<li>分辨 GC 壓力、長期保留與真正 leak</li>
</ol>
<hr>
<h2 id="觀察長時間服務的記憶體問題通常是趨勢問題">【觀察】長時間服務的記憶體問題通常是趨勢問題</h2>
<p>記憶體診斷的核心觀察是趨勢。Heap 是否持續上升、GC 後是否下降、goroutine 是否增加、某個操作後是否留下無法回收的資料，這些都比「現在用了多少 MB」更重要。</p>
<p>常見現象：</p>
<ul>
<li>啟動後 heap 穩定在某個區間：通常正常。</li>
<li>每次高峰後 heap 都能下降：可能是短暫配置。</li>
<li>GC 後 heap 仍持續上升：可能有長期保留或 leak。</li>
<li>GC 次數快速增加且 CPU 升高：可能是 allocation 壓力。</li>
<li>goroutine 與 heap 同時增加：可能是 goroutine leak 或 send <a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 堆積。</li>
</ul>
<p>Memory limit 可以幫 runtime 更積極控制 heap，但不能替代趨勢判讀。</p>
<h2 id="判讀gc-控制的是-heap-成長">【判讀】GC 控制的是 heap 成長</h2>
<p>Go GC 的核心目標是回收不再被引用的 heap 物件。Runtime 會根據 <code>GOGC</code> 控制下一次 GC 觸發點。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nv">GOGC</span><span class="o">=</span><span class="m">100</span> go run ./cmd/server</span></span></code></pre></div><p><code>GOGC=100</code> 大致表示 heap 在上次 GC 後成長約 100% 時觸發下一次 GC。數字越小，GC 越頻繁，記憶體通常較低但 CPU 成本較高；數字越大，GC 較少，記憶體通常較高但 CPU 成本較低。</p>
<p>這是取捨，不是調大或調小就一定更好。CPU 緊繃的服務可能不能承受過低 <code>GOGC</code>；記憶體緊繃的服務可能不能承受過高 <code>GOGC</code>。</p>
<h2 id="判讀memory-limit-是-runtime-軟目標">【判讀】memory limit 是 runtime 軟目標</h2>
<p><code>debug.SetMemoryLimit</code> 的核心用途是告訴 Go runtime 希望整體記憶體使用量靠近某個目標。當 runtime 接近目標時，會更積極回收 heap。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">configureRuntime</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="kd">const</span> <span class="nx">limit</span> <span class="p">=</span> <span class="mi">512</span> <span class="o">&lt;&lt;</span> <span class="mi">20</span> <span class="c1">// 512 MiB</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">debug</span><span class="p">.</span><span class="nf">SetMemoryLimit</span><span class="p">(</span><span class="nx">limit</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這不是作業系統層級的硬限制。程式仍可能短暫超過這個值，特別是有大量非 Go heap 記憶體、cgo、mmap、大型 byte slice 尖峰或外部 library 配置時。</p>
<p>Memory limit 適合容器、桌面常駐服務、背景 worker、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> server 這類需要避免吃掉過多資源的服務。若部署平台已有 memory limit，Go runtime 的 limit 通常應略低於平台限制，留給非 Go heap 與系統開銷。</p>
<h2 id="執行設定值應來自部署環境">【執行】設定值應來自部署環境</h2>
<p>Memory limit 的核心配置原則是由部署環境決定，而不是寫死在 library 裡。應用入口可以讀取環境變數，解析後設定 runtime。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">ConfigureMemoryLimitFromEnv</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">raw</span> <span class="o">:=</span> <span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;APP_MEMORY_LIMIT_MB&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">if</span> <span class="nx">raw</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">mb</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">strconv</span><span class="p">.</span><span class="nf">Atoi</span><span class="p">(</span><span class="nx">raw</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;parse APP_MEMORY_LIMIT_MB: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">if</span> <span class="nx">mb</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;APP_MEMORY_LIMIT_MB must be positive&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="nx">debug</span><span class="p">.</span><span class="nf">SetMemoryLimit</span><span class="p">(</span><span class="nb">int64</span><span class="p">(</span><span class="nx">mb</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">20</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>錯誤應在啟動時明確失敗。服務若用錯誤設定悄悄運行，後續記憶體行為會很難解釋。</p>
<h2 id="策略runtime-metrics-用來看調整是否有效">【策略】runtime metrics 用來看調整是否有效</h2>
<p>Runtime metrics 的核心用途是驗證調整效果。只改 <code>GOGC</code> 或 memory limit，不看 heap 與 GC 趨勢，容易變成憑感覺調參。</p>
<p>簡單方式可以用 <code>runtime.ReadMemStats</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">ReadHeapAlloc</span><span class="p">()</span> <span class="kt">uint64</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="kd">var</span> <span class="nx">stats</span> <span class="nx">runtime</span><span class="p">.</span><span class="nx">MemStats</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">runtime</span><span class="p">.</span><span class="nf">ReadMemStats</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">stats</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">return</span> <span class="nx">stats</span><span class="p">.</span><span class="nx">HeapAlloc</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>較完整的服務可以使用 <code>runtime/metrics</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">ReadRuntimeSamples</span><span class="p">()</span> <span class="p">[]</span><span class="nx">metrics</span><span class="p">.</span><span class="nx">Sample</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">samples</span> <span class="o">:=</span> <span class="p">[]</span><span class="nx">metrics</span><span class="p">.</span><span class="nx">Sample</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;/memory/classes/heap/objects:bytes&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;/gc/cycles/total:gc-cycles&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;/sched/goroutines:goroutines&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nx">metrics</span><span class="p">.</span><span class="nf">Read</span><span class="p">(</span><span class="nx">samples</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="k">return</span> <span class="nx">samples</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>觀察時要看趨勢：調整後 heap 峰值是否下降、GC 次數是否合理、CPU 是否上升、goroutine 是否仍持續增加。</p>
<h2 id="判讀memory-limit-不能修正仍被引用的資料">【判讀】memory limit 不能修正仍被引用的資料</h2>
<p>Memory limit 的核心邊界是它只能影響 GC 行為，不能讓仍被引用的物件消失。若程式把資料一直留在 map、slice、cache、goroutine 或 send buffer 裡，GC 不能回收。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">cache</span> <span class="p">=</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">][]</span><span class="kt">byte</span><span class="p">{}</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kd">func</span> <span class="nf">SavePayload</span><span class="p">(</span><span class="nx">id</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">payload</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">cache</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span> <span class="p">=</span> <span class="nx">payload</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>如果 <code>cache</code> 沒有大小限制、<a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a> 或刪除策略，memory limit 只會讓 GC 更常跑，但資料仍被 <code>cache</code> 引用。真正修正是設計 cache 淘汰、分頁、快照大小限制或資料釋放路徑。</p>
<p>因此遇到 heap 持續上升時，下一步是用 pprof 確認誰保留了記憶體。</p>
<h2 id="策略判斷是-gc-壓力還是長期保留">【策略】判斷是 GC 壓力還是長期保留</h2>
<p>記憶體問題的核心分流是：物件被大量配置但很快回收，還是物件被長期保留。</p>
<table>
  <thead>
      <tr>
          <th>現象</th>
          <th>可能問題</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>alloc_space</code> 高，<code>inuse_space</code> 不高</td>
          <td>短命配置多，GC 壓力大</td>
          <td>找熱路徑 allocation</td>
      </tr>
      <tr>
          <td><code>inuse_space</code> 持續上升</td>
          <td>長期保留或 leak</td>
          <td>看 heap profile retainers</td>
      </tr>
      <tr>
          <td>goroutine 數量同步上升</td>
          <td>goroutine leak 或 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> 堆積</td>
          <td>看 goroutine profile</td>
      </tr>
      <tr>
          <td>GC 次數暴增但 heap 仍高</td>
          <td>memory limit 壓力或資料保留</td>
          <td>檢查 cache/map/buffer</td>
      </tr>
  </tbody>
</table>
<p>這個分流會決定後續工具。GC 參數能緩解壓力，但保留資料要回到資料結構與 lifecycle 修。</p>
<h2 id="測試runtime-設定函式可以獨立測解析">【測試】runtime 設定函式可以獨立測解析</h2>
<p>Runtime 本身不需要在單元測試中反覆調參。應把環境解析邏輯獨立出來，測試輸入與錯誤即可。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">ParseMemoryLimitMB</span><span class="p">(</span><span class="nx">raw</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kt">int64</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">if</span> <span class="nx">raw</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="k">return</span> <span class="mi">0</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">mb</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">strconv</span><span class="p">.</span><span class="nf">Atoi</span><span class="p">(</span><span class="nx">raw</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">return</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;parse memory limit: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">if</span> <span class="nx">mb</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">return</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;memory limit must be positive&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">return</span> <span class="nb">int64</span><span class="p">(</span><span class="nx">mb</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">20</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>測試：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">TestParseMemoryLimitMB</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">got</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">ParseMemoryLimitMB</span><span class="p">(</span><span class="s">&#34;512&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;parse memory limit: %v&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">if</span> <span class="nx">got</span> <span class="o">!=</span> <span class="mi">512</span><span class="o">&lt;&lt;</span><span class="mi">20</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;limit = %d, want %d&#34;</span><span class="p">,</span> <span class="nx">got</span><span class="p">,</span> <span class="nb">int64</span><span class="p">(</span><span class="mi">512</span><span class="o">&lt;&lt;</span><span class="mi">20</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這讓設定邏輯可測，而不需要在每個測試中真的改 runtime 狀態。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先處理單一 Go process 如何判讀 heap、GC 與 memory limit；平台 OOM 與部署合約，會在下列章節再往外延伸：</p>
<ul>
<li><a href="/blog/go-advanced/07-distributed-operations/deployment-contracts/" data-link-title="7.5 Kubernetes、systemd 與 load balancer 合約" data-link-desc="理解部署平台如何影響 Go 服務的 shutdown、health 與資源限制">Go 進階：Kubernetes、systemd 與 load balancer 合約</a></li>
<li><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口</a></li>
</ul>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 runtime 壓力、allocation 與 pprof 診斷；如果你要先回看語言教材，可以讀：</p>
<ul>
<li><a href="/blog/go-advanced/03-runtime-profiling/allocation/" data-link-title="3.4 資料結構與 allocation 壓力" data-link-desc="分析列表、歷史資料與 WebSocket payload 的配置成本">Go：資料結構與 allocation 壓力</a></li>
<li><a href="/blog/go-advanced/03-runtime-profiling/goroutine-leak/" data-link-title="3.3 goroutine leak 偵測" data-link-desc="判斷背景工作與 client pump 是否正確退出">Go：goroutine leak 偵測</a></li>
<li><a href="/blog/go/07-refactoring/state-boundary/" data-link-title="7.4 狀態管理的安全邊界" data-link-desc="用 lock、copy 與 API 限制保護共享狀態">Go：狀態管理的安全邊界</a></li>
<li><a href="/blog/go/06-practical/new-background-worker/" data-link-title="6.4 如何新增背景工作流程" data-link-desc="接入 context、channel 與 shutdown">Go：如何新增背景工作流程</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>GC 控制 heap 回收節奏，memory limit 給 runtime 一個記憶體軟目標。合理設定能降低長時間服務的資源風險，但不能修正 cache、map、slice、goroutine 或 buffer 長期持有資料。診斷時先看趨勢，再用 pprof 區分 GC 壓力與長期保留。</p>
]]></content:encoded></item><item><title>3.2 pprof 基礎診斷流程</title><link>https://tarrragon.github.io/blog/go-advanced/03-runtime-profiling/pprof/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/03-runtime-profiling/pprof/</guid><description>&lt;p>pprof 的核心用途是用實際執行資料定位效能問題。它能協助觀察 heap、goroutine、CPU、block、mutex 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a>，讓工程師從「感覺哪裡慢」改成「依 profile 判斷哪裡有壓力」。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>安全地條件啟用 pprof endpoint&lt;/li>
&lt;li>判斷 heap、goroutine、CPU、block、mutex、trace 各自回答什麼問題&lt;/li>
&lt;li>用 &lt;code>go tool pprof&lt;/code> 取得 profile 並閱讀 &lt;code>top&lt;/code>&lt;/li>
&lt;li>區分 &lt;code>inuse_space&lt;/code> 與 &lt;code>alloc_space&lt;/code>&lt;/li>
&lt;li>把 profile 結果連回程式設計邊界&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察效能問題需要先問對問題">【觀察】效能問題需要先問對問題&lt;/h2>
&lt;p>pprof 診斷的核心起點是先確認你要回答哪個問題。不同 profile 回答不同問題，拿錯工具會讓分析變成猜測。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>問題&lt;/th>
 &lt;th>優先 profile&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>記憶體持續上升&lt;/td>
 &lt;td>heap &lt;code>inuse_space&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>GC 壓力高、配置很多&lt;/td>
 &lt;td>heap &lt;code>alloc_space&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>goroutine 數量持續增加&lt;/td>
 &lt;td>goroutine profile&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CPU 使用率高&lt;/td>
 &lt;td>CPU profile&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>goroutine 常卡在 channel 或 syscall&lt;/td>
 &lt;td>goroutine / trace&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>mutex 等待嚴重&lt;/td>
 &lt;td>mutex profile&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>channel/send/receive 阻塞多&lt;/td>
 &lt;td>block profile&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Profile 不是一次全抓就會自動給答案。先問清楚問題，再抓對應資料，分析成本會低很多。&lt;/p>
&lt;h2 id="判讀pprof-endpoint-是受控診斷入口">【判讀】pprof endpoint 是受控診斷入口&lt;/h2>
&lt;p>pprof endpoint 的核心安全責任是受控地暴露診斷資訊。它可能包含 goroutine stack、函式名稱、路徑、記憶體配置模式與部分請求脈絡；正式服務應把 &lt;code>/debug/pprof/&lt;/code> 放在明確啟用、內部網路或驗證保護之後。&lt;/p>
&lt;p>條件啟用範例：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nx">_&lt;/span> &lt;span class="s">&amp;#34;net/http/pprof&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">RegisterDebugEndpoints&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">mux&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ServeMux&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">os&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Getenv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;APP_PPROF&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s">&amp;#34;1&amp;#34;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> &lt;span class="nx">mux&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Handle&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/debug/pprof/&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">DefaultServeMux&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>實務上還可以只綁定 localhost、掛在內部管理 port、加上身份驗證，或只在開發與診斷環境啟用。重點是 pprof 要受控，而不是跟公開 API 一起裸露。&lt;/p>
&lt;h2 id="執行heap-profile-看記憶體保留與配置壓力">【執行】heap profile 看記憶體保留與配置壓力&lt;/h2>
&lt;p>Heap profile 的核心問題是「哪些物件佔用或配置了記憶體」。當服務記憶體持續上升時，heap profile 是第一個常用工具。&lt;/p>
&lt;p>看目前仍被保留的記憶體：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">go tool pprof http://localhost:8080/debug/pprof/heap&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>進入 pprof 後：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">(pprof) top&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>inuse_space&lt;/code> 代表目前仍被保留的記憶體，適合分析 leak、cache、map、slice、send &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a>、長期持有資料。&lt;/p>
&lt;p>看累積配置量：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">go tool pprof -alloc_space http://localhost:8080/debug/pprof/heap&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>alloc_space&lt;/code> 代表累積配置量，適合分析 JSON marshal、slice append、短命 object、熱路徑反覆配置造成的 GC 壓力。&lt;/p>
&lt;h2 id="判讀heap-profile-要連回資料結構">【判讀】heap profile 要連回資料結構&lt;/h2>
&lt;p>Heap profile 的核心解讀是問「誰持有資料」或「誰反覆配置」。看到某個函式在 top 裡，下一步要回到資料結構與生命週期。&lt;/p>
&lt;p>常見對應：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>profile 現象&lt;/th>
 &lt;th>可能設計問題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>map 持續佔用&lt;/td>
 &lt;td>cache 沒有淘汰或 key 無限制成長&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>slice/history 佔用高&lt;/td>
 &lt;td>history 無上限或 list 回傳太大&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>JSON marshal alloc 高&lt;/td>
 &lt;td>高頻推送每個 client 重複 marshal&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>bytes.Buffer 配置高&lt;/td>
 &lt;td>熱路徑重複建立 buffer&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">websocket&lt;/a> message 佔用高&lt;/td>
 &lt;td>send buffer 滿載或慢 client&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Profile 給的是線索，不是最終修正。修正要回到資料模型、copy boundary、buffer policy 或 cache policy。&lt;/p>
&lt;h2 id="執行goroutine-profile-看存活與卡住路徑">【執行】goroutine profile 看存活與卡住路徑&lt;/h2>
&lt;p>Goroutine profile 的核心問題是「哪些 goroutine 還活著，以及它們卡在哪裡」。它常用來診斷 goroutine leak、channel 等待、鎖等待與 network read 阻塞。&lt;/p></description><content:encoded><![CDATA[<p>pprof 的核心用途是用實際執行資料定位效能問題。它能協助觀察 heap、goroutine、CPU、block、mutex 與 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a>，讓工程師從「感覺哪裡慢」改成「依 profile 判斷哪裡有壓力」。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>安全地條件啟用 pprof endpoint</li>
<li>判斷 heap、goroutine、CPU、block、mutex、trace 各自回答什麼問題</li>
<li>用 <code>go tool pprof</code> 取得 profile 並閱讀 <code>top</code></li>
<li>區分 <code>inuse_space</code> 與 <code>alloc_space</code></li>
<li>把 profile 結果連回程式設計邊界</li>
</ol>
<hr>
<h2 id="觀察效能問題需要先問對問題">【觀察】效能問題需要先問對問題</h2>
<p>pprof 診斷的核心起點是先確認你要回答哪個問題。不同 profile 回答不同問題，拿錯工具會讓分析變成猜測。</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>優先 profile</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>記憶體持續上升</td>
          <td>heap <code>inuse_space</code></td>
      </tr>
      <tr>
          <td>GC 壓力高、配置很多</td>
          <td>heap <code>alloc_space</code></td>
      </tr>
      <tr>
          <td>goroutine 數量持續增加</td>
          <td>goroutine profile</td>
      </tr>
      <tr>
          <td>CPU 使用率高</td>
          <td>CPU profile</td>
      </tr>
      <tr>
          <td>goroutine 常卡在 channel 或 syscall</td>
          <td>goroutine / trace</td>
      </tr>
      <tr>
          <td>mutex 等待嚴重</td>
          <td>mutex profile</td>
      </tr>
      <tr>
          <td>channel/send/receive 阻塞多</td>
          <td>block profile</td>
      </tr>
  </tbody>
</table>
<p>Profile 不是一次全抓就會自動給答案。先問清楚問題，再抓對應資料，分析成本會低很多。</p>
<h2 id="判讀pprof-endpoint-是受控診斷入口">【判讀】pprof endpoint 是受控診斷入口</h2>
<p>pprof endpoint 的核心安全責任是受控地暴露診斷資訊。它可能包含 goroutine stack、函式名稱、路徑、記憶體配置模式與部分請求脈絡；正式服務應把 <code>/debug/pprof/</code> 放在明確啟用、內部網路或驗證保護之後。</p>
<p>條件啟用範例：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kn">import</span> <span class="nx">_</span> <span class="s">&#34;net/http/pprof&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kd">func</span> <span class="nf">RegisterDebugEndpoints</span><span class="p">(</span><span class="nx">mux</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">ServeMux</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">if</span> <span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;APP_PPROF&#34;</span><span class="p">)</span> <span class="o">!=</span> <span class="s">&#34;1&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="nx">mux</span><span class="p">.</span><span class="nf">Handle</span><span class="p">(</span><span class="s">&#34;/debug/pprof/&#34;</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">DefaultServeMux</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>實務上還可以只綁定 localhost、掛在內部管理 port、加上身份驗證，或只在開發與診斷環境啟用。重點是 pprof 要受控，而不是跟公開 API 一起裸露。</p>
<h2 id="執行heap-profile-看記憶體保留與配置壓力">【執行】heap profile 看記憶體保留與配置壓力</h2>
<p>Heap profile 的核心問題是「哪些物件佔用或配置了記憶體」。當服務記憶體持續上升時，heap profile 是第一個常用工具。</p>
<p>看目前仍被保留的記憶體：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">go tool pprof http://localhost:8080/debug/pprof/heap</span></span></code></pre></div><p>進入 pprof 後：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">(pprof) top</span></span></code></pre></div><p><code>inuse_space</code> 代表目前仍被保留的記憶體，適合分析 leak、cache、map、slice、send <a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a>、長期持有資料。</p>
<p>看累積配置量：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">go tool pprof -alloc_space http://localhost:8080/debug/pprof/heap</span></span></code></pre></div><p><code>alloc_space</code> 代表累積配置量，適合分析 JSON marshal、slice append、短命 object、熱路徑反覆配置造成的 GC 壓力。</p>
<h2 id="判讀heap-profile-要連回資料結構">【判讀】heap profile 要連回資料結構</h2>
<p>Heap profile 的核心解讀是問「誰持有資料」或「誰反覆配置」。看到某個函式在 top 裡，下一步要回到資料結構與生命週期。</p>
<p>常見對應：</p>
<table>
  <thead>
      <tr>
          <th>profile 現象</th>
          <th>可能設計問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>map 持續佔用</td>
          <td>cache 沒有淘汰或 key 無限制成長</td>
      </tr>
      <tr>
          <td>slice/history 佔用高</td>
          <td>history 無上限或 list 回傳太大</td>
      </tr>
      <tr>
          <td>JSON marshal alloc 高</td>
          <td>高頻推送每個 client 重複 marshal</td>
      </tr>
      <tr>
          <td>bytes.Buffer 配置高</td>
          <td>熱路徑重複建立 buffer</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">websocket</a> message 佔用高</td>
          <td>send buffer 滿載或慢 client</td>
      </tr>
  </tbody>
</table>
<p>Profile 給的是線索，不是最終修正。修正要回到資料模型、copy boundary、buffer policy 或 cache policy。</p>
<h2 id="執行goroutine-profile-看存活與卡住路徑">【執行】goroutine profile 看存活與卡住路徑</h2>
<p>Goroutine profile 的核心問題是「哪些 goroutine 還活著，以及它們卡在哪裡」。它常用來診斷 goroutine leak、channel 等待、鎖等待與 network read 阻塞。</p>
<p>互動模式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">go tool pprof http://localhost:8080/debug/pprof/goroutine</span></span></code></pre></div><p>文字 stack：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">curl <span class="s2">&#34;http://localhost:8080/debug/pprof/goroutine?debug=2&#34;</span></span></span></code></pre></div><p>若大量 goroutine 卡在同一個 channel receive、send、network read、ticker loop，通常代表某個退出條件、close path、<a href="/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">deadline</a> 或 unregister 設計有問題。</p>
<p>Goroutine profile 不只看數量。少量但卡在錯誤位置的 goroutine，也可能代表資源沒有被釋放。</p>
<h2 id="執行cpu-profile-看熱路徑">【執行】CPU profile 看熱路徑</h2>
<p>CPU profile 的核心問題是「程式把 CPU 時間花在哪裡」。它需要採樣一段時間，適合分析 CPU 使用率高或 request latency 異常。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">go tool pprof <span class="s2">&#34;http://localhost:8080/debug/pprof/profile?seconds=30&#34;</span></span></span></code></pre></div><p>常用指令：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">(pprof) top
</span></span><span class="line"><span class="ln">2</span><span class="cl">(pprof) list Encode</span></span></code></pre></div><p>CPU profile 要搭配流量情境解讀。低流量時抓到的 profile 可能沒有代表性；高流量時則要注意診斷本身也會造成額外負擔。</p>
<p>若 top 顯示大量時間花在 JSON encode、sort、lock、regex 或 compression，下一步應回到對應熱路徑，判斷是否可以減少工作、快取結果、改資料結構或降低呼叫頻率。</p>
<h2 id="策略block-與-mutex-profile-需要先啟用取樣">【策略】block 與 mutex profile 需要先啟用取樣</h2>
<p>Block/mutex profile 的核心用途是分析等待，而不是分析 CPU 計算。它們通常需要在程式中設定取樣比例。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">ConfigureBlockingProfiles</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">runtime</span><span class="p">.</span><span class="nf">SetBlockProfileRate</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">runtime</span><span class="p">.</span><span class="nf">SetMutexProfileFraction</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Block profile 看 goroutine 在同步原語上阻塞的時間，例如 channel send/receive、select、mutex。Mutex profile 看鎖競爭。</p>
<p>啟用取樣有成本，不一定要常駐開最高強度。診斷時可以條件啟用，或在壓測環境中使用。</p>
<h2 id="執行trace-看排程與延遲">【執行】trace 看排程與延遲</h2>
<p>Trace 的核心用途是觀察 goroutine 排程、network block、syscall、GC pause 與延遲事件。它比單一 profile 更完整，但也更重。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">curl -o trace.out <span class="s2">&#34;http://localhost:8080/debug/pprof/trace?seconds=5&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">go tool trace trace.out</span></span></code></pre></div><p>Trace 適合用在你已經知道有延遲問題，但 heap、CPU、goroutine profile 都不足以解釋時。它能顯示 goroutine 何時 runnable、何時 blocked、何時被排程。</p>
<p>Trace 檔案可能很大，不適合長時間收集。通常先抓短時間，確認問題窗口後再精準分析。</p>
<h2 id="策略診斷流程要先保留現場">【策略】診斷流程要先保留現場</h2>
<p>pprof 診斷的核心流程是先保留現場，再改程式。若你先重啟服務或調參，可能會清掉最重要的證據。</p>
<p>建議流程：</p>
<ol>
<li>記錄當下流量、版本、操作、時間區間。</li>
<li>讀 runtime <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a>：heap、GC、goroutine、<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> 長度。</li>
<li>依問題抓 profile：heap、goroutine、CPU 或 trace。</li>
<li>用 profile 找出函式與 stack pattern。</li>
<li>回到程式碼確認資料結構、goroutine lifecycle 或 hot path。</li>
<li>修改後用相同情境再抓一次 profile 驗證。</li>
</ol>
<p>這個流程能避免「看到 top 第一名就改」的衝動。Profile 需要和情境一起讀，才不會誤判。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先處理單一服務內的 profile 讀法；商用 APM 與分散式 tracing，會在下列章節再往外延伸：</p>
<ul>
<li><a href="/blog/go-advanced/07-distributed-operations/observability-pipeline/" data-link-title="7.4 Observability pipeline、metrics 與 tracing" data-link-desc="把 structured log、metric、trace 與 profile 組成可操作的診斷系統">Go 進階：Observability pipeline、metrics 與 tracing</a></li>
<li><a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">Backend：可觀測性平台</a></li>
</ul>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 goroutine、allocation 與 runtime metrics；如果你要先回看語言教材，可以讀：</p>
<ul>
<li><a href="/blog/go/04-concurrency/goroutine/" data-link-title="4.1 goroutine：輕量並發工作" data-link-desc="用 goroutine 啟動並發工作，並設計清楚的退出條件">Go：goroutine：輕量並發工作</a></li>
<li><a href="/blog/go/04-concurrency/select/" data-link-title="4.3 select：同時等待多種事件" data-link-desc="用 select 建立事件迴圈">Go：select：同時等待多種事件</a></li>
<li><a href="/blog/go/06-practical/new-background-worker/" data-link-title="6.4 如何新增背景工作流程" data-link-desc="接入 context、channel 與 shutdown">Go：如何新增背景工作流程</a></li>
<li><a href="/blog/go/07-refactoring/state-boundary/" data-link-title="7.4 狀態管理的安全邊界" data-link-desc="用 lock、copy 與 API 限制保護共享狀態">Go：狀態管理的安全邊界</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>pprof 是診斷工具，不是公開 API。Heap profile 看保留與配置，goroutine profile 看存活與卡住路徑，CPU profile 看熱點，block/mutex profile 看等待，trace 看排程與延遲。好的診斷流程會先問對問題、抓對 profile，再把結果連回資料結構、goroutine lifecycle 與服務行為。</p>
]]></content:encoded></item><item><title>3.3 goroutine leak 偵測</title><link>https://tarrragon.github.io/blog/go-advanced/03-runtime-profiling/goroutine-leak/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/03-runtime-profiling/goroutine-leak/</guid><description>&lt;p>Goroutine leak 偵測的核心目標是確認已經沒有存在價值的 goroutine 能被停止。它通常是生命週期問題：誰取消、誰 close、誰解除 I/O 阻塞、誰停止 ticker。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>分辨合理長期 goroutine 與 goroutine leak&lt;/li>
&lt;li>用 context、done channel、connection close 設計退出路徑&lt;/li>
&lt;li>用 pprof goroutine profile 判讀卡住 stack&lt;/li>
&lt;li>測試 worker、ticker、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> pump 是否能退出&lt;/li>
&lt;li>從 leak pattern 回到 ownership 修正&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察goroutine-leak-是生命週期沒有結束">【觀察】goroutine leak 是生命週期沒有結束&lt;/h2>
&lt;p>Goroutine leak 的核心定義是某個 goroutine 已經沒有存在價值，卻仍然活著。它可能卡在 channel receive、channel send、network read、ticker、mutex 或永遠不會觸發的 select case。&lt;/p>
&lt;p>反模式：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">StartWorker&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">jobs&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="nx">Job&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="k">go&lt;/span> &lt;span class="kd">func&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">job&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">jobs&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nf">process&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">job&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="p">}()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 worker 只有在 &lt;code>jobs&lt;/code> 被關閉時才會退出。若呼叫端永遠不關閉 &lt;code>jobs&lt;/code>，而 worker 也沒有 context，這個 goroutine 可能永久存在。&lt;/p>
&lt;p>長期存在不一定是 leak。HTTP server accept loop、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> exporter、background scheduler 都可能合理存在；問題是它們是否有明確停止條件，且 shutdown 時是否真的會走到。&lt;/p>
&lt;h2 id="判讀每個-goroutine-都要有退出原因">【判讀】每個 goroutine 都要有退出原因&lt;/h2>
&lt;p>Goroutine lifecycle 的核心檢查是每個 goroutine 都能回答三個問題：&lt;/p>
&lt;ol>
&lt;li>誰要求它停止？&lt;/li>
&lt;li>它如果卡在 channel 或 I/O，如何被喚醒？&lt;/li>
&lt;li>它停止後如何讓測試或上層知道？&lt;/li>
&lt;/ol>
&lt;p>若三題任一題答不出來，就有 leak 風險。&lt;/p>
&lt;p>例如 worker 應該有 context：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">RunWorker&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">jobs&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="nx">Job&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">select&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Done&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="nx">job&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ok&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">jobs&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">!&lt;/span>&lt;span class="nx">ok&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nf">process&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">job&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 worker 有兩條退出路徑：上層取消 context，或 jobs channel 被關閉。這比只依賴 channel close 更容易整合到服務 shutdown。&lt;/p>
&lt;h2 id="策略io-阻塞需要-deadline-或-close">【策略】I/O 阻塞需要 deadline 或 close&lt;/h2>
&lt;p>I/O goroutine 的核心風險是 context 本身不一定能打斷底層阻塞呼叫。WebSocket read、TCP read、file watcher、外部 API call 都要確認是否支援 context、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">deadline&lt;/a> 或 close。&lt;/p>
&lt;p>WebSocket read pump 常見退出方式：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">c&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Client&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">readPump&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">hub&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Hub&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">router&lt;/span> &lt;span class="nx">MessageRouter&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="kd">func&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">hub&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">unregister&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">c&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="p">}()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">message&lt;/span> &lt;span class="nx">ClientMessage&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">conn&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">ReadJSON&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">message&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nx">_&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">router&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Route&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">c&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">message&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>若 &lt;code>ReadJSON&lt;/code> 卡住，context 取消不一定直接讓它返回。實務上需要 read deadline、connection close 或 heartbeat 讓 read pump 有機會退出。&lt;/p>
&lt;h2 id="執行done-channel-讓測試能觀察退出">【執行】done channel 讓測試能觀察退出&lt;/h2>
&lt;p>測試 goroutine 是否退出的核心問題是需要可觀察訊號。&lt;code>done&lt;/code> channel 可以在 goroutine 結束時 close。&lt;/p></description><content:encoded><![CDATA[<p>Goroutine leak 偵測的核心目標是確認已經沒有存在價值的 goroutine 能被停止。它通常是生命週期問題：誰取消、誰 close、誰解除 I/O 阻塞、誰停止 ticker。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>分辨合理長期 goroutine 與 goroutine leak</li>
<li>用 context、done channel、connection close 設計退出路徑</li>
<li>用 pprof goroutine profile 判讀卡住 stack</li>
<li>測試 worker、ticker、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> pump 是否能退出</li>
<li>從 leak pattern 回到 ownership 修正</li>
</ol>
<hr>
<h2 id="觀察goroutine-leak-是生命週期沒有結束">【觀察】goroutine leak 是生命週期沒有結束</h2>
<p>Goroutine leak 的核心定義是某個 goroutine 已經沒有存在價值，卻仍然活著。它可能卡在 channel receive、channel send、network read、ticker、mutex 或永遠不會觸發的 select case。</p>
<p>反模式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">StartWorker</span><span class="p">(</span><span class="nx">jobs</span> <span class="o">&lt;-</span><span class="kd">chan</span> <span class="nx">Job</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="k">for</span> <span class="nx">job</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">jobs</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">            <span class="nf">process</span><span class="p">(</span><span class="nx">job</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">}()</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個 worker 只有在 <code>jobs</code> 被關閉時才會退出。若呼叫端永遠不關閉 <code>jobs</code>，而 worker 也沒有 context，這個 goroutine 可能永久存在。</p>
<p>長期存在不一定是 leak。HTTP server accept loop、<a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> exporter、background scheduler 都可能合理存在；問題是它們是否有明確停止條件，且 shutdown 時是否真的會走到。</p>
<h2 id="判讀每個-goroutine-都要有退出原因">【判讀】每個 goroutine 都要有退出原因</h2>
<p>Goroutine lifecycle 的核心檢查是每個 goroutine 都能回答三個問題：</p>
<ol>
<li>誰要求它停止？</li>
<li>它如果卡在 channel 或 I/O，如何被喚醒？</li>
<li>它停止後如何讓測試或上層知道？</li>
</ol>
<p>若三題任一題答不出來，就有 leak 風險。</p>
<p>例如 worker 應該有 context：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">RunWorker</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">jobs</span> <span class="o">&lt;-</span><span class="kd">chan</span> <span class="nx">Job</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Done</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">case</span> <span class="nx">job</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">jobs</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">                <span class="k">return</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="nf">process</span><span class="p">(</span><span class="nx">job</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個 worker 有兩條退出路徑：上層取消 context，或 jobs channel 被關閉。這比只依賴 channel close 更容易整合到服務 shutdown。</p>
<h2 id="策略io-阻塞需要-deadline-或-close">【策略】I/O 阻塞需要 deadline 或 close</h2>
<p>I/O goroutine 的核心風險是 context 本身不一定能打斷底層阻塞呼叫。WebSocket read、TCP read、file watcher、外部 API call 都要確認是否支援 context、<a href="/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">deadline</a> 或 close。</p>
<p>WebSocket read pump 常見退出方式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">c</span> <span class="o">*</span><span class="nx">Client</span><span class="p">)</span> <span class="nf">readPump</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">hub</span> <span class="o">*</span><span class="nx">Hub</span><span class="p">,</span> <span class="nx">router</span> <span class="nx">MessageRouter</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">defer</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="nx">hub</span><span class="p">.</span><span class="nx">unregister</span> <span class="o">&lt;-</span> <span class="nx">c</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="p">}()</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="kd">var</span> <span class="nx">message</span> <span class="nx">ClientMessage</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">c</span><span class="p">.</span><span class="nx">conn</span><span class="p">.</span><span class="nf">ReadJSON</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">message</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="nx">_</span> <span class="p">=</span> <span class="nx">router</span><span class="p">.</span><span class="nf">Route</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">c</span><span class="p">,</span> <span class="nx">message</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>若 <code>ReadJSON</code> 卡住，context 取消不一定直接讓它返回。實務上需要 read deadline、connection close 或 heartbeat 讓 read pump 有機會退出。</p>
<h2 id="執行done-channel-讓測試能觀察退出">【執行】done channel 讓測試能觀察退出</h2>
<p>測試 goroutine 是否退出的核心問題是需要可觀察訊號。<code>done</code> channel 可以在 goroutine 結束時 close。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">RunWorker</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">jobs</span> <span class="o">&lt;-</span><span class="kd">chan</span> <span class="nx">Job</span><span class="p">,</span> <span class="nx">done</span> <span class="kd">chan</span><span class="o">&lt;-</span> <span class="kd">struct</span><span class="p">{})</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">defer</span> <span class="nb">close</span><span class="p">(</span><span class="nx">done</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Done</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">case</span> <span class="nx">job</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">jobs</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">                <span class="k">return</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="nf">process</span><span class="p">(</span><span class="nx">job</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>測試：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">TestRunWorkerStops</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">ctx</span><span class="p">,</span> <span class="nx">cancel</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">WithCancel</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">())</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">jobs</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="nx">Job</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">done</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kd">struct</span><span class="p">{})</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">go</span> <span class="nf">RunWorker</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">jobs</span><span class="p">,</span> <span class="nx">done</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nf">cancel</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">done</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">time</span><span class="p">.</span><span class="nf">After</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;worker did not stop&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">Timeout</a> 是測試保護，不是功能本身。真正的退出訊號是 <code>done</code> 被關閉。</p>
<h2 id="執行ticker-必須停止">【執行】ticker 必須停止</h2>
<p>Ticker leak 的核心原因是建立 ticker 後沒有呼叫 <code>Stop</code>。Ticker 會持有 runtime 資源；長時間服務若反覆建立不停止，會累積不必要成本。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">RunCleanup</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">ticker</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">NewTicker</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nx">Minute</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">defer</span> <span class="nx">ticker</span><span class="p">.</span><span class="nf">Stop</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Done</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">ticker</span><span class="p">.</span><span class="nx">C</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="nf">cleanup</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>defer ticker.Stop()</code> 應緊跟在成功建立 ticker 後。這樣不管函式因 context、錯誤或 channel 關閉退出，ticker 都會被停止。</p>
<p><code>time.After</code> 在一次性 timeout 很方便，但在高頻迴圈裡反覆建立 timer 可能造成額外配置。需要重複觸發時，優先使用 <code>Ticker</code> 或可重設的 <code>Timer</code> 並明確停止。</p>
<h2 id="判讀pprof-goroutine-profile-看-stack-pattern">【判讀】pprof goroutine profile 看 stack pattern</h2>
<p>Goroutine profile 的核心價值是顯示 goroutine stack。當 goroutine 數量持續上升時，先看它們卡在哪裡。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">curl <span class="s2">&#34;http://localhost:8080/debug/pprof/goroutine?debug=2&#34;</span></span></span></code></pre></div><p>常見 pattern：</p>
<table>
  <thead>
      <tr>
          <th>stack 類型</th>
          <th>可能原因</th>
          <th>回到哪個邊界</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>channel receive</td>
          <td>上游不會再送，也沒 close/context</td>
          <td>channel ownership</td>
      </tr>
      <tr>
          <td>channel send</td>
          <td>下游不再接收或 <a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 滿</td>
          <td><a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> / unregister</td>
      </tr>
      <tr>
          <td>network read</td>
          <td>沒有 deadline 或 connection 未 close</td>
          <td>heartbeat / I/O lifecycle</td>
      </tr>
      <tr>
          <td>ticker loop</td>
          <td>context 沒接上或 ticker 未 stop</td>
          <td>select loop lifecycle</td>
      </tr>
      <tr>
          <td>mutex lock</td>
          <td>鎖競爭或死鎖</td>
          <td>shared state owner</td>
      </tr>
  </tbody>
</table>
<p>看到 stack 後，下一步是回到對應 lifecycle 設計：誰負責停止，誰負責釋放阻塞點。</p>
<h2 id="策略websocket-pump-leak-要看-readwriteunregister-三方">【策略】WebSocket pump leak 要看 read/write/unregister 三方</h2>
<p>WebSocket goroutine leak 的核心常見原因是 read pump、write pump、hub unregister 沒有形成閉環。</p>
<p>目標流程：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">read pump error 或 connection close
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">        │
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        ▼
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">hub unregister
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        │
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        ├── close client.send
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        └── close conn
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        │
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        ▼
</span></span><span class="line"><span class="ln">10</span><span class="cl">write pump exits</span></span></code></pre></div><p>若 hub 沒有 close <code>send</code>，write pump 可能一直等。若 connection 沒有 close，read pump 可能卡在 read。若 unregister 不是 idempotent，重複 close 可能 panic。</p>
<p>Goroutine profile 若顯示大量 goroutine 卡在 <code>writePump</code> 的 send receive，通常要檢查 <code>client.send</code> 是否會被 close。若卡在 <code>ReadJSON</code>，要檢查 read deadline、heartbeat 與 connection close。</p>
<h2 id="測試用-goroutine-數量做粗略回歸檢查">【測試】用 goroutine 數量做粗略回歸檢查</h2>
<p>Goroutine 數量測試的核心用途是粗略檢查是否有明顯 leak。它不是精準證明，因為 Go runtime 與測試環境本身也會有 goroutine。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">TestNoObviousGoroutineLeak</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">before</span> <span class="o">:=</span> <span class="nx">runtime</span><span class="p">.</span><span class="nf">NumGoroutine</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">ctx</span><span class="p">,</span> <span class="nx">cancel</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">WithCancel</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">())</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">done</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kd">struct</span><span class="p">{})</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">defer</span> <span class="nb">close</span><span class="p">(</span><span class="nx">done</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nf">RunWorker</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="nx">Job</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">}()</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nf">cancel</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">done</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">time</span><span class="p">.</span><span class="nf">After</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;worker did not stop&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="nf">eventually</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">,</span> <span class="kd">func</span><span class="p">()</span> <span class="kt">bool</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="k">return</span> <span class="nx">runtime</span><span class="p">.</span><span class="nf">NumGoroutine</span><span class="p">()</span> <span class="o">&lt;=</span> <span class="nx">before</span><span class="o">+</span><span class="mi">2</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這類測試要留緩衝，避免因 runtime 或其他測試 goroutine 造成假失敗。更可靠的測試仍是等待明確 <code>done</code> 訊號。</p>
<h2 id="判讀goroutine-leak-修正要改停止路徑">【判讀】goroutine leak 修正要改停止路徑</h2>
<p>Goroutine leak 的核心修正是補上停止路徑。</p>
<p>常見修正：</p>
<ul>
<li>加入 <code>ctx.Done()</code> case。</li>
<li>關閉由自己擁有的 output channel。</li>
<li>由 coordinator 等 sender 完成再 close。</li>
<li>對 network read/write 設定 deadline。</li>
<li>shutdown 時 close connection。</li>
<li>ticker 建立後 <code>defer Stop()</code>。</li>
<li>hub unregister 時 close send channel。</li>
</ul>
<p>修正後要用測試證明退出路徑真的會發生，再用 pprof 或 goroutine count 驗證趨勢。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先處理 goroutine 的啟動、停止與阻塞邊界；更完整的 worker 全域治理，會在下列章節再往外延伸：</p>
<ul>
<li><a href="/blog/go-advanced/01-concurrency-patterns/channel-ownership/" data-link-title="1.1 channel ownership 與關閉責任" data-link-desc="判斷誰能送出、接收與關閉 channel">Go 進階：channel ownership 與關閉責任</a></li>
<li><a href="/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">Go 進階：bounded worker pool</a></li>
<li><a href="/blog/go-advanced/01-concurrency-patterns/select-loop/" data-link-title="1.2 select loop 的生命週期設計" data-link-desc="理解長時間運行 goroutine 如何同時處理事件、ticker 與取消">Go 進階：select loop 的生命週期設計</a></li>
</ul>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 goroutine lifecycle、channel 與 shutdown；如果你要先回看語言教材，可以讀：</p>
<ul>
<li><a href="/blog/go/04-concurrency/goroutine/" data-link-title="4.1 goroutine：輕量並發工作" data-link-desc="用 goroutine 啟動並發工作，並設計清楚的退出條件">Go：goroutine：輕量並發工作</a></li>
<li><a href="/blog/go/04-concurrency/channel/" data-link-title="4.2 channel：資料傳遞與 backpressure " data-link-desc="理解 channel 如何在 goroutine 之間傳遞資料並形成 backpressure ">Go：channel：資料傳遞與 backpressure </a></li>
<li><a href="/blog/go/04-concurrency/select/" data-link-title="4.3 select：同時等待多種事件" data-link-desc="用 select 建立事件迴圈">Go：select：同時等待多種事件</a></li>
<li><a href="/blog/go/06-practical/new-background-worker/" data-link-title="6.4 如何新增背景工作流程" data-link-desc="接入 context、channel 與 shutdown">Go：如何新增背景工作流程</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>Goroutine leak 是生命週期問題。每個長期 goroutine 都應知道誰能停止它、如何解除阻塞、如何讓測試觀察退出。Context、done channel、deadline、connection close、ticker stop 與 hub unregister 是主要工具。pprof goroutine profile 則用來確認還活著的 goroutine 卡在哪個邊界。</p>
]]></content:encoded></item><item><title>3.4 資料結構與 allocation 壓力</title><link>https://tarrragon.github.io/blog/go-advanced/03-runtime-profiling/allocation/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/03-runtime-profiling/allocation/</guid><description>&lt;p>Allocation 分析的核心目標是區分必要的安全複製與可優化的重複配置。Go 服務中很多配置來自 slice 成長、map/list 複製、JSON marshal、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a> 建立與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> payload；優化前要先確認配置是否位於熱路徑，且不能破壞狀態邊界。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>理解 allocation 如何增加 GC 壓力&lt;/li>
&lt;li>分辨必要 copy boundary 與不必要重複配置&lt;/li>
&lt;li>用預配置降低 slice 成長成本&lt;/li>
&lt;li>判斷 JSON marshal 與 WebSocket payload 的重用邊界&lt;/li>
&lt;li>用 pprof 的 &lt;code>alloc_space&lt;/code> 與 &lt;code>inuse_space&lt;/code> 決定優化方向&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察allocation-壓力會放大-gc-成本">【觀察】allocation 壓力會放大 GC 成本&lt;/h2>
&lt;p>Allocation 的核心影響是增加 heap 成長速度，進而增加 GC 工作量。即使物件很快被回收，大量短命配置仍可能造成 CPU 與 latency 壓力。&lt;/p>
&lt;p>常見熱路徑：&lt;/p>
&lt;ul>
&lt;li>每次 WebSocket broadcast 都對每個 client 重新 marshal。&lt;/li>
&lt;li>每次 API list 都建立大型 slice。&lt;/li>
&lt;li>每次 repository 查詢都 copy 大型 map。&lt;/li>
&lt;li>每次 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a> 都組大量臨時欄位。&lt;/li>
&lt;li>每次 encode 都建立新的 &lt;code>bytes.Buffer&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>不是所有 allocation 都要消除。診斷重點是找出高頻、可避免、且不破壞邊界的配置。&lt;/p>
&lt;h2 id="判讀預配置解決的是成長成本">【判讀】預配置解決的是成長成本&lt;/h2>
&lt;p>Slice 預配置的核心用途是讓底層 array 成長符合預期。若結果長度可預估，應用 &lt;code>make&lt;/code> 設定容量。&lt;/p>
&lt;p>未預配置：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">BuildNames&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">users&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">names&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">users&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">names&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">names&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Name&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">names&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>預配置：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">BuildNames&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">users&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">names&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">([]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">users&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">users&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">names&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">names&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Name&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">names&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這不是微優化。若這段程式在高頻 list API、background &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection&lt;/a> 或 broadcast path 中執行，預配置可以減少反覆擴容與 copy。&lt;/p>
&lt;h2 id="判讀copy-boundary-是必要成本">【判讀】copy boundary 是必要成本&lt;/h2>
&lt;p>安全複製的核心目的是保護內部可變狀態。Repository 回傳資料時 copy slice 或 map，會增加 allocation，但能避免外部突變與 data race。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">UserRepository&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">ListUsers&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">([]&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">error&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">mu&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RLock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">mu&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RUnlock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nx">users&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">([]&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">users&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">users&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">users&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 allocation 是狀態邊界的成本。優化前要先確認它是否真的是瓶頸，不能只因為 profile 看到配置就移除 copy。&lt;/p>
&lt;p>若列表很大且讀取頻繁，應考慮分頁、projection、snapshot cache 或只回傳必要欄位。不要為了省配置而直接暴露內部 map。&lt;/p>
&lt;h2 id="策略大型-list-優先改資料形狀">【策略】大型 list 優先改資料形狀&lt;/h2>
&lt;p>大型 list allocation 的核心問題常常是 API 一次回太多資料。若每次請求都複製整個 repository，配置與延遲都會隨資料量線性成長。&lt;/p>
&lt;p>可選策略：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>策略&lt;/th>
 &lt;th>適用情境&lt;/th>
 &lt;th>代價&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>分頁&lt;/td>
 &lt;td>使用者只需要部分資料&lt;/td>
 &lt;td>API 需要 cursor 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>projection&lt;/td>
 &lt;td>只需要摘要欄位&lt;/td>
 &lt;td>要維護讀取模型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>snapshot cache&lt;/td>
 &lt;td>讀多寫少&lt;/td>
 &lt;td>要處理快取失效&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>incremental update&lt;/td>
 &lt;td>WebSocket 推送最新變化&lt;/td>
 &lt;td>client 要能合併狀態&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>優化資料形狀通常比取消 copy 更安全。Copy boundary 保護正確性，資料形狀決定每次 copy 的成本。&lt;/p></description><content:encoded><![CDATA[<p>Allocation 分析的核心目標是區分必要的安全複製與可優化的重複配置。Go 服務中很多配置來自 slice 成長、map/list 複製、JSON marshal、<a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 建立與 <a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> payload；優化前要先確認配置是否位於熱路徑，且不能破壞狀態邊界。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>理解 allocation 如何增加 GC 壓力</li>
<li>分辨必要 copy boundary 與不必要重複配置</li>
<li>用預配置降低 slice 成長成本</li>
<li>判斷 JSON marshal 與 WebSocket payload 的重用邊界</li>
<li>用 pprof 的 <code>alloc_space</code> 與 <code>inuse_space</code> 決定優化方向</li>
</ol>
<hr>
<h2 id="觀察allocation-壓力會放大-gc-成本">【觀察】allocation 壓力會放大 GC 成本</h2>
<p>Allocation 的核心影響是增加 heap 成長速度，進而增加 GC 工作量。即使物件很快被回收，大量短命配置仍可能造成 CPU 與 latency 壓力。</p>
<p>常見熱路徑：</p>
<ul>
<li>每次 WebSocket broadcast 都對每個 client 重新 marshal。</li>
<li>每次 API list 都建立大型 slice。</li>
<li>每次 repository 查詢都 copy 大型 map。</li>
<li>每次 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 都組大量臨時欄位。</li>
<li>每次 encode 都建立新的 <code>bytes.Buffer</code>。</li>
</ul>
<p>不是所有 allocation 都要消除。診斷重點是找出高頻、可避免、且不破壞邊界的配置。</p>
<h2 id="判讀預配置解決的是成長成本">【判讀】預配置解決的是成長成本</h2>
<p>Slice 預配置的核心用途是讓底層 array 成長符合預期。若結果長度可預估，應用 <code>make</code> 設定容量。</p>
<p>未預配置：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">BuildNames</span><span class="p">(</span><span class="nx">users</span> <span class="p">[]</span><span class="nx">User</span><span class="p">)</span> <span class="p">[]</span><span class="kt">string</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="kd">var</span> <span class="nx">names</span> <span class="p">[]</span><span class="kt">string</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">user</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">users</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="nx">names</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">names</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">Name</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">return</span> <span class="nx">names</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>預配置：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">BuildNames</span><span class="p">(</span><span class="nx">users</span> <span class="p">[]</span><span class="nx">User</span><span class="p">)</span> <span class="p">[]</span><span class="kt">string</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">names</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">string</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">users</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">user</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">users</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="nx">names</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">names</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">Name</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">return</span> <span class="nx">names</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這不是微優化。若這段程式在高頻 list API、background <a href="/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection</a> 或 broadcast path 中執行，預配置可以減少反覆擴容與 copy。</p>
<h2 id="判讀copy-boundary-是必要成本">【判讀】copy boundary 是必要成本</h2>
<p>安全複製的核心目的是保護內部可變狀態。Repository 回傳資料時 copy slice 或 map，會增加 allocation，但能避免外部突變與 data race。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">ListUsers</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="p">([]</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">RLock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">defer</span> <span class="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">RUnlock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">users</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="nx">User</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">user</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">users</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">users</span><span class="p">,</span> <span class="nx">user</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">return</span> <span class="nx">users</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個 allocation 是狀態邊界的成本。優化前要先確認它是否真的是瓶頸，不能只因為 profile 看到配置就移除 copy。</p>
<p>若列表很大且讀取頻繁，應考慮分頁、projection、snapshot cache 或只回傳必要欄位。不要為了省配置而直接暴露內部 map。</p>
<h2 id="策略大型-list-優先改資料形狀">【策略】大型 list 優先改資料形狀</h2>
<p>大型 list allocation 的核心問題常常是 API 一次回太多資料。若每次請求都複製整個 repository，配置與延遲都會隨資料量線性成長。</p>
<p>可選策略：</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>適用情境</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>分頁</td>
          <td>使用者只需要部分資料</td>
          <td>API 需要 cursor 或 <a href="/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset</a></td>
      </tr>
      <tr>
          <td>projection</td>
          <td>只需要摘要欄位</td>
          <td>要維護讀取模型</td>
      </tr>
      <tr>
          <td>snapshot cache</td>
          <td>讀多寫少</td>
          <td>要處理快取失效</td>
      </tr>
      <tr>
          <td>incremental update</td>
          <td>WebSocket 推送最新變化</td>
          <td>client 要能合併狀態</td>
      </tr>
  </tbody>
</table>
<p>優化資料形狀通常比取消 copy 更安全。Copy boundary 保護正確性，資料形狀決定每次 copy 的成本。</p>
<h2 id="執行json-marshal-是-websocket-常見配置來源">【執行】JSON marshal 是 WebSocket 常見配置來源</h2>
<p>JSON 序列化的核心成本是把 Go 資料結構轉成 bytes。高頻 WebSocket 推送若對每個 client 反覆 marshal 同一份 message，會造成大量短命配置。</p>
<p>反模式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">client</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">clients</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">payload</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">json</span><span class="p">.</span><span class="nf">Marshal</span><span class="p">(</span><span class="nx">message</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">client</span><span class="p">.</span><span class="nf">SendBytes</span><span class="p">(</span><span class="nx">payload</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>同一份 message 可以先 marshal 一次：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">payload</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">json</span><span class="p">.</span><span class="nf">Marshal</span><span class="p">(</span><span class="nx">message</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">client</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">clients</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nx">client</span><span class="p">.</span><span class="nf">SendBytes</span><span class="p">(</span><span class="nx">payload</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個優化的前提是 <code>payload</code> 被視為只讀。Send path 不應修改傳入的 bytes；若某個 client 需要修改，就應在該 client 邊界 copy，而不是讓共享 payload 被改動。</p>
<h2 id="判讀bytes-重用要先定義所有權">【判讀】bytes 重用要先定義所有權</h2>
<p>Bytes 重用的核心風險是共享 slice 被修改。<code>[]byte</code> 是可變資料，傳給多個 client 時要明確規定它只讀。</p>
<p>可以用型別或註解表達語意：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">type</span> <span class="nx">EncodedMessage</span> <span class="p">[]</span><span class="kt">byte</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">c</span> <span class="o">*</span><span class="nx">Client</span><span class="p">)</span> <span class="nf">SendEncoded</span><span class="p">(</span><span class="nx">message</span> <span class="nx">EncodedMessage</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">return</span> <span class="nx">c</span><span class="p">.</span><span class="nf">TrySend</span><span class="p">(</span><span class="nx">ServerMessage</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="nx">Encoded</span><span class="p">:</span> <span class="nx">message</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這不能從型別上完全禁止修改，但能讓 API 語意更清楚。真正保護仍靠 ownership 規則、測試與 code review。</p>
<p>若無法保證下游不修改，就必須 copy：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">CloneBytes</span><span class="p">(</span><span class="nx">input</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">[]</span><span class="kt">byte</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">output</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">input</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nb">copy</span><span class="p">(</span><span class="nx">output</span><span class="p">,</span> <span class="nx">input</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">return</span> <span class="nx">output</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>效能優化不能建立在模糊的可變資料共享上。</p>
<h2 id="策略syncpool-只適合已證明的熱路徑">【策略】sync.Pool 只適合已證明的熱路徑</h2>
<p><code>sync.Pool</code> 的核心用途是複用高頻、短命、可重建的暫存物件。它可以降低配置，但會增加所有權複雜度。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">var</span> <span class="nx">bufferPool</span> <span class="p">=</span> <span class="nx">sync</span><span class="p">.</span><span class="nx">Pool</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">New</span><span class="p">:</span> <span class="kd">func</span><span class="p">()</span> <span class="kt">any</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="k">return</span> <span class="nb">new</span><span class="p">(</span><span class="nx">bytes</span><span class="p">.</span><span class="nx">Buffer</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kd">func</span> <span class="nf">Encode</span><span class="p">(</span><span class="nx">value</span> <span class="kt">any</span><span class="p">)</span> <span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">buf</span> <span class="o">:=</span> <span class="nx">bufferPool</span><span class="p">.</span><span class="nf">Get</span><span class="p">().(</span><span class="o">*</span><span class="nx">bytes</span><span class="p">.</span><span class="nx">Buffer</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">defer</span> <span class="nx">bufferPool</span><span class="p">.</span><span class="nf">Put</span><span class="p">(</span><span class="nx">buf</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">buf</span><span class="p">.</span><span class="nf">Reset</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">json</span><span class="p">.</span><span class="nf">NewEncoder</span><span class="p">(</span><span class="nx">buf</span><span class="p">).</span><span class="nf">Encode</span><span class="p">(</span><span class="nx">value</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="nx">output</span> <span class="o">:=</span> <span class="nb">append</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="kc">nil</span><span class="p">),</span> <span class="nx">buf</span><span class="p">.</span><span class="nf">Bytes</span><span class="p">()</span><span class="o">...</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">return</span> <span class="nx">output</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這裡仍然 copy 出 <code>output</code>，因為 <code>buf</code> 會被放回 pool。若直接回傳 <code>buf.Bytes()</code>，呼叫端拿到的 slice 可能在 pool 重用後被覆寫。</p>
<p>不要一開始就使用 <code>sync.Pool</code>。先用 pprof 證明配置是瓶頸，再評估 pool 是否值得承擔額外複雜度。</p>
<h2 id="判讀inuse-與-alloc-回答不同問題">【判讀】inuse 與 alloc 回答不同問題</h2>
<p>Heap profile 的核心判讀是分清 <code>inuse_space</code> 與 <code>alloc_space</code>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">go tool pprof http://localhost:8080/debug/pprof/heap
</span></span><span class="line"><span class="ln">2</span><span class="cl">go tool pprof -alloc_space http://localhost:8080/debug/pprof/heap</span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>指標</th>
          <th>問題</th>
          <th>常見修正</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>inuse_space</code> 高</td>
          <td>現在誰保留記憶體</td>
          <td>cache 淘汰、釋放引用、限制 buffer</td>
      </tr>
      <tr>
          <td><code>alloc_space</code> 高</td>
          <td>誰累積配置最多</td>
          <td>預配置、重用、減少 marshal、改資料形狀</td>
      </tr>
  </tbody>
</table>
<p>若 <code>alloc_space</code> 高但 <code>inuse_space</code> 不高，代表配置很多但大多被回收，問題可能是 GC 壓力。若 <code>inuse_space</code> 持續上升，代表資料被長期保留，應檢查 cache、map、slice、goroutine reference 或 send buffer。</p>
<h2 id="策略allocation-優化要保留正確性邊界">【策略】allocation 優化要保留正確性邊界</h2>
<p>Allocation 優化的核心底線是不能破壞狀態安全。以下做法通常不可接受：</p>
<ul>
<li>為了省 copy，直接回傳 repository 內部 map。</li>
<li>為了省 bytes，讓多個 client 共享可修改 payload。</li>
<li>為了省 allocation，把 buffer 放回 pool 後仍回傳其底層 slice。</li>
<li>為了少建立 struct，把 request DTO 和 domain state 混用。</li>
</ul>
<p>較安全的優化順序：</p>
<ol>
<li>用 pprof 確認熱點。</li>
<li>預配置已知大小的 slice/map。</li>
<li>減少重複 marshal。</li>
<li>改 API 資料形狀，例如分頁或 projection。</li>
<li>最後才考慮 <code>sync.Pool</code>。</li>
</ol>
<p>這個順序先處理低風險、高可讀性的改動，再處理高複雜度工具。</p>
<h2 id="測試優化後要補邊界測試">【測試】優化後要補邊界測試</h2>
<p>Allocation 優化的測試核心是確保共享資料沒有被外部修改。若你重用 bytes、snapshot 或 pooled buffer，要補測試保護 ownership。</p>
<p>例如 repository list 仍要回傳 copy：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">TestListUsersReturnsCopy</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">repo</span> <span class="o">:=</span> <span class="nf">NewUserRepository</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">ctx</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">_</span> <span class="p">=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">Save</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">User</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;user_1&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">users</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">ListUsers</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;list users: %v&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">users</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">ID</span> <span class="p">=</span> <span class="s">&#34;changed&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nx">again</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">ListUsers</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;list users again: %v&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">if</span> <span class="nx">again</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">ID</span> <span class="o">!=</span> <span class="s">&#34;user_1&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;repository data was modified through returned slice&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這種測試能防止未來為了省 allocation 而移除必要 copy。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先處理熱路徑上的配置與資料形狀；更大範圍的序列化與 payload 策略，會在下列章節再往外延伸：</p>
<ul>
<li><a href="/blog/go/02-types-data/struct-json/" data-link-title="2.1 struct 與 JSON tag" data-link-desc="理解 Go struct 如何表達資料形狀，並透過 JSON tag 對應外部格式">Go 入門：struct 與 JSON tag</a></li>
<li><a href="/blog/go/02-types-data/slices-maps/" data-link-title="2.2 slice 與 map" data-link-desc="掌握 Go 最常用的集合型別：slice 與 map">Go 入門：slice 與 map</a></li>
<li><a href="/blog/go/02-types-data/pointers-copy/" data-link-title="2.5 指標與資料複製邊界" data-link-desc="理解指標、slice 與共享狀態的防護策略">Go 入門：指標與資料複製邊界</a></li>
<li><a href="/blog/go-advanced/03-runtime-profiling/pprof/" data-link-title="3.2 pprof 基礎診斷流程" data-link-desc="用 pprof endpoint 診斷 heap、goroutine 與 CPU 問題">Go 進階：pprof 基礎診斷流程</a></li>
</ul>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 copy boundary、JSON 與 runtime profile；如果你要先回看語言教材，可以讀：</p>
<ul>
<li><a href="/blog/go/06-practical/state-fields/" data-link-title="6.3 如何擴展狀態投影欄位" data-link-desc="更新狀態模型、repository 與 API 輸出">Go：如何擴展狀態投影欄位</a></li>
<li><a href="/blog/go/06-practical/repository-port/" data-link-title="6.6 如何新增 repository port" data-link-desc="先建立儲存邊界，再決定 memory、SQLite 或外部資料庫實作">Go：如何新增 repository port</a></li>
<li><a href="/blog/go/06-practical/new-websocket-action/" data-link-title="6.1 如何新增一個即時訊息 action" data-link-desc="修改 client message、路由與 handler">Go：如何新增一個即時訊息 action</a></li>
<li><a href="/blog/go/07-refactoring/state-boundary/" data-link-title="7.4 狀態管理的安全邊界" data-link-desc="用 lock、copy 與 API 限制保護共享狀態">Go：狀態管理的安全邊界</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>Allocation 優化要先判斷配置是否必要。保護狀態的 copy 是合理成本，高頻熱路徑的重複配置才是優先目標。JSON marshal、slice 成長、map/list 複製與 buffer 建立都是常見來源；用 pprof 區分 <code>inuse_space</code> 與 <code>alloc_space</code> 後，再決定預配置、分頁、projection、payload 重用或 <code>sync.Pool</code>。</p>
]]></content:encoded></item></channel></rss>