<?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>Concurrency on Tarragon</title><link>https://tarrragon.github.io/blog/tags/concurrency/</link><description>Recent content in Concurrency on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 19 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/concurrency/index.xml" rel="self" type="application/rss+xml"/><item><title>1.1 channel ownership 與關閉責任</title><link>https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/channel-ownership/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/channel-ownership/</guid><description>&lt;p>Channel ownership 的核心規則是：能保證不再送出資料的一方，才有資格關閉 channel。建立 channel 的程式碼不一定是 owner；真正的 owner 是掌握 send lifecycle 的元件。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>用 send lifecycle 判斷誰能 close channel&lt;/li>
&lt;li>分辨 sender、receiver、coordinator 的責任&lt;/li>
&lt;li>用 channel direction 表達能力限制&lt;/li>
&lt;li>設計多 sender 的安全關閉流程&lt;/li>
&lt;li>用 context 表達接收端提早停止，而不是關閉不屬於自己的 channel&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察close-channel-的核心風險是責任不清">【觀察】close channel 的核心風險是責任不清&lt;/h2>
&lt;p>Channel 關閉錯誤的核心問題是 ownership 沒定義清楚。接收端想停止讀取時關閉輸入 channel，多個 sender 中任一個 sender 自行 close，共用 channel 被外部任意 close，這些都可能造成 panic 或資料遺失。&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">Consume&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">input&lt;/span> &lt;span class="kd">chan&lt;/span> &lt;span class="nx">Event&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="nb">close&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">input&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>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">event&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">input&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="nf">handle&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">event&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="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>這段程式的問題是 &lt;code>Consume&lt;/code> 只是 receiver，卻關閉了 sender 還可能使用的 channel。只要上游晚一點送資料，就會出現 &lt;code>send on closed channel&lt;/code>。&lt;/p>
&lt;h2 id="判讀close-的語意是不再有新值">【判讀】close 的語意是不再有新值&lt;/h2>
&lt;p>&lt;code>close(ch)&lt;/code> 的核心語意是「這個 channel 不會再收到新值」。它不是取消 goroutine 的通用手段，也不是釋放記憶體的必要動作。&lt;/p>
&lt;p>單一 sender 可以安全 close：&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">Produce&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">items&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="kd">chan&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">out&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&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"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&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"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nb">close&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">out&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">item&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">items&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">out&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">item&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="p">}()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">out&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;/code>&lt;/pre>&lt;/div>&lt;p>這個 goroutine 是唯一 sender，因此它能保證迴圈結束後不再送出。receiver 可以用 &lt;code>range&lt;/code> 讀到 channel 關閉為止：&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="k">for&lt;/span> &lt;span class="nx">item&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nf">Produce&lt;/span>&lt;span class="p">([]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s">&amp;#34;a&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;b&amp;#34;&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">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">item&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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>receiver 不需要 close &lt;code>out&lt;/code>。接收完資料是 receiver 的狀態，不代表 sender 的生命週期已經結束。&lt;/p>
&lt;h2 id="策略先畫出-sender-和-receiver">【策略】先畫出 sender 和 receiver&lt;/h2>
&lt;p>Channel 設計的核心動作是先列出誰會 send、誰會 receive、誰知道所有 sender 已經結束。這比先決定 &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;table>
 &lt;thead>
 &lt;tr>
 &lt;th>角色&lt;/th>
 &lt;th>能力&lt;/th>
 &lt;th>close 責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>single sender&lt;/td>
 &lt;td>送出資料，知道自己何時結束&lt;/td>
 &lt;td>擁有 close 責任&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>receiver&lt;/td>
 &lt;td>接收資料，可能提早停止&lt;/td>
 &lt;td>透過 context 通知停止&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>coordinator&lt;/td>
 &lt;td>等待所有 sender 結束&lt;/td>
 &lt;td>擁有 close 責任&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>external caller&lt;/td>
 &lt;td>持有 channel reference 但不了解生命週期&lt;/td>
 &lt;td>不參與 close 決策&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>如果無法回答「誰知道所有 sender 都結束」，就不應該 close 這個 channel。沒有 close 不一定是 bug；錯誤 close 才是更嚴重的問題。&lt;/p>
&lt;h2 id="執行多-sender-需要-coordinator-關閉">【執行】多 sender 需要 coordinator 關閉&lt;/h2>
&lt;p>多個 goroutine 送往同一個 channel 時，關閉責任必須交給 coordinator。任一 sender 都不能單方面 close，因為其他 sender 可能還在送。&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">Merge&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">inputs&lt;/span> &lt;span class="o">...&amp;lt;-&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="nx">Event&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="nx">Event&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">out&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="nx">Event&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="kd">var&lt;/span> &lt;span class="nx">wg&lt;/span> &lt;span class="nx">sync&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">WaitGroup&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">wg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Add&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">inputs&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">input&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">inputs&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">input&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">input&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">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"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">wg&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">10&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">event&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">input&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="nx">out&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">event&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;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="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">17&lt;/span>&lt;span class="cl"> &lt;span class="nx">wg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Wait&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="nb">close&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">out&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="p">}()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">out&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>轉送 goroutine 只負責送資料。另一個 goroutine 等所有 sender 結束後才 close &lt;code>out&lt;/code>。這把「送資料」和「宣告所有資料送完」分成兩個責任。&lt;/p></description><content:encoded><![CDATA[<p>Channel ownership 的核心規則是：能保證不再送出資料的一方，才有資格關閉 channel。建立 channel 的程式碼不一定是 owner；真正的 owner 是掌握 send lifecycle 的元件。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>用 send lifecycle 判斷誰能 close channel</li>
<li>分辨 sender、receiver、coordinator 的責任</li>
<li>用 channel direction 表達能力限制</li>
<li>設計多 sender 的安全關閉流程</li>
<li>用 context 表達接收端提早停止，而不是關閉不屬於自己的 channel</li>
</ol>
<hr>
<h2 id="觀察close-channel-的核心風險是責任不清">【觀察】close channel 的核心風險是責任不清</h2>
<p>Channel 關閉錯誤的核心問題是 ownership 沒定義清楚。接收端想停止讀取時關閉輸入 channel，多個 sender 中任一個 sender 自行 close，共用 channel 被外部任意 close，這些都可能造成 panic 或資料遺失。</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">Consume</span><span class="p">(</span><span class="nx">input</span> <span class="kd">chan</span> <span class="nx">Event</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">input</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="nx">event</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">input</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="nf">handle</span><span class="p">(</span><span class="nx">event</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>這段程式的問題是 <code>Consume</code> 只是 receiver，卻關閉了 sender 還可能使用的 channel。只要上游晚一點送資料，就會出現 <code>send on closed channel</code>。</p>
<h2 id="判讀close-的語意是不再有新值">【判讀】close 的語意是不再有新值</h2>
<p><code>close(ch)</code> 的核心語意是「這個 channel 不會再收到新值」。它不是取消 goroutine 的通用手段，也不是釋放記憶體的必要動作。</p>
<p>單一 sender 可以安全 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">Produce</span><span class="p">(</span><span class="nx">items</span> <span class="p">[]</span><span class="kt">string</span><span class="p">)</span> <span class="o">&lt;-</span><span class="kd">chan</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">out</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kt">string</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">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="k">defer</span> <span class="nb">close</span><span class="p">(</span><span class="nx">out</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">item</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">items</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="nx">out</span> <span class="o">&lt;-</span> <span class="nx">item</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><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="k">return</span> <span class="nx">out</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個 goroutine 是唯一 sender，因此它能保證迴圈結束後不再送出。receiver 可以用 <code>range</code> 讀到 channel 關閉為止：</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">item</span> <span class="o">:=</span> <span class="k">range</span> <span class="nf">Produce</span><span class="p">([]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;a&#34;</span><span class="p">,</span> <span class="s">&#34;b&#34;</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">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">item</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>receiver 不需要 close <code>out</code>。接收完資料是 receiver 的狀態，不代表 sender 的生命週期已經結束。</p>
<h2 id="策略先畫出-sender-和-receiver">【策略】先畫出 sender 和 receiver</h2>
<p>Channel 設計的核心動作是先列出誰會 send、誰會 receive、誰知道所有 sender 已經結束。這比先決定 <a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 大小更重要。</p>
<table>
  <thead>
      <tr>
          <th>角色</th>
          <th>能力</th>
          <th>close 責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>single sender</td>
          <td>送出資料，知道自己何時結束</td>
          <td>擁有 close 責任</td>
      </tr>
      <tr>
          <td>receiver</td>
          <td>接收資料，可能提早停止</td>
          <td>透過 context 通知停止</td>
      </tr>
      <tr>
          <td>coordinator</td>
          <td>等待所有 sender 結束</td>
          <td>擁有 close 責任</td>
      </tr>
      <tr>
          <td>external caller</td>
          <td>持有 channel reference 但不了解生命週期</td>
          <td>不參與 close 決策</td>
      </tr>
  </tbody>
</table>
<p>如果無法回答「誰知道所有 sender 都結束」，就不應該 close 這個 channel。沒有 close 不一定是 bug；錯誤 close 才是更嚴重的問題。</p>
<h2 id="執行多-sender-需要-coordinator-關閉">【執行】多 sender 需要 coordinator 關閉</h2>
<p>多個 goroutine 送往同一個 channel 時，關閉責任必須交給 coordinator。任一 sender 都不能單方面 close，因為其他 sender 可能還在送。</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">Merge</span><span class="p">(</span><span class="nx">inputs</span> <span class="o">...&lt;-</span><span class="kd">chan</span> <span class="nx">Event</span><span class="p">)</span> <span class="o">&lt;-</span><span class="kd">chan</span> <span class="nx">Event</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">out</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="nx">Event</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="kd">var</span> <span class="nx">wg</span> <span class="nx">sync</span><span class="p">.</span><span class="nx">WaitGroup</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">wg</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="nx">inputs</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">input</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">inputs</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">input</span> <span class="o">:=</span> <span class="nx">input</span>
</span></span><span class="line"><span class="ln"> 8</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"> 9</span><span class="cl">            <span class="k">defer</span> <span class="nx">wg</span><span class="p">.</span><span class="nf">Done</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="k">for</span> <span class="nx">event</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">input</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">                <span class="nx">out</span> <span class="o">&lt;-</span> <span class="nx">event</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><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="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="nx">wg</span><span class="p">.</span><span class="nf">Wait</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="nb">close</span><span class="p">(</span><span class="nx">out</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="p">}()</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="k">return</span> <span class="nx">out</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>轉送 goroutine 只負責送資料。另一個 goroutine 等所有 sender 結束後才 close <code>out</code>。這把「送資料」和「宣告所有資料送完」分成兩個責任。</p>
<h2 id="執行接收端提早停止要用-context">【執行】接收端提早停止要用 context</h2>
<p>Receiver 提早停止的核心做法是通知上游停止，而不是關閉輸入 channel。<code>context.Context</code> 是 Go 服務中最常見的停止訊號。</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">Consume</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">input</span> <span class="o">&lt;-</span><span class="kd">chan</span> <span class="nx">Event</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="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 class="nx">ctx</span><span class="p">.</span><span class="nf">Err</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="nx">event</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">input</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 class="kc">nil</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">handle</span><span class="p">(</span><span class="nx">event</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>Consume</code> 可以因為 context 取消而退出，也可以因為 input 關閉而退出。它沒有 close <code>input</code>，因為 input 的 send lifecycle 不屬於它。</p>
<p>這個邊界在服務中很重要。HTTP handler、background worker、connection writer 都可能提早退出，但不能任意 close 上游仍可能使用的 channel。</p>
<h2 id="策略channel-direction-把能力寫進型別">【策略】channel direction 把能力寫進型別</h2>
<p>Channel direction 的核心價值是限制函式能做的事。<code>chan&lt;- T</code> 只能 send，<code>&lt;-chan T</code> 只能 receive；這讓 ownership 更容易被讀者看見。</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">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">results</span> <span class="kd">chan</span><span class="o">&lt;-</span> <span class="nx">Result</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="nx">results</span> <span class="o">&lt;-</span> <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><code>StartWorker</code> 只能從 <code>jobs</code> 收資料，只能往 <code>results</code> 送資料。它不能 close <code>jobs</code>，因為型別上就不是 sender；它是否能 close <code>results</code> 則要看它是不是唯一 sender。</p>
<p>方向限制不會自動解決所有權，但它能減少誤用，也讓 API 比註解更可靠。</p>
<h2 id="判讀done-channel-和-data-channel-分開表達不同語意">【判讀】done channel 和 data channel 分開表達不同語意</h2>
<p>停止訊號的核心語意應該和資料流分開。資料 channel 傳遞值；done channel 或 context 表示停止。把兩者混在一起會讓 close 的語意變模糊。</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">Worker</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">jobs</span> <span class="o">&lt;-</span><span class="kd">chan</span> <span class="nx">Job</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><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="kd">func</span> <span class="p">(</span><span class="nx">w</span> <span class="nx">Worker</span><span class="p">)</span> <span class="nf">Run</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"> 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="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</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"> 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="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">w</span><span class="p">.</span><span class="nx">jobs</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">11</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">12</span><span class="cl">                <span class="k">return</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="nf">process</span><span class="p">(</span><span class="nx">job</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="p">}</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>jobs</code> 關閉代表沒有更多 job。<code>ctx.Done()</code> 代表上層要求停止。這兩種退出原因不同，分開處理才能在 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>、metric 或測試中看清楚。</p>
<h2 id="測試測試-close-行為要避免靠-sleep">【測試】測試 close 行為要避免靠 sleep</h2>
<p>Channel ownership 的測試目標是確認 sender 結束後會 close、receiver 取消時不會 panic、多 sender 等全部完成才 close。這類測試應使用 channel 同步，不應依賴任意 sleep。</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">TestProduceClosesOutput</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">output</span> <span class="o">:=</span> <span class="nf">Produce</span><span class="p">([]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;a&#34;</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">if</span> <span class="nx">got</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">output</span><span class="p">;</span> <span class="nx">got</span> <span class="o">!=</span> <span class="s">&#34;a&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</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;first value = %q, want %q&#34;</span><span class="p">,</span> <span class="nx">got</span><span class="p">,</span> <span class="s">&#34;a&#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></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">_</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">output</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">if</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="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;output should be closed after producer finishes&#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="p">}</span></span></span></code></pre></div><p>多 sender 測試可以讀到輸出 channel 關閉為止，確認所有值都收到：</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">TestMergeClosesAfterAllInputsClose</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">a</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="nx">Event</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">b</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="nx">Event</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">a</span> <span class="o">&lt;-</span> <span class="nx">Event</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;a&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">b</span> <span class="o">&lt;-</span> <span class="nx">Event</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;b&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nb">close</span><span class="p">(</span><span class="nx">a</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nb">close</span><span class="p">(</span><span class="nx">b</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="nx">output</span> <span class="o">:=</span> <span class="nf">Merge</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">got</span> <span class="o">:=</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">bool</span><span class="p">{}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">for</span> <span class="nx">event</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">output</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="nx">got</span><span class="p">[</span><span class="nx">event</span><span class="p">.</span><span class="nx">ID</span><span class="p">]</span> <span class="p">=</span> <span class="kc">true</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></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">if</span> <span class="p">!</span><span class="nx">got</span><span class="p">[</span><span class="s">&#34;a&#34;</span><span class="p">]</span> <span class="o">||</span> <span class="p">!</span><span class="nx">got</span><span class="p">[</span><span class="s">&#34;b&#34;</span><span class="p">]</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</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;merge should forward all events before closing&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個測試沒有固定等待時間。它把 channel close 本身當成同步訊號，結果更穩定。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先聚焦單一 Go process 內的 channel close 與 goroutine lifecycle；跨 process 的 <a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack</a>、[<a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> group](/go-advanced/backend/knowledge-cards/consumer-group) 與分散式訊號，會在下列章節再往外延伸：</p>
<ul>
<li><a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Backend：訊息佇列與事件傳遞</a></li>
<li><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">Backend：可靠性驗證流程</a></li>
</ul>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 channel、goroutine 與 select 的協作；如果你要先回看語言教材，可以讀：</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/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">Go：bounded worker pool</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>Channel ownership 的核心是 send lifecycle。唯一 sender 可以在送完後 close；多 sender 需要 coordinator 統一 close；receiver 想停止時應使用 context，而不是關閉輸入 channel。把 sender、receiver、coordinator 分清楚，才能避免 <code>send on closed channel</code>、goroutine leak 與資料流混亂。</p>
]]></content:encoded></item><item><title>4.1 goroutine：輕量並發工作</title><link>https://tarrragon.github.io/blog/go/04-concurrency/goroutine/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/04-concurrency/goroutine/</guid><description>&lt;p>goroutine 是 Go 執行並發工作的基本單位。它的核心用途是讓一段函式和目前流程同時進行，但每個 goroutine 都必須有明確的退出條件，否則長時間程式會累積無法回收的背景工作。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>用 &lt;code>go&lt;/code> 啟動 goroutine&lt;/li>
&lt;li>理解 goroutine 和一般函式呼叫的差異&lt;/li>
&lt;li>判斷哪些工作適合放進 goroutine&lt;/li>
&lt;li>為 goroutine 設計退出條件&lt;/li>
&lt;li>避免 goroutine leak&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察go-關鍵字啟動並發工作">【觀察】go 關鍵字啟動並發工作&lt;/h2>
&lt;p>&lt;code>go&lt;/code> 的核心規則是：在函式呼叫前加上 &lt;code>go&lt;/code>，該函式會在新的 goroutine 中執行，呼叫端不會等待它完成。&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">say&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">message&lt;/span> &lt;span class="kt">string&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">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&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">3&lt;/span>&lt;span class="cl">&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="kd">func&lt;/span> &lt;span class="nf">main&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">6&lt;/span>&lt;span class="cl"> &lt;span class="k">go&lt;/span> &lt;span class="nf">say&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;background&amp;#34;&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="nf">say&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;foreground&amp;#34;&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;/code>&lt;/pre>&lt;/div>&lt;p>這段程式啟動一個背景 goroutine 執行 &lt;code>say(&amp;quot;background&amp;quot;)&lt;/code>，主 goroutine 會繼續執行 &lt;code>say(&amp;quot;foreground&amp;quot;)&lt;/code>。&lt;/p>
&lt;h2 id="判讀goroutine-需要明確完成保證">【判讀】goroutine 需要明確完成保證&lt;/h2>
&lt;p>goroutine 的生命週期規則是：程式不會因為你啟動了 goroutine 就自動等待它完成。&lt;code>main()&lt;/code> 結束時，整個 process 會結束，尚未完成的 goroutine 也會停止。&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">main&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="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;background&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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>主程式太快結束時，背景 goroutine 可能還沒得到執行機會。&lt;/p>
&lt;p>需要等待結果時，應該使用 channel、&lt;code>sync.WaitGroup&lt;/code> 或其他同步機制。&lt;/p>
&lt;h2 id="策略goroutine-適合等待型或獨立型工作">【策略】goroutine 適合等待型或獨立型工作&lt;/h2>
&lt;p>goroutine 使用的核心規則是：只有當工作能和目前流程並發進行，且生命週期可被管理時，才啟動 goroutine。&lt;/p>
&lt;p>適合 goroutine 的工作：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工作類型&lt;/th>
 &lt;th>原因&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>等待 I/O&lt;/td>
 &lt;td>等檔案、網路、外部程序時不阻塞主流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>背景 worker&lt;/td>
 &lt;td>從 channel 收 job 並處理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>定時任務&lt;/td>
 &lt;td>定期清理、同步或掃描&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多個獨立請求&lt;/td>
 &lt;td>可同時發出、再收集結果&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>等待 I/O 的核心訊號是目前流程會花時間等外部回應，例如讀檔、呼叫 HTTP API、等待資料庫查詢或讀取 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/socket/" data-link-title="Socket" data-link-desc="說明 network socket 如何成為 application 與網路之間的資料傳輸邊界">socket&lt;/a>。這類工作放進 goroutine 後，呼叫端可以繼續處理其他事件，但仍然要用 context 或 channel 管理結果與取消。&lt;/p>
&lt;p>背景 worker 的核心訊號是工作來自 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> 或 channel，而且處理時間和 request 生命週期分離。例如使用者送出匯入任務後，server 只先接受任務，後續由 worker 逐筆處理資料。這種 goroutine 通常需要明確的 job channel、錯誤回報與 shutdown 流程。&lt;/p>
&lt;p>定時任務的核心訊號是行為按時間觸發，例如每分鐘清理過期 session、同步外部狀態或刷新快取。這類 goroutine 應使用 ticker 搭配 context，讓服務停止時可以一起退出。&lt;/p>
&lt;p>多個獨立請求的核心訊號是多個工作彼此沒有順序依賴，例如同時查三個外部 API，最後合併結果。這類 goroutine 的重點是收集結果、限制並發數量，並在其中一個工作失敗時決定是否取消其他工作。&lt;/p>
&lt;p>需要先補齊生命週期設計的情境：&lt;/p>
&lt;ul>
&lt;li>只是想讓程式「看起來比較快」&lt;/li>
&lt;li>沒有任何退出條件&lt;/li>
&lt;li>呼叫端需要結果但沒有同步機制&lt;/li>
&lt;li>多個 goroutine 會同時修改共享資料但沒有保護&lt;/li>
&lt;/ul>
&lt;h2 id="執行用-waitgroup-等待一組工作">【執行】用 WaitGroup 等待一組工作&lt;/h2>
&lt;p>&lt;code>sync.WaitGroup&lt;/code> 的核心用途是等待一組 goroutine 完成。&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">main&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">var&lt;/span> &lt;span class="nx">wg&lt;/span> &lt;span class="nx">sync&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">WaitGroup&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="p">&amp;lt;&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="o">++&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="nx">wg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&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">go&lt;/span> &lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">id&lt;/span> &lt;span class="kt">int&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"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">wg&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"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;worker&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">id&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 class="nx">i&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="nx">wg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Wait&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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段程式有三個關鍵：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>動作&lt;/th>
 &lt;th>意義&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>wg.Add(1)&lt;/code>&lt;/td>
 &lt;td>增加一個待完成工作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>defer wg.Done()&lt;/code>&lt;/td>
 &lt;td>goroutine 結束時標記完成&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>wg.Wait()&lt;/code>&lt;/td>
 &lt;td>等待所有工作完成&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>id&lt;/code> 作為參數傳入 goroutine，可以避免 loop 變數捕捉造成混淆。&lt;/p>
&lt;h2 id="長時間-goroutine-要能停止">長時間 goroutine 要能停止&lt;/h2>
&lt;p>長時間 goroutine 的核心規則是：迴圈中必須等待取消訊號或輸入 channel 關閉。&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">worker&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">handle&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，它都會退出。&lt;/p></description><content:encoded><![CDATA[<p>goroutine 是 Go 執行並發工作的基本單位。它的核心用途是讓一段函式和目前流程同時進行，但每個 goroutine 都必須有明確的退出條件，否則長時間程式會累積無法回收的背景工作。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>用 <code>go</code> 啟動 goroutine</li>
<li>理解 goroutine 和一般函式呼叫的差異</li>
<li>判斷哪些工作適合放進 goroutine</li>
<li>為 goroutine 設計退出條件</li>
<li>避免 goroutine leak</li>
</ol>
<hr>
<h2 id="觀察go-關鍵字啟動並發工作">【觀察】go 關鍵字啟動並發工作</h2>
<p><code>go</code> 的核心規則是：在函式呼叫前加上 <code>go</code>，該函式會在新的 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">say</span><span class="p">(</span><span class="nx">message</span> <span class="kt">string</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">fmt</span><span class="p">.</span><span class="nf">Println</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="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="kd">func</span> <span class="nf">main</span><span class="p">()</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="nf">say</span><span class="p">(</span><span class="s">&#34;background&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nf">say</span><span class="p">(</span><span class="s">&#34;foreground&#34;</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>這段程式啟動一個背景 goroutine 執行 <code>say(&quot;background&quot;)</code>，主 goroutine 會繼續執行 <code>say(&quot;foreground&quot;)</code>。</p>
<h2 id="判讀goroutine-需要明確完成保證">【判讀】goroutine 需要明確完成保證</h2>
<p>goroutine 的生命週期規則是：程式不會因為你啟動了 goroutine 就自動等待它完成。<code>main()</code> 結束時，整個 process 會結束，尚未完成的 goroutine 也會停止。</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">main</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="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;background&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>主程式太快結束時，背景 goroutine 可能還沒得到執行機會。</p>
<p>需要等待結果時，應該使用 channel、<code>sync.WaitGroup</code> 或其他同步機制。</p>
<h2 id="策略goroutine-適合等待型或獨立型工作">【策略】goroutine 適合等待型或獨立型工作</h2>
<p>goroutine 使用的核心規則是：只有當工作能和目前流程並發進行，且生命週期可被管理時，才啟動 goroutine。</p>
<p>適合 goroutine 的工作：</p>
<table>
  <thead>
      <tr>
          <th>工作類型</th>
          <th>原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>等待 I/O</td>
          <td>等檔案、網路、外部程序時不阻塞主流程</td>
      </tr>
      <tr>
          <td>背景 worker</td>
          <td>從 channel 收 job 並處理</td>
      </tr>
      <tr>
          <td>定時任務</td>
          <td>定期清理、同步或掃描</td>
      </tr>
      <tr>
          <td>多個獨立請求</td>
          <td>可同時發出、再收集結果</td>
      </tr>
  </tbody>
</table>
<p>等待 I/O 的核心訊號是目前流程會花時間等外部回應，例如讀檔、呼叫 HTTP API、等待資料庫查詢或讀取 <a href="/blog/backend/knowledge-cards/socket/" data-link-title="Socket" data-link-desc="說明 network socket 如何成為 application 與網路之間的資料傳輸邊界">socket</a>。這類工作放進 goroutine 後，呼叫端可以繼續處理其他事件，但仍然要用 context 或 channel 管理結果與取消。</p>
<p>背景 worker 的核心訊號是工作來自 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> 或 channel，而且處理時間和 request 生命週期分離。例如使用者送出匯入任務後，server 只先接受任務，後續由 worker 逐筆處理資料。這種 goroutine 通常需要明確的 job channel、錯誤回報與 shutdown 流程。</p>
<p>定時任務的核心訊號是行為按時間觸發，例如每分鐘清理過期 session、同步外部狀態或刷新快取。這類 goroutine 應使用 ticker 搭配 context，讓服務停止時可以一起退出。</p>
<p>多個獨立請求的核心訊號是多個工作彼此沒有順序依賴，例如同時查三個外部 API，最後合併結果。這類 goroutine 的重點是收集結果、限制並發數量，並在其中一個工作失敗時決定是否取消其他工作。</p>
<p>需要先補齊生命週期設計的情境：</p>
<ul>
<li>只是想讓程式「看起來比較快」</li>
<li>沒有任何退出條件</li>
<li>呼叫端需要結果但沒有同步機制</li>
<li>多個 goroutine 會同時修改共享資料但沒有保護</li>
</ul>
<h2 id="執行用-waitgroup-等待一組工作">【執行】用 WaitGroup 等待一組工作</h2>
<p><code>sync.WaitGroup</code> 的核心用途是等待一組 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">main</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">var</span> <span class="nx">wg</span> <span class="nx">sync</span><span class="p">.</span><span class="nx">WaitGroup</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="nx">i</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="p">&lt;</span> <span class="mi">3</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="nx">wg</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="mi">1</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="nx">id</span> <span class="kt">int</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="nx">wg</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="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;worker&#34;</span><span class="p">,</span> <span class="nx">id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="p">}(</span><span class="nx">i</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="nx">wg</span><span class="p">.</span><span class="nf">Wait</span><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>這段程式有三個關鍵：</p>
<table>
  <thead>
      <tr>
          <th>動作</th>
          <th>意義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>wg.Add(1)</code></td>
          <td>增加一個待完成工作</td>
      </tr>
      <tr>
          <td><code>defer wg.Done()</code></td>
          <td>goroutine 結束時標記完成</td>
      </tr>
      <tr>
          <td><code>wg.Wait()</code></td>
          <td>等待所有工作完成</td>
      </tr>
  </tbody>
</table>
<p><code>id</code> 作為參數傳入 goroutine，可以避免 loop 變數捕捉造成混淆。</p>
<h2 id="長時間-goroutine-要能停止">長時間 goroutine 要能停止</h2>
<p>長時間 goroutine 的核心規則是：迴圈中必須等待取消訊號或輸入 channel 關閉。</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">worker</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">handle</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，它都會退出。</p>
<h2 id="設計檢查">設計檢查</h2>
<h3 id="需要結果時要有等待機制">需要結果時要有等待機制</h3>
<p>需要結果或完成保證時，goroutine 應搭配 channel 或 <code>WaitGroup</code>。<code>go doWork()</code> 只負責啟動工作，結果收集與完成等待需要另外設計。</p>
<h3 id="錯誤要有回報路徑">錯誤要有回報路徑</h3>
<p>goroutine 裡的錯誤需要明確回報路徑。需要錯誤結果時，用 channel 傳回：</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">errCh</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kt">error</span><span class="p">,</span> <span class="mi">1</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="nx">errCh</span> <span class="o">&lt;-</span> <span class="nf">doWork</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></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="o">&lt;-</span><span class="nx">errCh</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">7</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="長時間工作要有退出條件">長時間工作要有退出條件</h3>
<p>長時間 worker 至少要監聽 context 或 channel close。永遠 <code>for {}</code> 會讓 goroutine 生命週期失去 owner，服務停止時也難以清理。</p>
]]></content:encoded></item><item><title>模組一：進階並發模式</title><link>https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/</guid><description>&lt;p>Go 並發設計的核心是明確定義 ownership、生命週期、 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a> 與共享狀態邊界。goroutine 很便宜，但失控的 goroutine、關閉錯誤的 channel、無限制堆積的 &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>本模組承接入門篇的 goroutine、channel、select、mutex 基礎，進一步處理長時間運行服務會遇到的問題：誰能關閉 channel、worker 如何停止、channel 滿載時怎麼回應、共享 map/slice 如何避免 data race、工作量如何限制、入口速率如何控制。&lt;/p>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/channel-ownership/" data-link-title="1.1 channel ownership 與關閉責任" data-link-desc="判斷誰能送出、接收與關閉 channel">1.1&lt;/a>&lt;/td>
 &lt;td>channel ownership 與關閉責任&lt;/td>
 &lt;td>用 sender lifecycle 判斷誰能 close channel&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/select-loop/" data-link-title="1.2 select loop 的生命週期設計" data-link-desc="理解長時間運行 goroutine 如何同時處理事件、ticker 與取消">1.2&lt;/a>&lt;/td>
 &lt;td>select loop 的生命週期設計&lt;/td>
 &lt;td>同時處理輸入、ticker、取消與資源釋放&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/non-blocking-send/" data-link-title="1.3 非阻塞送出與事件丟棄策略" data-link-desc="設計 channel 滿載時的服務行為">1.3&lt;/a>&lt;/td>
 &lt;td>非阻塞送出與事件丟棄策略&lt;/td>
 &lt;td>把 channel 滿載轉成明確服務行為&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/shared-state/" data-link-title="1.4 共享狀態與複製邊界" data-link-desc="用 lock 與 copy 保護長期服務的狀態資料">1.4&lt;/a>&lt;/td>
 &lt;td>共享狀態與複製邊界&lt;/td>
 &lt;td>用 lock、copy 與 owner method 保護可變資料&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/worker-pool/" data-link-title="1.5 bounded worker pool" data-link-desc="限制同時執行的 goroutine 數量，讓背景工作有明確容量邊界">1.5&lt;/a>&lt;/td>
 &lt;td>bounded &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool&lt;/a>&lt;/td>
 &lt;td>限制同時執行的工作量，避免 goroutine 無限制堆積&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/rate-limit/" data-link-title="1.6 rate limiting 與 backpressure " data-link-desc="用本地速率限制與 backpressure 策略保護服務入口與下游依賴">1.6&lt;/a>&lt;/td>
 &lt;td>rate limiting 與 backpressure&lt;/td>
 &lt;td>用本地速率限制保護服務入口與下游依賴&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="本模組使用的範例主題">本模組使用的範例主題&lt;/h2>
&lt;p>本模組使用虛構的通知與工作處理服務作為範例。範例會包含背景 worker、事件佇列、即時推送、狀態 repository 與測試 fake。&lt;/p>
&lt;p>範例只用來展示 Go 並發設計方法，不假設讀者正在維護任何特定專案。&lt;/p>
&lt;h2 id="本模組的-go-核心概念">本模組的 Go 核心概念&lt;/h2>
&lt;ul>
&lt;li>用 channel direction 表達 send-only 與 receive-only 能力。&lt;/li>
&lt;li>用 context 作為 goroutine 停止訊號。&lt;/li>
&lt;li>用 select 管理多種輸入與 ticker。&lt;/li>
&lt;li>用 buffered channel 吸收短暫尖峰，但不把 buffer 當成容量規劃替代品。&lt;/li>
&lt;li>用 mutex 保護共享 map/slice。&lt;/li>
&lt;li>用 copy boundary 防止呼叫端修改內部狀態。&lt;/li>
&lt;li>用 worker pool 控制同時執行數。&lt;/li>
&lt;li>用 rate limiter 把過量輸入轉成可預期回應。&lt;/li>
&lt;li>用 race detector 與 focused tests 驗證並發邊界。&lt;/li>
&lt;/ul>
&lt;h2 id="學習重點">學習重點&lt;/h2>
&lt;p>學完本模組後，你應該能判斷：&lt;/p>
&lt;ol>
&lt;li>哪個 goroutine 擁有 channel 的關閉責任&lt;/li>
&lt;li>一個長期 worker 停止時需要釋放哪些資源&lt;/li>
&lt;li>channel 滿載時應該等待、回錯、丟棄還是降級&lt;/li>
&lt;li>map、slice、pointer 何時會洩漏內部狀態&lt;/li>
&lt;li>什麼情況下 mutex 比 channel 更適合表達狀態擁有權&lt;/li>
&lt;li>什麼情況下需要 bounded worker pool&lt;/li>
&lt;li>入口過量時應排隊、限速、拒絕還是降級&lt;/li>
&lt;/ol>
&lt;h2 id="本模組不處理">本模組不處理&lt;/h2>
&lt;p>本模組不討論分散式鎖、actor framework 或高階 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> 系統。這些主題建立在本模組的基礎之上；本模組先把單一 Go process 內的 goroutine、worker、速率與共享資料邊界講清楚。外部 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> 與分散式流量治理會放在 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Backend：訊息佇列與事件傳遞&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Go 並發設計的核心是明確定義 ownership、生命週期、 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> 與共享狀態邊界。goroutine 很便宜，但失控的 goroutine、關閉錯誤的 channel、無限制堆積的 <a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a>，以及外洩的可變資料都會讓服務難以維護。</p>
<p>本模組承接入門篇的 goroutine、channel、select、mutex 基礎，進一步處理長時間運行服務會遇到的問題：誰能關閉 channel、worker 如何停止、channel 滿載時怎麼回應、共享 map/slice 如何避免 data race、工作量如何限制、入口速率如何控制。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/go-advanced/01-concurrency-patterns/channel-ownership/" data-link-title="1.1 channel ownership 與關閉責任" data-link-desc="判斷誰能送出、接收與關閉 channel">1.1</a></td>
          <td>channel ownership 與關閉責任</td>
          <td>用 sender lifecycle 判斷誰能 close channel</td>
      </tr>
      <tr>
          <td><a href="/blog/go-advanced/01-concurrency-patterns/select-loop/" data-link-title="1.2 select loop 的生命週期設計" data-link-desc="理解長時間運行 goroutine 如何同時處理事件、ticker 與取消">1.2</a></td>
          <td>select loop 的生命週期設計</td>
          <td>同時處理輸入、ticker、取消與資源釋放</td>
      </tr>
      <tr>
          <td><a href="/blog/go-advanced/01-concurrency-patterns/non-blocking-send/" data-link-title="1.3 非阻塞送出與事件丟棄策略" data-link-desc="設計 channel 滿載時的服務行為">1.3</a></td>
          <td>非阻塞送出與事件丟棄策略</td>
          <td>把 channel 滿載轉成明確服務行為</td>
      </tr>
      <tr>
          <td><a href="/blog/go-advanced/01-concurrency-patterns/shared-state/" data-link-title="1.4 共享狀態與複製邊界" data-link-desc="用 lock 與 copy 保護長期服務的狀態資料">1.4</a></td>
          <td>共享狀態與複製邊界</td>
          <td>用 lock、copy 與 owner method 保護可變資料</td>
      </tr>
      <tr>
          <td><a href="/blog/go-advanced/01-concurrency-patterns/worker-pool/" data-link-title="1.5 bounded worker pool" data-link-desc="限制同時執行的 goroutine 數量，讓背景工作有明確容量邊界">1.5</a></td>
          <td>bounded <a href="/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool</a></td>
          <td>限制同時執行的工作量，避免 goroutine 無限制堆積</td>
      </tr>
      <tr>
          <td><a href="/blog/go-advanced/01-concurrency-patterns/rate-limit/" data-link-title="1.6 rate limiting 與 backpressure " data-link-desc="用本地速率限制與 backpressure 策略保護服務入口與下游依賴">1.6</a></td>
          <td>rate limiting 與 backpressure</td>
          <td>用本地速率限制保護服務入口與下游依賴</td>
      </tr>
  </tbody>
</table>
<h2 id="本模組使用的範例主題">本模組使用的範例主題</h2>
<p>本模組使用虛構的通知與工作處理服務作為範例。範例會包含背景 worker、事件佇列、即時推送、狀態 repository 與測試 fake。</p>
<p>範例只用來展示 Go 並發設計方法，不假設讀者正在維護任何特定專案。</p>
<h2 id="本模組的-go-核心概念">本模組的 Go 核心概念</h2>
<ul>
<li>用 channel direction 表達 send-only 與 receive-only 能力。</li>
<li>用 context 作為 goroutine 停止訊號。</li>
<li>用 select 管理多種輸入與 ticker。</li>
<li>用 buffered channel 吸收短暫尖峰，但不把 buffer 當成容量規劃替代品。</li>
<li>用 mutex 保護共享 map/slice。</li>
<li>用 copy boundary 防止呼叫端修改內部狀態。</li>
<li>用 worker pool 控制同時執行數。</li>
<li>用 rate limiter 把過量輸入轉成可預期回應。</li>
<li>用 race detector 與 focused tests 驗證並發邊界。</li>
</ul>
<h2 id="學習重點">學習重點</h2>
<p>學完本模組後，你應該能判斷：</p>
<ol>
<li>哪個 goroutine 擁有 channel 的關閉責任</li>
<li>一個長期 worker 停止時需要釋放哪些資源</li>
<li>channel 滿載時應該等待、回錯、丟棄還是降級</li>
<li>map、slice、pointer 何時會洩漏內部狀態</li>
<li>什麼情況下 mutex 比 channel 更適合表達狀態擁有權</li>
<li>什麼情況下需要 bounded worker pool</li>
<li>入口過量時應排隊、限速、拒絕還是降級</li>
</ol>
<h2 id="本模組不處理">本模組不處理</h2>
<p>本模組不討論分散式鎖、actor framework 或高階 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> 系統。這些主題建立在本模組的基礎之上；本模組先把單一 Go process 內的 goroutine、worker、速率與共享資料邊界講清楚。外部 <a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> 與分散式流量治理會放在 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Backend：訊息佇列與事件傳遞</a> 與 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口</a>。</p>
<h2 id="先備知識">先備知識</h2>
<ul>
<li><a href="/blog/go/04-concurrency/" data-link-title="模組四：並發模型" data-link-desc="從 goroutine、channel、select 與 RWMutex 理解 Go 並發模型">Go 入門：並發模型</a></li>
<li>知道 goroutine、channel、select、mutex 的基本用法</li>
</ul>
<h2 id="學習時間">學習時間</h2>
<p>預計 4-5 小時</p>
]]></content:encoded></item><item><title>1.2 select loop 的生命週期設計</title><link>https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/select-loop/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/select-loop/</guid><description>&lt;p>&lt;code>select&lt;/code> loop 的核心責任是管理長時間 goroutine 的生命週期。它不只是等待多個 channel 的語法，而是決定元件如何接收輸入、處理定時任務、回應取消、釋放資源與停止。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>拆解 &lt;code>select&lt;/code> loop 中每個 case 的責任&lt;/li>
&lt;li>用 &lt;code>ctx.Done()&lt;/code> 設計一致的退出路徑&lt;/li>
&lt;li>正確建立與停止 ticker&lt;/li>
&lt;li>處理 channel 關閉後的 nil channel pattern&lt;/li>
&lt;li>測試 worker 在事件、ticker、取消下的行為&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察長期-goroutine-通常同時等待多種訊號">【觀察】長期 goroutine 通常同時等待多種訊號&lt;/h2>
&lt;p>長期 goroutine 的核心特徵是它不只處理一種資料。背景 worker 可能同時等待外部事件、定時掃描、清理工作與停止訊號。&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">w&lt;/span> &lt;span class="nx">Worker&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Run&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="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">statusTicker&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewTicker&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">statusInterval&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">statusTicker&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Stop&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">cleanupTicker&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewTicker&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">cleanupInterval&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">defer&lt;/span> &lt;span class="nx">cleanupTicker&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Stop&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&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"> 9&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">10&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">11&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">ctx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Err&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="k">case&lt;/span> &lt;span class="nx">event&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">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">events&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">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">14&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">15&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">processEvent&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">event&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">case&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">statusTicker&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">C&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">scanStatus&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&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">cleanupTicker&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">C&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">cleanup&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 loop 的責任不是「跑一個無限迴圈」，而是定義 worker 活著時能接受哪些訊號，以及停止時要如何退出。&lt;/p>
&lt;h2 id="判讀select-loop-是元件的生命週期表">【判讀】select loop 是元件的生命週期表&lt;/h2>
&lt;p>&lt;code>select&lt;/code> loop 的核心價值是把元件生命週期寫成明確表格。每個 case 都應該能回答：收到什麼訊號、代表什麼意思、下一步做什麼。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>case&lt;/th>
 &lt;th>系統意義&lt;/th>
 &lt;th>下一步&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>ctx.Done()&lt;/code>&lt;/td>
 &lt;td>上層要求停止&lt;/td>
 &lt;td>回傳 context error&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>w.events&lt;/code>&lt;/td>
 &lt;td>收到外部事件&lt;/td>
 &lt;td>套用處理流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>statusTicker.C&lt;/code>&lt;/td>
 &lt;td>到時間掃描狀態&lt;/td>
 &lt;td>執行週期任務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>cleanupTicker.C&lt;/code>&lt;/td>
 &lt;td>到時間清理暫存資料&lt;/td>
 &lt;td>回收資源&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>若某個 case 的意義說不清楚，通常代表 worker 責任太多，或事件來源還沒有被整理成清楚的 channel。&lt;/p>
&lt;h2 id="策略每個長期-goroutine-先回答四個問題">【策略】每個長期 goroutine 先回答四個問題&lt;/h2>
&lt;p>Select loop 設計的核心檢查是生命週期，而不是語法。寫 loop 前先回答四個問題：&lt;/p>
&lt;ol>
&lt;li>誰能停止它？&lt;/li>
&lt;li>它消費哪些輸入？&lt;/li>
&lt;li>它擁有哪些資源？&lt;/li>
&lt;li>停止時要回報錯誤、靜默退出，還是交給上層判斷？&lt;/li>
&lt;/ol>
&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">type&lt;/span> &lt;span class="nx">Worker&lt;/span> &lt;span class="kd">struct&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">events&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="nx">Event&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">statusInterval&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Duration&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">cleanupInterval&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Duration&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nx">processor&lt;/span> &lt;span class="nx">Processor&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;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Worker&lt;/code> 消費 &lt;code>events&lt;/code>，擁有兩個 ticker，停止訊號來自 &lt;code>context.Context&lt;/code>。這些資訊應該能從型別與 &lt;code>Run&lt;/code> 方法看出來，而不是藏在任意 goroutine 裡。&lt;/p>
&lt;h2 id="執行ticker-要由使用者停止">【執行】ticker 要由使用者停止&lt;/h2>
&lt;p>Ticker 的核心規則是建立者負責停止。&lt;code>time.NewTicker&lt;/code> 會建立 runtime 資源；不再使用時應呼叫 &lt;code>Stop&lt;/code>。&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">w&lt;/span> &lt;span class="nx">Worker&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Run&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="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">ticker&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewTicker&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">interval&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">ticker&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Stop&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="k">for&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">select&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">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"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">ctx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Err&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">case&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">ticker&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">C&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="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">SyncOnce&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">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">11&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">err&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;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 class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ticker 放在 &lt;code>Run&lt;/code> 裡建立，表示它的生命週期和 &lt;code>Run&lt;/code> 一致。&lt;code>defer ticker.Stop()&lt;/code> 讓 worker 不論因為 context、錯誤或 channel 關閉退出，都能釋放 ticker。&lt;/p>
&lt;p>如果 ticker 由外部傳入，外部就應該負責停止。擁有權要一致，否則測試和 shutdown 都會變得模糊。&lt;/p>
&lt;h2 id="執行處理已關閉-channel-要避免忙等">【執行】處理已關閉 channel 要避免忙等&lt;/h2>
&lt;p>已關閉 channel 的核心行為是讀取會立即回傳零值與 &lt;code>ok=false&lt;/code>。在 &lt;code>select&lt;/code> loop 裡，如果不處理這件事，loop 可能一直選到同一個已關閉 channel。&lt;/p>
&lt;p>當一個輸入關閉後，還要繼續處理其他輸入，可以把它設成 nil：&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">w&lt;/span> &lt;span class="nx">Worker&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Run&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="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">events&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">events&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">alerts&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">alerts&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="k">for&lt;/span> &lt;span class="nx">events&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">alerts&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"> 6&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"> 7&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"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">ctx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Err&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">case&lt;/span> &lt;span class="nx">event&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">events&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="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">11&lt;/span>&lt;span class="cl"> &lt;span class="nx">events&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">12&lt;/span>&lt;span class="cl"> &lt;span class="k">continue&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;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">processEvent&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">event&lt;/span>&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 class="k">case&lt;/span> &lt;span class="nx">alert&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">alerts&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&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">17&lt;/span>&lt;span class="cl"> &lt;span class="nx">alerts&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">18&lt;/span>&lt;span class="cl"> &lt;span class="k">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">processAlert&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">alert&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&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">25&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Nil channel 在 &lt;code>select&lt;/code> 中永遠不會 ready。這讓 worker 能在某個來源關閉後繼續處理其他來源，而不是忙等或提早退出。&lt;/p></description><content:encoded><![CDATA[<p><code>select</code> loop 的核心責任是管理長時間 goroutine 的生命週期。它不只是等待多個 channel 的語法，而是決定元件如何接收輸入、處理定時任務、回應取消、釋放資源與停止。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>拆解 <code>select</code> loop 中每個 case 的責任</li>
<li>用 <code>ctx.Done()</code> 設計一致的退出路徑</li>
<li>正確建立與停止 ticker</li>
<li>處理 channel 關閉後的 nil channel pattern</li>
<li>測試 worker 在事件、ticker、取消下的行為</li>
</ol>
<hr>
<h2 id="觀察長期-goroutine-通常同時等待多種訊號">【觀察】長期 goroutine 通常同時等待多種訊號</h2>
<p>長期 goroutine 的核心特徵是它不只處理一種資料。背景 worker 可能同時等待外部事件、定時掃描、清理工作與停止訊號。</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">w</span> <span class="nx">Worker</span><span class="p">)</span> <span class="nf">Run</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="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">statusTicker</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">w</span><span class="p">.</span><span class="nx">statusInterval</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">statusTicker</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="nx">cleanupTicker</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">w</span><span class="p">.</span><span class="nx">cleanupInterval</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">defer</span> <span class="nx">cleanupTicker</span><span class="p">.</span><span class="nf">Stop</span><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="k">for</span> <span class="p">{</span>
</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">ctx</span><span class="p">.</span><span class="nf">Done</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="k">return</span> <span class="nx">ctx</span><span class="p">.</span><span class="nf">Err</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="k">case</span> <span class="nx">event</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">w</span><span class="p">.</span><span class="nx">events</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="p">!</span><span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">                <span class="k">return</span> <span class="kc">nil</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="nx">w</span><span class="p">.</span><span class="nf">processEvent</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">event</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">statusTicker</span><span class="p">.</span><span class="nx">C</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">            <span class="nx">w</span><span class="p">.</span><span class="nf">scanStatus</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">cleanupTicker</span><span class="p">.</span><span class="nx">C</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">            <span class="nx">w</span><span class="p">.</span><span class="nf">cleanup</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個 loop 的責任不是「跑一個無限迴圈」，而是定義 worker 活著時能接受哪些訊號，以及停止時要如何退出。</p>
<h2 id="判讀select-loop-是元件的生命週期表">【判讀】select loop 是元件的生命週期表</h2>
<p><code>select</code> loop 的核心價值是把元件生命週期寫成明確表格。每個 case 都應該能回答：收到什麼訊號、代表什麼意思、下一步做什麼。</p>
<table>
  <thead>
      <tr>
          <th>case</th>
          <th>系統意義</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ctx.Done()</code></td>
          <td>上層要求停止</td>
          <td>回傳 context error</td>
      </tr>
      <tr>
          <td><code>w.events</code></td>
          <td>收到外部事件</td>
          <td>套用處理流程</td>
      </tr>
      <tr>
          <td><code>statusTicker.C</code></td>
          <td>到時間掃描狀態</td>
          <td>執行週期任務</td>
      </tr>
      <tr>
          <td><code>cleanupTicker.C</code></td>
          <td>到時間清理暫存資料</td>
          <td>回收資源</td>
      </tr>
  </tbody>
</table>
<p>若某個 case 的意義說不清楚，通常代表 worker 責任太多，或事件來源還沒有被整理成清楚的 channel。</p>
<h2 id="策略每個長期-goroutine-先回答四個問題">【策略】每個長期 goroutine 先回答四個問題</h2>
<p>Select loop 設計的核心檢查是生命週期，而不是語法。寫 loop 前先回答四個問題：</p>
<ol>
<li>誰能停止它？</li>
<li>它消費哪些輸入？</li>
<li>它擁有哪些資源？</li>
<li>停止時要回報錯誤、靜默退出，還是交給上層判斷？</li>
</ol>
<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">Worker</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">events</span>          <span class="o">&lt;-</span><span class="kd">chan</span> <span class="nx">Event</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">statusInterval</span>  <span class="nx">time</span><span class="p">.</span><span class="nx">Duration</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">cleanupInterval</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Duration</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nx">processor</span>       <span class="nx">Processor</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>Worker</code> 消費 <code>events</code>，擁有兩個 ticker，停止訊號來自 <code>context.Context</code>。這些資訊應該能從型別與 <code>Run</code> 方法看出來，而不是藏在任意 goroutine 裡。</p>
<h2 id="執行ticker-要由使用者停止">【執行】ticker 要由使用者停止</h2>
<p>Ticker 的核心規則是建立者負責停止。<code>time.NewTicker</code> 會建立 runtime 資源；不再使用時應呼叫 <code>Stop</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="p">(</span><span class="nx">w</span> <span class="nx">Worker</span><span class="p">)</span> <span class="nf">Run</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="kt">error</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">w</span><span class="p">.</span><span class="nx">interval</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 class="nx">ctx</span><span class="p">.</span><span class="nf">Err</span><span class="p">()</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="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">w</span><span class="p">.</span><span class="nf">SyncOnce</span><span class="p">(</span><span class="nx">ctx</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">11</span><span class="cl">                <span class="k">return</span> <span class="nx">err</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><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>Ticker 放在 <code>Run</code> 裡建立，表示它的生命週期和 <code>Run</code> 一致。<code>defer ticker.Stop()</code> 讓 worker 不論因為 context、錯誤或 channel 關閉退出，都能釋放 ticker。</p>
<p>如果 ticker 由外部傳入，外部就應該負責停止。擁有權要一致，否則測試和 shutdown 都會變得模糊。</p>
<h2 id="執行處理已關閉-channel-要避免忙等">【執行】處理已關閉 channel 要避免忙等</h2>
<p>已關閉 channel 的核心行為是讀取會立即回傳零值與 <code>ok=false</code>。在 <code>select</code> loop 裡，如果不處理這件事，loop 可能一直選到同一個已關閉 channel。</p>
<p>當一個輸入關閉後，還要繼續處理其他輸入，可以把它設成 nil：</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">w</span> <span class="nx">Worker</span><span class="p">)</span> <span class="nf">Run</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="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">events</span> <span class="o">:=</span> <span class="nx">w</span><span class="p">.</span><span class="nx">events</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">alerts</span> <span class="o">:=</span> <span class="nx">w</span><span class="p">.</span><span class="nx">alerts</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="nx">events</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="o">||</span> <span class="nx">alerts</span> <span class="o">!=</span> <span class="kc">nil</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 class="nx">ctx</span><span class="p">.</span><span class="nf">Err</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">case</span> <span class="nx">event</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">events</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">10</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">11</span><span class="cl">                <span class="nx">events</span> <span class="p">=</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">                <span class="k">continue</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="nx">w</span><span class="p">.</span><span class="nf">processEvent</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">event</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="k">case</span> <span class="nx">alert</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">alerts</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">16</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">17</span><span class="cl">                <span class="nx">alerts</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="k">continue</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">            <span class="nx">w</span><span class="p">.</span><span class="nf">processAlert</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">alert</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Nil channel 在 <code>select</code> 中永遠不會 ready。這讓 worker 能在某個來源關閉後繼續處理其他來源，而不是忙等或提早退出。</p>
<h2 id="判讀default-case-會改變-loop-的語意">【判讀】default case 會改變 loop 的語意</h2>
<p><code>default</code> 的核心效果是讓 <code>select</code> 不等待。這在非阻塞送出很有用，但在長期 worker 的主 loop 中要小心，因為它可能造成 busy loop。</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="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</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"> 4</span><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">case</span> <span class="nx">event</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">events</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="nf">process</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nf">cleanup</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="p">}</span></span></span></code></pre></div><p>當沒有事件時，這個 loop 會不停執行 <code>cleanup()</code>，可能吃滿 CPU。週期任務應該用 ticker 表達，不應用 <code>default</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="nx">cleanupTicker</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"> 2</span><span class="cl"><span class="k">defer</span> <span class="nx">cleanupTicker</span><span class="p">.</span><span class="nf">Stop</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">event</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">events</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nf">process</span><span class="p">(</span><span class="nx">event</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">cleanupTicker</span><span class="p">.</span><span class="nx">C</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="nf">cleanup</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>Ticker 讓頻率明確，也讓測試可以透過可控時間或手動觸發 channel 驗證行為。</p>
<h2 id="策略長工作要移出主要-loop">【策略】長工作要移出主要 loop</h2>
<p>Select loop 的核心風險是某個 case 裡的工作太久，導致其他訊號無法被處理。若 <code>processEvent</code> 可能執行很久，worker 在這段期間就不會回應 context 或 ticker。</p>
<p>可選策略：</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>適用情境</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>case 內同步執行</td>
          <td>工作短、需要順序處理</td>
          <td>慢事件會阻塞整個 loop</td>
      </tr>
      <tr>
          <td>啟動 bounded <a href="/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool</a></td>
          <td>工作可並行、需要限制併發</td>
          <td>需要排隊與 shutdown 設計</td>
      </tr>
      <tr>
          <td>送入另一個 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a></td>
          <td>入口要快速接收</td>
          <td>需要 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> 策略</td>
      </tr>
  </tbody>
</table>
<p>長工作需要 bounded worker pool、另一個 queue 或明確的同步策略。無限制地在 case 裡 <code>go process(event)</code> 只會把排隊問題從 channel 轉成 goroutine 堆積，並讓 shutdown 和錯誤回報更難處理。</p>
<h2 id="測試把單次工作抽成方法">【測試】把單次工作抽成方法</h2>
<p>Select loop 的測試核心是避免所有邏輯都只能透過無限迴圈測。把單次工作抽成 <code>SyncOnce</code>、<code>ProcessOne</code> 或 <code>CleanupOnce</code>，可以讓規則測試和 lifecycle 測試分開。</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">w</span> <span class="nx">Worker</span><span class="p">)</span> <span class="nf">SyncOnce</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="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">return</span> <span class="nx">w</span><span class="p">.</span><span class="nx">processor</span><span class="p">.</span><span class="nf">Sync</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>Run</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="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">2</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">w</span><span class="p">.</span><span class="nf">SyncOnce</span><span class="p">(</span><span class="nx">ctx</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">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></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">TestSyncOnceCallsProcessor</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">processor</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">fakeProcessor</span><span class="p">{}</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">worker</span> <span class="o">:=</span> <span class="nx">Worker</span><span class="p">{</span><span class="nx">processor</span><span class="p">:</span> <span class="nx">processor</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">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">worker</span><span class="p">.</span><span class="nf">SyncOnce</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</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"> 6</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;sync once: %v&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">if</span> <span class="p">!</span><span class="nx">processor</span><span class="p">.</span><span class="nx">called</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</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;processor should be called&#34;</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 class="p">}</span></span></span></code></pre></div><p>Lifecycle 測試則只確認 context 取消能讓 <code>Run</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">TestRunStopsOnContextCancel</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="nf">cancel</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">worker</span> <span class="o">:=</span> <span class="nx">Worker</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="nx">events</span><span class="p">:</span>          <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="nx">Event</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">statusInterval</span><span class="p">:</span>  <span class="nx">time</span><span class="p">.</span><span class="nx">Hour</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">cleanupInterval</span><span class="p">:</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Hour</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="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">worker</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="nx">ctx</span><span class="p">);</span> <span class="p">!</span><span class="nx">errors</span><span class="p">.</span><span class="nf">Is</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Canceled</span><span class="p">)</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;run error = %v, want context canceled&#34;</span><span class="p">,</span> <span class="nx">err</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>這種拆法讓測試不需要等待真實 ticker，也不需要在無限 loop 裡猜時間。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先把長生命週期 goroutine 的停止、輸入與排空講清楚；更完整的 worker 協調與平台排程責任，會在下列章節再往外延伸：</p>
<ul>
<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/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">Go 進階：graceful shutdown 與 signal handling</a></li>
<li><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">Backend：可靠性驗證流程</a></li>
</ul>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 goroutine、channel 與 shutdown loop；如果你要先回看語言教材，可以讀：</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/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">Go：bounded worker pool</a></li>
<li><a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">Go：graceful shutdown 與 signal handling</a></li>
</ul>
<h2 id="小結">小結</h2>
<p><code>select</code> loop 是長期 goroutine 的生命週期表。好的 loop 會明確處理 context 取消、輸入 channel、ticker、資源釋放與 channel 關閉。避免在主 loop 中濫用 <code>default</code> 或無限制開 goroutine，才能讓服務在高流量、錯誤與 shutdown 情境下保持可預測。</p>
]]></content:encoded></item><item><title>4.2 channel：資料傳遞與 backpressure</title><link>https://tarrragon.github.io/blog/go/04-concurrency/channel/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/04-concurrency/channel/</guid><description>&lt;p>channel 是 Go 用來在 goroutine 之間傳遞資料的同步工具。它的核心意義是建立資料流邊界：誰送出資料、誰接收資料、當接收端跟不上時送出端如何被阻擋或丟棄。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>建立與使用 channel&lt;/li>
&lt;li>看懂 channel 的方向與資料型別&lt;/li>
&lt;li>理解 buffered channel 的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a> 意義&lt;/li>
&lt;li>分辨 blocking send 與 non-blocking send&lt;/li>
&lt;li>用 channel 畫出資料流&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察channel-連接送出端與接收端">【觀察】channel 連接送出端與接收端&lt;/h2>
&lt;p>channel 的核心規則是：送出端用 &lt;code>&amp;lt;-&lt;/code> 把值放入 channel，接收端用 &lt;code>&amp;lt;-&lt;/code> 從 channel 取出值。以下範例建立一個傳遞 &lt;code>string&lt;/code> 的 channel：&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="nx">messages&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&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>&lt;/span>&lt;span class="line">&lt;span class="ln">3&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">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">messages&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="s">&amp;#34;hello&amp;#34;&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">msg&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">messages&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">msg&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>make(chan string)&lt;/code> 建立只能傳 &lt;code>string&lt;/code> 的 channel。&lt;code>messages &amp;lt;- &amp;quot;hello&amp;quot;&lt;/code> 是送出，&lt;code>msg := &amp;lt;-messages&lt;/code> 是接收。&lt;/p>
&lt;h2 id="判讀channel-是同步點不只是佇列">【判讀】channel 是同步點，不只是佇列&lt;/h2>
&lt;p>unbuffered channel 的核心規則是：送出和接收必須同時準備好，資料才會通過。這表示 channel 也是同步點。&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="nx">ch&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="kt">int&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>&lt;/span>&lt;span class="line">&lt;span class="ln">3&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">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">ch&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="c1">// 等到有人接收才會繼續&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">value&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">ch&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">value&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>buffered channel 的核心規則是：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a> 未滿時送出不會阻塞，buffer 滿時送出會阻塞。&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="nx">jobs&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&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="mi">10&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">jobs&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">Job&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">ID&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s">&amp;#34;1&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>buffer 大小不是隨便的數字。它代表系統允許累積多少尚未處理的工作；接收端處理速度跟不上時，buffer 會逐漸填滿，最後形成 backpressure 。&lt;/p>
&lt;h2 id="策略用方向限制表達所有權">【策略】用方向限制表達所有權&lt;/h2>
&lt;p>channel direction 的核心規則是：函式簽名應限制自己只需要的能力。Go 可以用 channel direction 表達函式只讀或只寫：&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">producer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">out&lt;/span> &lt;span class="kd">chan&lt;/span>&lt;span class="o">&amp;lt;-&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="nx">out&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">Job&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">ID&lt;/span>&lt;span class="p">:&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">3&lt;/span>&lt;span class="cl">&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="kd">func&lt;/span> &lt;span class="nf">consumer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">in&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">6&lt;/span>&lt;span class="cl"> &lt;span class="nx">job&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">in&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="nf">handle&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">8&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>chan&amp;lt;- Job&lt;/code> 表示只能送出，&lt;code>&amp;lt;-chan Job&lt;/code> 表示只能接收。這是 API 層的保護：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/producer/" data-link-title="Producer" data-link-desc="說明 producer 如何把工作、事件或資料送入後續處理路徑">producer&lt;/a> 不能讀取 channel，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a> 不能寫入 channel。&lt;/p>
&lt;h2 id="執行non-blocking-send-的取捨">【執行】non-blocking send 的取捨&lt;/h2>
&lt;p>non-blocking send 的核心規則是：送不出去時立即走 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback&lt;/a>，不等待接收端。它適合「寧可丟棄或記錄，也不要卡住呼叫端」的情境。&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="k">select&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">case&lt;/span> &lt;span class="nx">jobs&lt;/span> &lt;span class="o">&amp;lt;-&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">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;job queued&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">job&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ID&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">default&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="nx">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Warn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;job queue full&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">job&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ID&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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個策略的代價是資料可能被丟棄，所以必須記錄 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a> 或回傳明確錯誤。若資料不能丟，就不要用 default；讓送出端阻塞或回傳「系統忙碌」會更誠實。&lt;/p>
&lt;h2 id="關閉-channel">關閉 channel&lt;/h2>
&lt;p>關閉 channel 的核心規則是：由送出端關閉，表示不會再有新資料。接收端可以用 &lt;code>range&lt;/code> 讀到 channel 關閉：&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">producer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">out&lt;/span> &lt;span class="kd">chan&lt;/span>&lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="kt">int&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="nb">close&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">out&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">i&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="p">&amp;lt;&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="o">++&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">out&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">i&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">consumer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">in&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="kt">int&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"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">value&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">in&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="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">value&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;/code>&lt;/pre>&lt;/div>&lt;p>接收端不應關閉自己沒有所有權的 channel；否則送出端可能在送資料時遇到 panic。&lt;/p></description><content:encoded><![CDATA[<p>channel 是 Go 用來在 goroutine 之間傳遞資料的同步工具。它的核心意義是建立資料流邊界：誰送出資料、誰接收資料、當接收端跟不上時送出端如何被阻擋或丟棄。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>建立與使用 channel</li>
<li>看懂 channel 的方向與資料型別</li>
<li>理解 buffered channel 的 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> 意義</li>
<li>分辨 blocking send 與 non-blocking send</li>
<li>用 channel 畫出資料流</li>
</ol>
<hr>
<h2 id="觀察channel-連接送出端與接收端">【觀察】channel 連接送出端與接收端</h2>
<p>channel 的核心規則是：送出端用 <code>&lt;-</code> 把值放入 channel，接收端用 <code>&lt;-</code> 從 channel 取出值。以下範例建立一個傳遞 <code>string</code> 的 channel：</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">messages</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kt">string</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="k">go</span> <span class="kd">func</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">messages</span> <span class="o">&lt;-</span> <span class="s">&#34;hello&#34;</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">msg</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">messages</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">msg</span><span class="p">)</span></span></span></code></pre></div><p><code>make(chan string)</code> 建立只能傳 <code>string</code> 的 channel。<code>messages &lt;- &quot;hello&quot;</code> 是送出，<code>msg := &lt;-messages</code> 是接收。</p>
<h2 id="判讀channel-是同步點不只是佇列">【判讀】channel 是同步點，不只是佇列</h2>
<p>unbuffered channel 的核心規則是：送出和接收必須同時準備好，資料才會通過。這表示 channel 也是同步點。</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">ch</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kt">int</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="k">go</span> <span class="kd">func</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">ch</span> <span class="o">&lt;-</span> <span class="mi">1</span> <span class="c1">// 等到有人接收才會繼續</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">value</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">ch</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">value</span><span class="p">)</span></span></span></code></pre></div><p>buffered channel 的核心規則是：<a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 未滿時送出不會阻塞，buffer 滿時送出會阻塞。</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">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 class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">jobs</span> <span class="o">&lt;-</span> <span class="nx">Job</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;1&#34;</span><span class="p">}</span></span></span></code></pre></div><p>buffer 大小不是隨便的數字。它代表系統允許累積多少尚未處理的工作；接收端處理速度跟不上時，buffer 會逐漸填滿，最後形成 backpressure 。</p>
<h2 id="策略用方向限制表達所有權">【策略】用方向限制表達所有權</h2>
<p>channel direction 的核心規則是：函式簽名應限制自己只需要的能力。Go 可以用 channel direction 表達函式只讀或只寫：</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">producer</span><span class="p">(</span><span class="nx">out</span> <span class="kd">chan</span><span class="o">&lt;-</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="nx">out</span> <span class="o">&lt;-</span> <span class="nx">Job</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;1&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><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="kd">func</span> <span class="nf">consumer</span><span class="p">(</span><span class="nx">in</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">6</span><span class="cl">    <span class="nx">job</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">in</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nf">handle</span><span class="p">(</span><span class="nx">job</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>chan&lt;- Job</code> 表示只能送出，<code>&lt;-chan Job</code> 表示只能接收。這是 API 層的保護：<a href="/blog/backend/knowledge-cards/producer/" data-link-title="Producer" data-link-desc="說明 producer 如何把工作、事件或資料送入後續處理路徑">producer</a> 不能讀取 channel，<a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 不能寫入 channel。</p>
<h2 id="執行non-blocking-send-的取捨">【執行】non-blocking send 的取捨</h2>
<p>non-blocking send 的核心規則是：送不出去時立即走 <a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback</a>，不等待接收端。它適合「寧可丟棄或記錄，也不要卡住呼叫端」的情境。</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">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">case</span> <span class="nx">jobs</span> <span class="o">&lt;-</span> <span class="nx">job</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;job queued&#34;</span><span class="p">,</span> <span class="s">&#34;id&#34;</span><span class="p">,</span> <span class="nx">job</span><span class="p">.</span><span class="nx">ID</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nx">logger</span><span class="p">.</span><span class="nf">Warn</span><span class="p">(</span><span class="s">&#34;job queue full&#34;</span><span class="p">,</span> <span class="s">&#34;id&#34;</span><span class="p">,</span> <span class="nx">job</span><span class="p">.</span><span class="nx">ID</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個策略的代價是資料可能被丟棄，所以必須記錄 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 或回傳明確錯誤。若資料不能丟，就不要用 default；讓送出端阻塞或回傳「系統忙碌」會更誠實。</p>
<h2 id="關閉-channel">關閉 channel</h2>
<p>關閉 channel 的核心規則是：由送出端關閉，表示不會再有新資料。接收端可以用 <code>range</code> 讀到 channel 關閉：</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">producer</span><span class="p">(</span><span class="nx">out</span> <span class="kd">chan</span><span class="o">&lt;-</span> <span class="kt">int</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">out</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">i</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="p">&lt;</span> <span class="mi">3</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="nx">out</span> <span class="o">&lt;-</span> <span class="nx">i</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></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="kd">func</span> <span class="nf">consumer</span><span class="p">(</span><span class="nx">in</span> <span class="o">&lt;-</span><span class="kd">chan</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">for</span> <span class="nx">value</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">in</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">value</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></code></pre></div><p>接收端不應關閉自己沒有所有權的 channel；否則送出端可能在送資料時遇到 panic。</p>
]]></content:encoded></item><item><title>1.3 非阻塞送出與事件丟棄策略</title><link>https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/non-blocking-send/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/non-blocking-send/</guid><description>&lt;p>非阻塞送出的核心取捨是用明確降級換取呼叫端可用性。當 channel 滿載時，程式可以等待、回錯、丟棄、覆蓋或轉交可靠儲存；選擇哪一個是服務語意，不是 &lt;code>select&lt;/code> 語法偏好。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>分辨 blocking send 與 non-blocking send 的服務語意&lt;/li>
&lt;li>為 HTTP、worker、即時推送設計不同滿載策略&lt;/li>
&lt;li>判斷哪些事件可以丟、哪些不能丟&lt;/li>
&lt;li>為 drop 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> full 建立 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>/metric&lt;/li>
&lt;li>測試 channel 滿載時的行為&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察channel-滿載是容量訊號">【觀察】channel 滿載是容量訊號&lt;/h2>
&lt;p>Channel 滿載的核心意義是下游處理速度跟不上上游輸入速度。這可能是短暫尖峰，也可能是系統長期容量不足。&lt;/p>
&lt;p>最直接的 send 會接受 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a> ：&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="nx">events&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">event&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果 &lt;code>events&lt;/code> 沒有 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a>，或 buffer 已滿，sender 會等待 receiver。這能保留資料，但也可能讓 HTTP handler、connection writer 或其他 goroutine 卡住。&lt;/p>
&lt;p>對批次 worker 來說，等待可能合理；對使用者 request 來說，無限等待通常會變成 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 或 goroutine 堆積。&lt;/p>
&lt;h2 id="判讀blocking-send-表示願意等待">【判讀】blocking send 表示願意等待&lt;/h2>
&lt;p>Blocking send 的核心語意是 sender 接受下游 backpressure 。資料不會被丟掉，但 sender 的生命週期會被 receiver 影響。&lt;/p>
&lt;p>有 context 的 blocking send：&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">Enqueue&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">events&lt;/span> &lt;span class="kd">chan&lt;/span>&lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">Event&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&lt;/span> &lt;span class="nx">Event&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="k">select&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">case&lt;/span> &lt;span class="nx">events&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">event&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="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">6&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">ctx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Err&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="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;/code>&lt;/pre>&lt;/div>&lt;p>這種寫法仍然願意等待，但不會無限等待。若 request 被取消或 timeout，send 也會停止。&lt;/p>
&lt;p>Blocking send 適合資料不能丟、上游能等待、且等待時間受 context 控制的情境。若沒有 context，blocking send 在服務入口通常風險較高。&lt;/p>
&lt;h2 id="判讀non-blocking-send-表示立即選擇替代路徑">【判讀】non-blocking send 表示立即選擇替代路徑&lt;/h2>
&lt;p>Non-blocking send 的核心語意是「能送就送，不能送就立刻走其他策略」。Go 常用 &lt;code>select&lt;/code> 加 &lt;code>default&lt;/code> 表達。&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">var&lt;/span> &lt;span class="nx">ErrQueueFull&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">errors&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">New&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;event queue is full&amp;#34;&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>&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">TryEnqueue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">events&lt;/span> &lt;span class="kd">chan&lt;/span>&lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">Event&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&lt;/span> &lt;span class="nx">Event&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"> 4&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"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="nx">events&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">event&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">return&lt;/span> &lt;span class="kc">nil&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">default&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 class="nx">ErrQueueFull&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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段程式不會等待 receiver。當 buffer 滿載時，呼叫端會立刻拿到 &lt;code>ErrQueueFull&lt;/code>，並可以決定回 HTTP 錯誤、記錄 drop、或改走其他儲存。&lt;/p>
&lt;p>Non-blocking send 不是比較進階的寫法。它只是把 backpressure 從「等待」改成「立即決策」。&lt;/p>
&lt;h2 id="策略先定義事件的保留等級">【策略】先定義事件的保留等級&lt;/h2>
&lt;p>滿載策略的核心判斷是資料語意。每種事件都應先定義保留等級：必須保存、可降級、可覆蓋、可取樣，或可延後處理。這個等級決定 channel 滿載時要等待、回錯、丟棄、覆蓋或轉交可靠儲存。&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;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log&lt;/a>&lt;/td>
 &lt;td>不應直接丟，應寫可靠儲存或回錯&lt;/td>
 &lt;td>資料遺失會破壞稽核&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>UI 即時提示&lt;/td>
 &lt;td>可丟棄或覆蓋&lt;/td>
 &lt;td>使用者可重新查詢狀態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>狀態轉移事件&lt;/td>
 &lt;td>通常不應丟&lt;/td>
 &lt;td>會造成 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a> 不一致&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> sample&lt;/td>
 &lt;td>可取樣或丟棄&lt;/td>
 &lt;td>趨勢比單筆資料重要&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>background refresh&lt;/td>
 &lt;td>可跳過本輪&lt;/td>
 &lt;td>下次仍可重新計算&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這個表格的重點是要求每種事件都要有明確策略。若團隊只說「channel 滿了就 default」，通常代表資料語意還沒有想清楚。&lt;/p>
&lt;h2 id="執行http-入口要把滿載轉成狀態碼">【執行】HTTP 入口要把滿載轉成狀態碼&lt;/h2>
&lt;p>HTTP 入口的核心責任是把內部滿載轉成呼叫端能理解的結果。不要讓 request 一直等到 timeout，也不要把未接受的事件回成成功。&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">EventHandler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">events&lt;/span> &lt;span class="kd">chan&lt;/span>&lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">Event&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">HandlerFunc&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">return&lt;/span> &lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ResponseWriter&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">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Request&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">event&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">Event&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">ID&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">Header&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;X-Request-ID&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&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="nf">TryEnqueue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">events&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&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"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">errors&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Is&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ErrQueueFull&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"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Header&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nf">Set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Retry-After&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;5&amp;#34;&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="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;event queue is full&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">StatusServiceUnavailable&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">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;event enqueue failed&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">StatusInternalServerError&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="k">return&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;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteHeader&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">StatusAccepted&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>202 Accepted&lt;/code> 表示事件已被服務接受進入後續處理。&lt;code>503 Service Unavailable&lt;/code> 表示服務目前無法接受更多事件，呼叫端可以稍後重試。&lt;/p></description><content:encoded><![CDATA[<p>非阻塞送出的核心取捨是用明確降級換取呼叫端可用性。當 channel 滿載時，程式可以等待、回錯、丟棄、覆蓋或轉交可靠儲存；選擇哪一個是服務語意，不是 <code>select</code> 語法偏好。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>分辨 blocking send 與 non-blocking send 的服務語意</li>
<li>為 HTTP、worker、即時推送設計不同滿載策略</li>
<li>判斷哪些事件可以丟、哪些不能丟</li>
<li>為 drop 與 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> full 建立 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>/metric</li>
<li>測試 channel 滿載時的行為</li>
</ol>
<hr>
<h2 id="觀察channel-滿載是容量訊號">【觀察】channel 滿載是容量訊號</h2>
<p>Channel 滿載的核心意義是下游處理速度跟不上上游輸入速度。這可能是短暫尖峰，也可能是系統長期容量不足。</p>
<p>最直接的 send 會接受 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> ：</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">events</span> <span class="o">&lt;-</span> <span class="nx">event</span></span></span></code></pre></div><p>如果 <code>events</code> 沒有 <a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a>，或 buffer 已滿，sender 會等待 receiver。這能保留資料，但也可能讓 HTTP handler、connection writer 或其他 goroutine 卡住。</p>
<p>對批次 worker 來說，等待可能合理；對使用者 request 來說，無限等待通常會變成 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 或 goroutine 堆積。</p>
<h2 id="判讀blocking-send-表示願意等待">【判讀】blocking send 表示願意等待</h2>
<p>Blocking send 的核心語意是 sender 接受下游 backpressure 。資料不會被丟掉，但 sender 的生命週期會被 receiver 影響。</p>
<p>有 context 的 blocking send：</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">Enqueue</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">events</span> <span class="kd">chan</span><span class="o">&lt;-</span> <span class="nx">Event</span><span class="p">,</span> <span class="nx">event</span> <span class="nx">Event</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="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">case</span> <span class="nx">events</span> <span class="o">&lt;-</span> <span class="nx">event</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="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">6</span><span class="cl">        <span class="k">return</span> <span class="nx">ctx</span><span class="p">.</span><span class="nf">Err</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <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>這種寫法仍然願意等待，但不會無限等待。若 request 被取消或 timeout，send 也會停止。</p>
<p>Blocking send 適合資料不能丟、上游能等待、且等待時間受 context 控制的情境。若沒有 context，blocking send 在服務入口通常風險較高。</p>
<h2 id="判讀non-blocking-send-表示立即選擇替代路徑">【判讀】non-blocking send 表示立即選擇替代路徑</h2>
<p>Non-blocking send 的核心語意是「能送就送，不能送就立刻走其他策略」。Go 常用 <code>select</code> 加 <code>default</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">ErrQueueFull</span> <span class="p">=</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;event queue is full&#34;</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">TryEnqueue</span><span class="p">(</span><span class="nx">events</span> <span class="kd">chan</span><span class="o">&lt;-</span> <span class="nx">Event</span><span class="p">,</span> <span class="nx">event</span> <span class="nx">Event</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">case</span> <span class="nx">events</span> <span class="o">&lt;-</span> <span class="nx">event</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">default</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">ErrQueueFull</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="p">}</span></span></span></code></pre></div><p>這段程式不會等待 receiver。當 buffer 滿載時，呼叫端會立刻拿到 <code>ErrQueueFull</code>，並可以決定回 HTTP 錯誤、記錄 drop、或改走其他儲存。</p>
<p>Non-blocking send 不是比較進階的寫法。它只是把 backpressure 從「等待」改成「立即決策」。</p>
<h2 id="策略先定義事件的保留等級">【策略】先定義事件的保留等級</h2>
<p>滿載策略的核心判斷是資料語意。每種事件都應先定義保留等級：必須保存、可降級、可覆蓋、可取樣，或可延後處理。這個等級決定 channel 滿載時要等待、回錯、丟棄、覆蓋或轉交可靠儲存。</p>
<table>
  <thead>
      <tr>
          <th>事件類型</th>
          <th>建議策略</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a></td>
          <td>不應直接丟，應寫可靠儲存或回錯</td>
          <td>資料遺失會破壞稽核</td>
      </tr>
      <tr>
          <td>UI 即時提示</td>
          <td>可丟棄或覆蓋</td>
          <td>使用者可重新查詢狀態</td>
      </tr>
      <tr>
          <td>狀態轉移事件</td>
          <td>通常不應丟</td>
          <td>會造成 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a> 不一致</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> sample</td>
          <td>可取樣或丟棄</td>
          <td>趨勢比單筆資料重要</td>
      </tr>
      <tr>
          <td>background refresh</td>
          <td>可跳過本輪</td>
          <td>下次仍可重新計算</td>
      </tr>
  </tbody>
</table>
<p>這個表格的重點是要求每種事件都要有明確策略。若團隊只說「channel 滿了就 default」，通常代表資料語意還沒有想清楚。</p>
<h2 id="執行http-入口要把滿載轉成狀態碼">【執行】HTTP 入口要把滿載轉成狀態碼</h2>
<p>HTTP 入口的核心責任是把內部滿載轉成呼叫端能理解的結果。不要讓 request 一直等到 timeout，也不要把未接受的事件回成成功。</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">EventHandler</span><span class="p">(</span><span class="nx">events</span> <span class="kd">chan</span><span class="o">&lt;-</span> <span class="nx">Event</span><span class="p">)</span> <span class="nx">http</span><span class="p">.</span><span class="nx">HandlerFunc</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">return</span> <span class="kd">func</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</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">event</span> <span class="o">:=</span> <span class="nx">Event</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Header</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="s">&#34;X-Request-ID&#34;</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">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">TryEnqueue</span><span class="p">(</span><span class="nx">events</span><span class="p">,</span> <span class="nx">event</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"> 6</span><span class="cl">            <span class="k">if</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Is</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">ErrQueueFull</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">                <span class="nx">w</span><span class="p">.</span><span class="nf">Header</span><span class="p">().</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;Retry-After&#34;</span><span class="p">,</span> <span class="s">&#34;5&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">                <span class="nx">http</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">&#34;event queue is full&#34;</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusServiceUnavailable</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">http</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">&#34;event enqueue failed&#34;</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusInternalServerError</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="k">return</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></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusAccepted</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 class="p">}</span></span></span></code></pre></div><p><code>202 Accepted</code> 表示事件已被服務接受進入後續處理。<code>503 Service Unavailable</code> 表示服務目前無法接受更多事件，呼叫端可以稍後重試。</p>
<p>若事件不能丟，HTTP handler 應該回錯或寫入可靠儲存，不應假裝成功。</p>
<h2 id="執行即時推送可以選擇-drop-或-disconnect">【執行】即時推送可以選擇 drop 或 disconnect</h2>
<p>即時推送的核心問題是慢 client 不能拖住整個服務。若某個連線的 send buffer 滿了，服務要決定丟掉該訊息、丟掉舊訊息，還是關閉連線。</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">Client</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">send</span> <span class="kd">chan</span> <span class="nx">Message</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><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="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">TrySend</span><span class="p">(</span><span class="nx">message</span> <span class="nx">Message</span><span class="p">)</span> <span class="kt">bool</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="nx">c</span><span class="p">.</span><span class="nx">send</span> <span class="o">&lt;-</span> <span class="nx">message</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="kc">true</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">default</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="kc">false</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></code></pre></div><p>呼叫端可以根據 <code>false</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="k">if</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">client</span><span class="p">.</span><span class="nf">TrySend</span><span class="p">(</span><span class="nx">message</span><span class="p">);</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">metrics</span><span class="p">.</span><span class="nf">Inc</span><span class="p">(</span><span class="s">&#34;client_send_dropped&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">logger</span><span class="p">.</span><span class="nf">Warn</span><span class="p">(</span><span class="s">&#34;drop client message&#34;</span><span class="p">,</span> <span class="s">&#34;reason&#34;</span><span class="p">,</span> <span class="s">&#34;send_buffer_full&#34;</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>對狀態型 UI 來說，丟掉中間更新可能可以接受，因為下一次 snapshot 會補上最新狀態。對逐筆不可遺失訊息來說，應改用可靠佇列或明確斷線重連協定。</p>
<h2 id="策略buffer-只能吸收短暫尖峰">【策略】buffer 只能吸收短暫尖峰</h2>
<p>Buffer 的核心作用是平滑短時間流量差，不是解決長期處理能力不足。把 channel buffer 調大，只是延後滿載時間，也可能增加記憶體與延遲。</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">events</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="nx">Event</span><span class="p">,</span> <span class="mi">1024</span><span class="p">)</span></span></span></code></pre></div><p>設計 buffer 時至少要考慮：</p>
<ul>
<li>單筆事件大小</li>
<li><a href="/blog/backend/knowledge-cards/producer/" data-link-title="Producer" data-link-desc="說明 producer 如何把工作、事件或資料送入後續處理路徑">producer</a> 峰值速度</li>
<li><a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 穩定處理速度</li>
<li>允許排隊延遲</li>
<li>滿載時的回應策略</li>
</ul>
<p>若 producer 每秒 1000 筆、consumer 每秒 100 筆，任何有限 buffer 都會滿。這時要改善 consumer 能力、增加 worker、做取樣、回錯或使用可靠 queue，而不是只調大數字。</p>
<h2 id="策略丟棄一定要可觀測">【策略】丟棄一定要可觀測</h2>
<p>Drop strategy 的核心要求是可觀測。只要系統選擇丟棄或降級，就應該留下 metric 或 structured log，否則資料遺失會變成隱性 bug。</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">TryEnqueueWithMetrics</span><span class="p">(</span><span class="nx">events</span> <span class="kd">chan</span><span class="o">&lt;-</span> <span class="nx">Event</span><span class="p">,</span> <span class="nx">event</span> <span class="nx">Event</span><span class="p">,</span> <span class="nx">logger</span> <span class="o">*</span><span class="nx">slog</span><span class="p">.</span><span class="nx">Logger</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="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">case</span> <span class="nx">events</span> <span class="o">&lt;-</span> <span class="nx">event</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="nx">metrics</span><span class="p">.</span><span class="nf">Inc</span><span class="p">(</span><span class="s">&#34;event_enqueue_success&#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 class="kc">nil</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">default</span><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">Inc</span><span class="p">(</span><span class="s">&#34;event_enqueue_dropped&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">logger</span><span class="p">.</span><span class="nf">Warn</span><span class="p">(</span><span class="s">&#34;drop event&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="s">&#34;reason&#34;</span><span class="p">,</span> <span class="s">&#34;queue_full&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="s">&#34;event_type&#34;</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">Type</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="s">&#34;subject_id&#34;</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">SubjectID</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="k">return</span> <span class="nx">ErrQueueFull</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>Log 適合保留單次事件脈絡，metric 適合觀察趨勢。若 drop rate 升高，代表服務正在降級；這應該能被監控看見。</p>
<h2 id="測試滿載行為要直接測">【測試】滿載行為要直接測</h2>
<p>Non-blocking send 的測試核心是先讓 channel 滿載，再確認函式立刻回錯。不要用 sleep 等待「可能會滿」。</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">TestTryEnqueueReturnsQueueFull</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">events</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="nx">Event</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">events</span> <span class="o">&lt;-</span> <span class="nx">Event</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;already_full&#34;</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">err</span> <span class="o">:=</span> <span class="nf">TryEnqueue</span><span class="p">(</span><span class="nx">events</span><span class="p">,</span> <span class="nx">Event</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;next&#34;</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="p">!</span><span class="nx">errors</span><span class="p">.</span><span class="nf">Is</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">ErrQueueFull</span><span class="p">)</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;error = %v, want ErrQueueFull&#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="p">}</span></span></span></code></pre></div><p>Blocking send with 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">TestEnqueueStopsWhenContextCanceled</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">events</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="nx">Event</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="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"> 4</span><span class="cl">    <span class="nf">cancel</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">err</span> <span class="o">:=</span> <span class="nf">Enqueue</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">events</span><span class="p">,</span> <span class="nx">Event</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;evt_1&#34;</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">errors</span><span class="p">.</span><span class="nf">Is</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Canceled</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">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;error = %v, want context canceled&#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="p">}</span></span></span></code></pre></div><p>這些測試把滿載和取消變成可重現條件，不需要依賴時間推測。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先處理單一 process 內的滿載處理策略；當訊息需要持久化、重試或跨 process 傳遞時，會在下列章節再往外延伸：</p>
<ul>
<li><a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Backend：訊息佇列與事件傳遞</a></li>
<li><a href="/blog/go-advanced/07-distributed-operations/outbox-idempotency/" data-link-title="7.2 Durable queue、outbox 與 idempotency" data-link-desc="設計跨 process 事件傳遞的可靠性與去重邊界">Go 進階：Durable queue、outbox 與 idempotency</a></li>
</ul>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 channel backpressure 、worker capacity 與事件丟棄策略；如果你要先回看語言教材，可以讀：</p>
<ul>
<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/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">Go：rate limiting 與 backpressure </a></li>
<li><a href="/blog/go-advanced/04-architecture-boundaries/event-fusion/" data-link-title="4.4 多來源 event 融合" data-link-desc="合併 HTTP、queue、timer 與外部事件來源">Go：多來源 event 融合</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>非阻塞送出是服務策略，不是語法技巧。Channel 滿載時，系統必須明確選擇等待、回錯、丟棄、覆蓋或轉交可靠儲存。選擇之前先定義事件的保留等級，選擇之後補上 log、metric 與測試，才能讓 backpressure 成為可管理的服務行為。</p>
]]></content:encoded></item><item><title>4.3 select：同時等待多種事件</title><link>https://tarrragon.github.io/blog/go/04-concurrency/select/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/04-concurrency/select/</guid><description>&lt;p>&lt;code>select&lt;/code> 是 Go 用來同時等待多個 channel 操作的控制結構。它的核心用途是讓一個 goroutine 同時處理資料輸入、取消訊號、timer/ticker 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback&lt;/a> 行為。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>理解 &lt;code>select&lt;/code> 的基本語法&lt;/li>
&lt;li>同時等待多個 channel&lt;/li>
&lt;li>用 &lt;code>ctx.Done()&lt;/code> 停止事件迴圈&lt;/li>
&lt;li>用 ticker 建立定時工作&lt;/li>
&lt;li>理解 &lt;code>default&lt;/code> 的 non-blocking 行為&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察select-同時等待多個-channel">【觀察】select 同時等待多個 channel&lt;/h2>
&lt;p>&lt;code>select&lt;/code> 的核心規則是：多個 case 中哪個 channel 先 ready，就執行哪個 case。以下範例同時等待 job 和取消訊號：&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">worker&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="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="nf">handle&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"> 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="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;/code>&lt;/pre>&lt;/div>&lt;p>這個 worker 不需要先固定等待 jobs，也不需要用輪詢檢查 context。&lt;code>select&lt;/code> 會同時等待兩者。&lt;/p>
&lt;h2 id="判讀select-loop-是長期-goroutine-的生命週期中心">【判讀】select loop 是長期 goroutine 的生命週期中心&lt;/h2>
&lt;p>長期 goroutine 的核心規則是：事件迴圈必須同時處理工作來源與退出訊號。只讀工作 channel 而不讀 &lt;code>ctx.Done()&lt;/code>，goroutine 可能無法按上層要求停止。&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">worker&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">handle&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>&lt;code>ok == false&lt;/code> 表示 channel 已關閉。這讓 worker 在「上層取消」和「工作來源結束」兩種情境都能退出。&lt;/p>
&lt;h2 id="策略ticker-case-要有-stop">【策略】ticker case 要有 Stop&lt;/h2>
&lt;p>ticker 的核心規則是：建立 ticker 後要呼叫 &lt;code>Stop()&lt;/code>，避免不再使用時仍保留 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">cleanupLoop&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">ticker&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewTicker&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Minute&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">ticker&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Stop&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="k">for&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">select&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">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"> 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="k">case&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">ticker&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">C&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="nf">cleanup&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>這種模式常用於定期清理、同步、掃描或報表輸出。&lt;/p>
&lt;h2 id="執行default-建立-non-blocking-select">【執行】default 建立 non-blocking select&lt;/h2>
&lt;p>&lt;code>default&lt;/code> 的核心規則是：沒有任何 channel ready 時，立即執行 default。這會讓 &lt;code>select&lt;/code> 不阻塞。&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="k">select&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">case&lt;/span> &lt;span class="nx">jobs&lt;/span> &lt;span class="o">&amp;lt;-&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">3&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">4&lt;/span>&lt;span class="cl">&lt;span class="k">default&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 class="nx">ErrQueueFull&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;/code>&lt;/pre>&lt;/div>&lt;p>這段程式嘗試把 job 放進 channel；如果 channel 不能立即接收，就回傳 &lt;code>ErrQueueFull&lt;/code>。這適合保護呼叫端不要被背景佇列卡住。&lt;/p>
&lt;p>不適合用 default 的情境是你其實需要等待結果：&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="k">select&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">case&lt;/span> &lt;span class="nx">result&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">results&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">return&lt;/span> &lt;span class="nx">result&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">default&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 class="nx">Result&lt;/span>&lt;span class="p">{}&lt;/span> &lt;span class="c1">// 可能過早返回&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;/code>&lt;/pre>&lt;/div>&lt;p>若結果是必要的，應該等待或設定 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a>，而不是直接 default。&lt;/p>
&lt;h2 id="timeout-pattern">timeout pattern&lt;/h2>
&lt;p>timeout 的核心規則是：需要等待但不能無限等待時，用 &lt;code>time.After&lt;/code> 或 context timeout。&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="k">select&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">case&lt;/span> &lt;span class="nx">result&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">results&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">return&lt;/span> &lt;span class="nx">result&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">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">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">After&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&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 class="nx">Result&lt;/span>&lt;span class="p">{},&lt;/span> &lt;span class="nx">errors&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">New&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;timeout&amp;#34;&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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在較大的系統中，通常更偏好 &lt;code>context.WithTimeout&lt;/code>，讓 timeout 可以沿呼叫鏈傳遞。&lt;/p></description><content:encoded><![CDATA[<p><code>select</code> 是 Go 用來同時等待多個 channel 操作的控制結構。它的核心用途是讓一個 goroutine 同時處理資料輸入、取消訊號、timer/ticker 與 <a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback</a> 行為。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>理解 <code>select</code> 的基本語法</li>
<li>同時等待多個 channel</li>
<li>用 <code>ctx.Done()</code> 停止事件迴圈</li>
<li>用 ticker 建立定時工作</li>
<li>理解 <code>default</code> 的 non-blocking 行為</li>
</ol>
<hr>
<h2 id="觀察select-同時等待多個-channel">【觀察】select 同時等待多個 channel</h2>
<p><code>select</code> 的核心規則是：多個 case 中哪個 channel 先 ready，就執行哪個 case。以下範例同時等待 job 和取消訊號：</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">worker</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="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="nf">handle</span><span class="p">(</span><span class="nx">job</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><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個 worker 不需要先固定等待 jobs，也不需要用輪詢檢查 context。<code>select</code> 會同時等待兩者。</p>
<h2 id="判讀select-loop-是長期-goroutine-的生命週期中心">【判讀】select loop 是長期 goroutine 的生命週期中心</h2>
<p>長期 goroutine 的核心規則是：事件迴圈必須同時處理工作來源與退出訊號。只讀工作 channel 而不讀 <code>ctx.Done()</code>，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">worker</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">handle</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><code>ok == false</code> 表示 channel 已關閉。這讓 worker 在「上層取消」和「工作來源結束」兩種情境都能退出。</p>
<h2 id="策略ticker-case-要有-stop">【策略】ticker case 要有 Stop</h2>
<p>ticker 的核心規則是：建立 ticker 後要呼叫 <code>Stop()</code>，避免不再使用時仍保留 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">cleanupLoop</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>這種模式常用於定期清理、同步、掃描或報表輸出。</p>
<h2 id="執行default-建立-non-blocking-select">【執行】default 建立 non-blocking select</h2>
<p><code>default</code> 的核心規則是：沒有任何 channel ready 時，立即執行 default。這會讓 <code>select</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="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">case</span> <span class="nx">jobs</span> <span class="o">&lt;-</span> <span class="nx">job</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="kc">nil</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span> <span class="nx">ErrQueueFull</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段程式嘗試把 job 放進 channel；如果 channel 不能立即接收，就回傳 <code>ErrQueueFull</code>。這適合保護呼叫端不要被背景佇列卡住。</p>
<p>不適合用 default 的情境是你其實需要等待結果：</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">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">case</span> <span class="nx">result</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">results</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">result</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span> <span class="nx">Result</span><span class="p">{}</span> <span class="c1">// 可能過早返回</span>
</span></span><span class="line"><span class="ln">6</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>，而不是直接 default。</p>
<h2 id="timeout-pattern">timeout pattern</h2>
<p>timeout 的核心規則是：需要等待但不能無限等待時，用 <code>time.After</code> 或 context timeout。</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">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">case</span> <span class="nx">result</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">results</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">result</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="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="mi">2</span> <span class="o">*</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">5</span><span class="cl">    <span class="k">return</span> <span class="nx">Result</span><span class="p">{},</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;timeout&#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></code></pre></div><p>在較大的系統中，通常更偏好 <code>context.WithTimeout</code>，讓 timeout 可以沿呼叫鏈傳遞。</p>
]]></content:encoded></item><item><title>5.3 race condition 檢查</title><link>https://tarrragon.github.io/blog/go-advanced/05-testing-reliability/race-check/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/05-testing-reliability/race-check/</guid><description>&lt;p>Race detector 的核心作用是找出測試執行期間發生的 data race。它能指出未同步讀寫同一份記憶體的位置，但不能取代 ownership、mutex、channel 與狀態邊界設計。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>分辨 data race 與一般邏輯競爭&lt;/li>
&lt;li>用 &lt;code>go test -race ./...&lt;/code> 檢查並發路徑&lt;/li>
&lt;li>寫出能觸發共享狀態讀寫的測試&lt;/li>
&lt;li>依 race report 找到讀寫來源&lt;/li>
&lt;li>選擇 mutex、channel owner 或 atomic 修正同步邊界&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察並發-bug-常常不會穩定重現">【觀察】並發 bug 常常不會穩定重現&lt;/h2>
&lt;p>Data race 的核心問題是測試可能偶爾通過、偶爾失敗，也可能完全不失敗但資料已經不安全。單次執行結果正確，不代表沒有未同步讀寫。&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">var&lt;/span> &lt;span class="nx">count&lt;/span> &lt;span class="kt">int&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">increment&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="nx">count&lt;/span>&lt;span class="o">++&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;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>count++&lt;/code> 不是原子操作。它包含讀取、加一、寫回。多個 goroutine 同時執行時，可能互相覆蓋結果，也可能被 race detector 偵測到未同步讀寫。&lt;/p>
&lt;h2 id="判讀data-race-是未同步的並發讀寫">【判讀】data race 是未同步的並發讀寫&lt;/h2>
&lt;p>Data race 的核心定義是至少兩個 goroutine 同時存取同一份記憶體，其中至少一個是寫入，而且沒有同步保護。&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">TestIncrementRace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">t&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">testing&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">T&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">var&lt;/span> &lt;span class="nx">wg&lt;/span> &lt;span class="nx">sync&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">WaitGroup&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="p">&amp;lt;&lt;/span> &lt;span class="mi">100&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="o">++&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="nx">wg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&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">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"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">wg&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"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nf">increment&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;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="nx">wg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Wait&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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>一般 &lt;code>go test&lt;/code> 不一定會失敗。&lt;code>go test -race&lt;/code> 會在 runtime 偵測這類未同步讀寫，並輸出讀取與寫入發生的位置。&lt;/p>
&lt;h2 id="執行用-go-test--race-跑到相關路徑">【執行】用 go test -race 跑到相關路徑&lt;/h2>
&lt;p>Race detector 的核心限制是只能檢查實際執行到的程式路徑。沒有被測試覆蓋的 goroutine、handler、repository 或 broadcast path，不會被它發現。&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 &lt;span class="nb">test&lt;/span> -race ./...&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個指令會用 race detector 跑所有 package 的測試。它會比一般測試慢，但對含有 goroutine、共享 map、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> hub、background worker 的服務非常重要。&lt;/p>
&lt;p>若專案很大，可以先針對相關 package：&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 &lt;span class="nb">test&lt;/span> -race ./internal/websocket ./internal/storage ./internal/worker&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>範圍縮小能讓日常執行更快，但合併前仍應跑完整路徑。&lt;/p>
&lt;h2 id="策略併發測試要讓共享狀態真的被同時讀寫">【策略】併發測試要讓共享狀態真的被同時讀寫&lt;/h2>
&lt;p>Race detector 的核心前提是測試要製造相關路徑。只建立 repository 卻不並發讀寫，race detector 沒有機會回報。&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">TestRepositoryConcurrentAccess&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">t&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">testing&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">T&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">repo&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">NewUserRepository&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">ctx&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Background&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="kd">var&lt;/span> &lt;span class="nx">wg&lt;/span> &lt;span class="nx">sync&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">WaitGroup&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">i&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="p">&amp;lt;&lt;/span> &lt;span class="mi">100&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="o">++&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">i&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">i&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">wg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&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">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">10&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">wg&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">11&lt;/span>&lt;span class="cl"> &lt;span class="nx">id&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Sprintf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;user_%d&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&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="nx">_&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">repo&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Save&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">User&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">ID&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">id&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="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">_&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">repo&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Find&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">id&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 class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="nx">wg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Wait&lt;/span>&lt;span class="p">()&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>這個測試的主要斷言在「讓 race detector 執行共享 map 的讀寫路徑」。若 repository 忘記加 lock，&lt;code>-race&lt;/code> 會指出問題。&lt;/p>
&lt;h2 id="執行websocket-hub-也需要-race-path">【執行】WebSocket hub 也需要 race path&lt;/h2>
&lt;p>WebSocket hub 的核心並發風險是 client 註冊、取消註冊、訂閱變更與 broadcast 可能同時發生。測試應讓這些路徑交錯執行。&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">TestHubConcurrentBroadcastAndUnregister&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">t&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">testing&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">T&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">hub&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">NewHub&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">clients&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">([]&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nx">Client&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">100&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="k">for&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="p">&amp;lt;&lt;/span> &lt;span class="mi">100&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="o">++&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="nx">client&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">NewTestClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Sprintf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;client_%d&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="mi">8&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">client&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Subscribe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;alerts&amp;#34;&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="nx">hub&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">clients&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">client&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kd">struct&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="nx">clients&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">clients&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">client&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="kd">var&lt;/span> &lt;span class="nx">wg&lt;/span> &lt;span class="nx">sync&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">WaitGroup&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">wg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&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>&lt;/span>&lt;span class="line">&lt;span class="ln">15&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">16&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">wg&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">17&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="p">&amp;lt;&lt;/span> &lt;span class="mi">100&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="o">++&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="nx">hub&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Broadcast&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;alerts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ServerMessage&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">Type&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s">&amp;#34;notification&amp;#34;&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="p">}()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&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">23&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">wg&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">24&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">client&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">clients&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl"> &lt;span class="nx">hub&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">unregisterClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">client&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl"> &lt;span class="p">}()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl"> &lt;span class="nx">wg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Wait&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個測試是否需要 lock，取決於 hub 的設計。如果 hub 保證所有操作都在單一 event loop 中執行，測試就應該透過 channel 操作，而不是直接呼叫未同步方法。測試要符合 ownership 設計，不應製造不被 API 允許的並發。&lt;/p></description><content:encoded><![CDATA[<p>Race detector 的核心作用是找出測試執行期間發生的 data race。它能指出未同步讀寫同一份記憶體的位置，但不能取代 ownership、mutex、channel 與狀態邊界設計。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>分辨 data race 與一般邏輯競爭</li>
<li>用 <code>go test -race ./...</code> 檢查並發路徑</li>
<li>寫出能觸發共享狀態讀寫的測試</li>
<li>依 race report 找到讀寫來源</li>
<li>選擇 mutex、channel owner 或 atomic 修正同步邊界</li>
</ol>
<hr>
<h2 id="觀察並發-bug-常常不會穩定重現">【觀察】並發 bug 常常不會穩定重現</h2>
<p>Data race 的核心問題是測試可能偶爾通過、偶爾失敗，也可能完全不失敗但資料已經不安全。單次執行結果正確，不代表沒有未同步讀寫。</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">var</span> <span class="nx">count</span> <span class="kt">int</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">increment</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">count</span><span class="o">++</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>count++</code> 不是原子操作。它包含讀取、加一、寫回。多個 goroutine 同時執行時，可能互相覆蓋結果，也可能被 race detector 偵測到未同步讀寫。</p>
<h2 id="判讀data-race-是未同步的並發讀寫">【判讀】data race 是未同步的並發讀寫</h2>
<p>Data race 的核心定義是至少兩個 goroutine 同時存取同一份記憶體，其中至少一個是寫入，而且沒有同步保護。</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">TestIncrementRace</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="kd">var</span> <span class="nx">wg</span> <span class="nx">sync</span><span class="p">.</span><span class="nx">WaitGroup</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="nx">i</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="p">&lt;</span> <span class="mi">100</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="nx">wg</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="mi">1</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="nx">wg</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="nf">increment</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="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="nx">wg</span><span class="p">.</span><span class="nf">Wait</span><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>go test</code> 不一定會失敗。<code>go test -race</code> 會在 runtime 偵測這類未同步讀寫，並輸出讀取與寫入發生的位置。</p>
<h2 id="執行用-go-test--race-跑到相關路徑">【執行】用 go test -race 跑到相關路徑</h2>
<p>Race detector 的核心限制是只能檢查實際執行到的程式路徑。沒有被測試覆蓋的 goroutine、handler、repository 或 broadcast path，不會被它發現。</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 <span class="nb">test</span> -race ./...</span></span></code></pre></div><p>這個指令會用 race detector 跑所有 package 的測試。它會比一般測試慢，但對含有 goroutine、共享 map、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> hub、background worker 的服務非常重要。</p>
<p>若專案很大，可以先針對相關 package：</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 <span class="nb">test</span> -race ./internal/websocket ./internal/storage ./internal/worker</span></span></code></pre></div><p>範圍縮小能讓日常執行更快，但合併前仍應跑完整路徑。</p>
<h2 id="策略併發測試要讓共享狀態真的被同時讀寫">【策略】併發測試要讓共享狀態真的被同時讀寫</h2>
<p>Race detector 的核心前提是測試要製造相關路徑。只建立 repository 卻不並發讀寫，race detector 沒有機會回報。</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">TestRepositoryConcurrentAccess</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></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="kd">var</span> <span class="nx">wg</span> <span class="nx">sync</span><span class="p">.</span><span class="nx">WaitGroup</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">for</span> <span class="nx">i</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="p">&lt;</span> <span class="mi">100</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">i</span> <span class="o">:=</span> <span class="nx">i</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">wg</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</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">10</span><span class="cl">            <span class="k">defer</span> <span class="nx">wg</span><span class="p">.</span><span class="nf">Done</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="nx">id</span> <span class="o">:=</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;user_%d&#34;</span><span class="p">,</span> <span class="nx">i</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</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="nx">id</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="nx">_</span><span class="p">,</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">_</span> <span class="p">=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">Find</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">id</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 class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="nx">wg</span><span class="p">.</span><span class="nf">Wait</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個測試的主要斷言在「讓 race detector 執行共享 map 的讀寫路徑」。若 repository 忘記加 lock，<code>-race</code> 會指出問題。</p>
<h2 id="執行websocket-hub-也需要-race-path">【執行】WebSocket hub 也需要 race path</h2>
<p>WebSocket hub 的核心並發風險是 client 註冊、取消註冊、訂閱變更與 broadcast 可能同時發生。測試應讓這些路徑交錯執行。</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">TestHubConcurrentBroadcastAndUnregister</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">hub</span> <span class="o">:=</span> <span class="nf">NewHub</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">clients</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="o">*</span><span class="nx">Client</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">100</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="nx">i</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="p">&lt;</span> <span class="mi">100</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="nx">client</span> <span class="o">:=</span> <span class="nf">NewTestClient</span><span class="p">(</span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;client_%d&#34;</span><span class="p">,</span> <span class="nx">i</span><span class="p">),</span> <span class="mi">8</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">Subscribe</span><span class="p">(</span><span class="s">&#34;alerts&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">hub</span><span class="p">.</span><span class="nx">clients</span><span class="p">[</span><span class="nx">client</span><span class="p">]</span> <span class="p">=</span> <span class="kd">struct</span><span class="p">{}{}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nx">clients</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">clients</span><span class="p">,</span> <span class="nx">client</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="kd">var</span> <span class="nx">wg</span> <span class="nx">sync</span><span class="p">.</span><span class="nx">WaitGroup</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nx">wg</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</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">16</span><span class="cl">        <span class="k">defer</span> <span class="nx">wg</span><span class="p">.</span><span class="nf">Done</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="k">for</span> <span class="nx">i</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="p">&lt;</span> <span class="mi">100</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">            <span class="nx">hub</span><span class="p">.</span><span class="nf">Broadcast</span><span class="p">(</span><span class="s">&#34;alerts&#34;</span><span class="p">,</span> <span class="nx">ServerMessage</span><span class="p">{</span><span class="nx">Type</span><span class="p">:</span> <span class="s">&#34;notification&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="p">}</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></span><span class="line"><span class="ln">22</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">23</span><span class="cl">        <span class="k">defer</span> <span class="nx">wg</span><span class="p">.</span><span class="nf">Done</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">24</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">25</span><span class="cl">            <span class="nx">hub</span><span class="p">.</span><span class="nf">unregisterClient</span><span class="p">(</span><span class="nx">client</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">    <span class="p">}()</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">
</span></span><span class="line"><span class="ln">29</span><span class="cl">    <span class="nx">wg</span><span class="p">.</span><span class="nf">Wait</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個測試是否需要 lock，取決於 hub 的設計。如果 hub 保證所有操作都在單一 event loop 中執行，測試就應該透過 channel 操作，而不是直接呼叫未同步方法。測試要符合 ownership 設計，不應製造不被 API 允許的並發。</p>
<h2 id="判讀race-report-要看讀寫兩端">【判讀】race report 要看讀寫兩端</h2>
<p>Race report 的核心資訊是兩個位置：一端讀或寫，另一端寫。修正時不要只看最後一行，要找出是哪個共享資料缺少同步。</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">WARNING: DATA RACE
</span></span><span class="line"><span class="ln">2</span><span class="cl">Read at 0x...
</span></span><span class="line"><span class="ln">3</span><span class="cl">  example.com/app.(*UserRepository).Find()
</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">Previous write at 0x...
</span></span><span class="line"><span class="ln">6</span><span class="cl">  example.com/app.(*UserRepository).Save()</span></span></code></pre></div><p>這表示 <code>Find</code> 和 <code>Save</code> 同時碰到同一份資料，且缺少同步。修正方向是在 repository owner 補上 mutex、channel ownership 或其他同步邊界。</p>
<h2 id="策略修正方式要對應狀態形狀">【策略】修正方式要對應狀態形狀</h2>
<p>修正 data race 的核心選擇是建立正確同步邊界。常見方法有 mutex、channel owner、atomic。</p>
<table>
  <thead>
      <tr>
          <th>方法</th>
          <th>適用情境</th>
          <th>注意事項</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>mutex</td>
          <td>多方法讀寫同一份 map/slice/state</td>
          <td>lock 要保護完整不變式</td>
      </tr>
      <tr>
          <td>channel owner</td>
          <td>狀態修改可集中成事件 loop</td>
          <td>要設計 reply、shutdown、<a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a></td>
      </tr>
      <tr>
          <td>atomic</td>
          <td>單一數值 counter 或 flag</td>
          <td>不適合複雜狀態</td>
      </tr>
  </tbody>
</table>
<p>Mutex 範例：</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">Counter</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">mu</span>    <span class="nx">sync</span><span class="p">.</span><span class="nx">Mutex</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">value</span> <span class="kt">int</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="kd">func</span> <span class="p">(</span><span class="nx">c</span> <span class="o">*</span><span class="nx">Counter</span><span class="p">)</span> <span class="nf">Inc</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">c</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Lock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">defer</span> <span class="nx">c</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">c</span><span class="p">.</span><span class="nx">value</span><span class="o">++</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="kd">func</span> <span class="p">(</span><span class="nx">c</span> <span class="o">*</span><span class="nx">Counter</span><span class="p">)</span> <span class="nf">Value</span><span class="p">()</span> <span class="kt">int</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nx">c</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Lock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">defer</span> <span class="nx">c</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">return</span> <span class="nx">c</span><span class="p">.</span><span class="nx">value</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>鎖應該屬於擁有狀態的型別，並保護一個清楚的不變條件。只為了讓 race detector 安靜而到處加鎖，會讓 ownership 分散，後續仍然難以判斷資料一致性。</p>
<h2 id="判讀race-free-不代表行為正確">【判讀】race-free 不代表行為正確</h2>
<p>Race detector 的核心邊界是它只找 data race，不保證並發邏輯正確。沒有 data race 的程式仍可能 deadlock、漏訊息、順序錯誤、重複 close 或違反資料語意。</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">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">case</span> <span class="nx">client</span><span class="p">.</span><span class="nx">send</span> <span class="o">&lt;-</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">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="c1">// drop</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段程式可能沒有 data race，但「<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> full 時丟訊息」是否正確是服務語意問題。Race detector 不會告訴你該丟、該斷線、還是該寫可靠 queue。</p>
<p>因此並發測試要分成兩層：</p>
<ul>
<li>用 <code>go test -race</code> 找未同步記憶體存取。</li>
<li>用行為測試檢查 channel close、queue full、context cancel、cleanup、<a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a>。</li>
</ul>
<h2 id="測試把-race-check-納入固定流程">【測試】把 race check 納入固定流程</h2>
<p>Race check 的核心價值來自重複執行。只在出事後手動跑，效果有限。</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 <span class="nb">test</span> ./...
</span></span><span class="line"><span class="ln">2</span><span class="cl">go <span class="nb">test</span> -race ./...</span></span></code></pre></div><p>日常開發可以先跑相關 package，提交前或 CI 跑完整 race suite。若 race suite 太慢，至少讓含有 hub、repository、worker、client state 的 package 固定跑 <code>-race</code>。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先處理共享 state、channel ownership 與 goroutine lifecycle 的 race 風險；lock-free 與完整 memory model，會在下列章節再往外延伸：</p>
<ul>
<li><a href="/blog/go-advanced/01-concurrency-patterns/shared-state/" data-link-title="1.4 共享狀態與複製邊界" data-link-desc="用 lock 與 copy 保護長期服務的狀態資料">Go 進階：共享狀態與複製邊界</a></li>
<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/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>這一章承接的是共享狀態、channel ownership 與 lifecycle；如果你要先回看語言教材，可以讀：</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/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><code>go test -race</code> 是 Go 並發服務的基本安全網，但它只檢查測試執行到的 data race。你仍然需要設計清楚的 state owner、lock boundary、channel ownership 與行為測試。Race-free 不是正確性的全部；它只是可靠性的第一層檢查。</p>
]]></content:encoded></item><item><title>1.4 共享狀態與複製邊界</title><link>https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/shared-state/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/shared-state/</guid><description>&lt;p>共享狀態的核心規則是同一份可變資料若會被多個 goroutine 存取，就必須有明確 owner 與保護邊界。Map 需要同步，slice 回傳前通常要 copy，可變指標不能隨意暴露，修改行為應集中在擁有狀態的型別內。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>判斷哪個型別擁有共享狀態&lt;/li>
&lt;li>用 &lt;code>sync.RWMutex&lt;/code> 保護 map 與 slice&lt;/li>
&lt;li>避免回傳內部 map、slice、pointer&lt;/li>
&lt;li>分辨 shallow copy 與 deep copy 的邊界&lt;/li>
&lt;li>用測試與 race detector 驗證共享狀態安全性&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察共享狀態風險通常延遲出現">【觀察】共享狀態風險通常延遲出現&lt;/h2>
&lt;p>共享狀態的核心風險是錯誤可能只在特定併發時序下出現。單元測試可能通過，本地手動操作也正常，但高流量下會出現 data race、map panic、狀態被外部修改或歷史資料被覆蓋。&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">type&lt;/span> &lt;span class="nx">Store&lt;/span> &lt;span class="kd">struct&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">users&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&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="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">s&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Store&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Save&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">user&lt;/span> &lt;span class="nx">User&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"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">s&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="nx">ID&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">user&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;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">s&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Store&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Users&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&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">10&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&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;/code>&lt;/pre>&lt;/div>&lt;p>這個型別有兩個問題：map 沒有同步保護，且 &lt;code>Users&lt;/code> 直接暴露內部 map。呼叫端拿到 map 後可以繞過 &lt;code>Store&lt;/code> 修改資料。&lt;/p>
&lt;h2 id="判讀mutex-保護的是狀態不變式">【判讀】mutex 保護的是狀態不變式&lt;/h2>
&lt;p>Mutex 的核心責任不是讓程式「不會同時跑」，而是保護某一組資料的不變式。只要讀寫同一份可變資料，就應該由同一個 owner 控制 lock。&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">type&lt;/span> &lt;span class="nx">UserRepository&lt;/span> &lt;span class="kd">struct&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">mu&lt;/span> &lt;span class="nx">sync&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">RWMutex&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">users&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&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="kd">func&lt;/span> &lt;span class="nf">NewUserRepository&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">UserRepository&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">return&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">UserRepository&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="nx">users&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&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"> 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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>UserRepository&lt;/code> 是 &lt;code>users&lt;/code> map 的 owner。外部程式不應持有 &lt;code>users&lt;/code> 的 reference，也不應知道它用 map、資料庫或其他結構保存。&lt;/p>
&lt;h2 id="執行所有讀寫都經過-owner-method">【執行】所有讀寫都經過 owner method&lt;/h2>
&lt;p>共享 map 的核心規則是所有讀寫都經過同一組方法。寫入使用 &lt;code>Lock&lt;/code>，讀取使用 &lt;code>RLock&lt;/code>。&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">Save&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">user&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>&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">Lock&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">Unlock&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">r&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="nx">ID&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">user&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="kc">nil&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;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&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">Find&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">id&lt;/span> &lt;span class="kt">string&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">bool&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">10&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">11&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">12&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">user&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ok&lt;/span> &lt;span class="o">:=&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 class="nx">id&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="k">return&lt;/span> &lt;span class="nx">user&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ok&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">15&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>context.Context&lt;/code> 在 memory repository 裡可能用不到，但保留在 method signature 可以讓未來改成資料庫或遠端儲存時支援取消。這是 repository port 常見的演進邊界。&lt;/p>
&lt;h2 id="判讀回傳內部-map-會破壞-lock">【判讀】回傳內部 map 會破壞 lock&lt;/h2>
&lt;p>回傳 map 的核心風險是鎖只保護到方法結束。方法回傳後，呼叫端拿到的仍然是同一份 map，任何修改都會繞過 owner。&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="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">UnsafeUsers&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="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&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">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="k">return&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&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;/code>&lt;/pre>&lt;/div>&lt;p>這段程式看起來有加鎖，但鎖釋放後外部仍能修改內部 map：&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="nx">users&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">repo&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">UnsafeUsers&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&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="nb">delete&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">users&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;user_1&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>安全做法是回傳 copy：&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">Users&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="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&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">result&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&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">id&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">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">id&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">user&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">result&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>呼叫端可以自由修改 &lt;code>result&lt;/code>，不會影響 repository 內部狀態。&lt;/p>
&lt;h2 id="判讀slice-copy-保護的是底層-array">【判讀】slice copy 保護的是底層 array&lt;/h2>
&lt;p>Slice 的核心風險是 slice header 會被複製，但底層 array 可能共享。直接回傳 slice 會讓呼叫端修改 owner 的內部資料。&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">type&lt;/span> &lt;span class="nx">RecentEvents&lt;/span> &lt;span class="kd">struct&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">mu&lt;/span> &lt;span class="nx">sync&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">RWMutex&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">events&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="nx">Event&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="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">RecentEvents&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Append&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">event&lt;/span> &lt;span class="nx">Event&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"> 7&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">Lock&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">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">Unlock&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>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">events&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">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">events&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&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;/code>&lt;/pre>&lt;/div>&lt;p>安全的 list method：&lt;/p></description><content:encoded><![CDATA[<p>共享狀態的核心規則是同一份可變資料若會被多個 goroutine 存取，就必須有明確 owner 與保護邊界。Map 需要同步，slice 回傳前通常要 copy，可變指標不能隨意暴露，修改行為應集中在擁有狀態的型別內。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>判斷哪個型別擁有共享狀態</li>
<li>用 <code>sync.RWMutex</code> 保護 map 與 slice</li>
<li>避免回傳內部 map、slice、pointer</li>
<li>分辨 shallow copy 與 deep copy 的邊界</li>
<li>用測試與 race detector 驗證共享狀態安全性</li>
</ol>
<hr>
<h2 id="觀察共享狀態風險通常延遲出現">【觀察】共享狀態風險通常延遲出現</h2>
<p>共享狀態的核心風險是錯誤可能只在特定併發時序下出現。單元測試可能通過，本地手動操作也正常，但高流量下會出現 data race、map panic、狀態被外部修改或歷史資料被覆蓋。</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">Store</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">users</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><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="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">Store</span><span class="p">)</span> <span class="nf">Save</span><span class="p">(</span><span class="nx">user</span> <span class="nx">User</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">s</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="nx">ID</span><span class="p">]</span> <span class="p">=</span> <span class="nx">user</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><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="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">Store</span><span class="p">)</span> <span class="nf">Users</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="nx">User</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="nx">s</span><span class="p">.</span><span class="nx">users</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個型別有兩個問題：map 沒有同步保護，且 <code>Users</code> 直接暴露內部 map。呼叫端拿到 map 後可以繞過 <code>Store</code> 修改資料。</p>
<h2 id="判讀mutex-保護的是狀態不變式">【判讀】mutex 保護的是狀態不變式</h2>
<p>Mutex 的核心責任不是讓程式「不會同時跑」，而是保護某一組資料的不變式。只要讀寫同一份可變資料，就應該由同一個 owner 控制 lock。</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">UserRepository</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">mu</span>    <span class="nx">sync</span><span class="p">.</span><span class="nx">RWMutex</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">users</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</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="kd">func</span> <span class="nf">NewUserRepository</span><span class="p">()</span> <span class="o">*</span><span class="nx">UserRepository</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="o">&amp;</span><span class="nx">UserRepository</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">users</span><span class="p">:</span> <span class="nb">make</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="nx">User</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="p">}</span></span></span></code></pre></div><p><code>UserRepository</code> 是 <code>users</code> map 的 owner。外部程式不應持有 <code>users</code> 的 reference，也不應知道它用 map、資料庫或其他結構保存。</p>
<h2 id="執行所有讀寫都經過-owner-method">【執行】所有讀寫都經過 owner method</h2>
<p>共享 map 的核心規則是所有讀寫都經過同一組方法。寫入使用 <code>Lock</code>，讀取使用 <code>RLock</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="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">Save</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">user</span> <span class="nx">User</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">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Lock</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">Unlock</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">r</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="nx">ID</span><span class="p">]</span> <span class="p">=</span> <span class="nx">user</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><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="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">Find</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">id</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="nx">User</span><span class="p">,</span> <span class="kt">bool</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">10</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">11</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">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nx">user</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">return</span> <span class="nx">user</span><span class="p">,</span> <span class="nx">ok</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>context.Context</code> 在 memory repository 裡可能用不到，但保留在 method signature 可以讓未來改成資料庫或遠端儲存時支援取消。這是 repository port 常見的演進邊界。</p>
<h2 id="判讀回傳內部-map-會破壞-lock">【判讀】回傳內部 map 會破壞 lock</h2>
<p>回傳 map 的核心風險是鎖只保護到方法結束。方法回傳後，呼叫端拿到的仍然是同一份 map，任何修改都會繞過 owner。</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="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">UnsafeUsers</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="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</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="k">return</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段程式看起來有加鎖，但鎖釋放後外部仍能修改內部 map：</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">users</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">UnsafeUsers</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">delete</span><span class="p">(</span><span class="nx">users</span><span class="p">,</span> <span class="s">&#34;user_1&#34;</span><span class="p">)</span></span></span></code></pre></div><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="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">Users</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="kd">map</span><span class="p">[</span><span class="kt">string</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">result</span> <span class="o">:=</span> <span class="nb">make</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="nx">User</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">id</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">result</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span> <span class="p">=</span> <span class="nx">user</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">result</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>呼叫端可以自由修改 <code>result</code>，不會影響 repository 內部狀態。</p>
<h2 id="判讀slice-copy-保護的是底層-array">【判讀】slice copy 保護的是底層 array</h2>
<p>Slice 的核心風險是 slice header 會被複製，但底層 array 可能共享。直接回傳 slice 會讓呼叫端修改 owner 的內部資料。</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">RecentEvents</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">mu</span>     <span class="nx">sync</span><span class="p">.</span><span class="nx">RWMutex</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">events</span> <span class="p">[]</span><span class="nx">Event</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="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">RecentEvents</span><span class="p">)</span> <span class="nf">Append</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">event</span> <span class="nx">Event</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</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">Lock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 8</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">Unlock</span><span class="p">()</span>
</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">    <span class="nx">r</span><span class="p">.</span><span class="nx">events</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">events</span><span class="p">,</span> <span class="nx">event</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>安全的 list method：</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">RecentEvents</span><span class="p">)</span> <span class="nf">List</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">Event</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">result</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="nx">Event</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">events</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nb">copy</span><span class="p">(</span><span class="nx">result</span><span class="p">,</span> <span class="nx">r</span><span class="p">.</span><span class="nx">events</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="nx">result</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>copy</code> 建立新的底層 array。呼叫端對 <code>result</code> 排序、截斷、append 或修改元素，不會改到 <code>r.events</code>。</p>
<h2 id="策略值型別可以-shallow-copy可變欄位需要-deep-copy">【策略】值型別可以 shallow copy，可變欄位需要 deep copy</h2>
<p>Copy boundary 的核心判斷是資料裡是否還包含可變 reference。若 struct 只有 string、int、time.Time 這類值型別，shallow copy 通常足夠；若 struct 包含 map、slice 或 pointer，就要考慮 deep copy。</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">Event</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">ID</span>        <span class="kt">string</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">Type</span>      <span class="kt">string</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">CreatedAt</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</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>Event</code> 放在 slice 裡，用 <code>copy</code> 複製 slice 通常足夠。</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">Event</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">ID</span>       <span class="kt">string</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">Type</span>     <span class="kt">string</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">Metadata</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這時只 copy slice 不夠，因為每個 <code>Event.Metadata</code> 仍然指向同一份 map。需要 clone：</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">CloneEvent</span><span class="p">(</span><span class="nx">event</span> <span class="nx">Event</span><span class="p">)</span> <span class="nx">Event</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">cloned</span> <span class="o">:=</span> <span class="nx">event</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">if</span> <span class="nx">event</span><span class="p">.</span><span class="nx">Metadata</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">cloned</span><span class="p">.</span><span class="nx">Metadata</span> <span class="p">=</span> <span class="nb">make</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">string</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">Metadata</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="k">for</span> <span class="nx">key</span><span class="p">,</span> <span class="nx">value</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">event</span><span class="p">.</span><span class="nx">Metadata</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            <span class="nx">cloned</span><span class="p">.</span><span class="nx">Metadata</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="p">=</span> <span class="nx">value</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <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">cloned</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>是否需要 deep copy 取決於 API 承諾。如果呼叫端不應修改 repository 內部資料，就要複製所有可變 reference。</p>
<h2 id="判讀回傳-pointer-要代表明確修改權">【判讀】回傳 pointer 要代表明確修改權</h2>
<p>Pointer 回傳的核心語意是呼叫端取得同一份資料的參照。若資料屬於共享狀態，回傳 pointer 通常會破壞 owner 邊界。</p>
<p>容易誤解的 API：</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">FindPointer</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">id</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">User</span><span class="p">,</span> <span class="kt">bool</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">user</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">id</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="p">!</span><span class="nx">ok</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="kc">nil</span><span class="p">,</span> <span class="kc">false</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="o">&amp;</span><span class="nx">user</span><span class="p">,</span> <span class="kc">true</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段程式回傳的是區域變數 <code>user</code> 的指標，不是 map 內部資料的可修改入口。呼叫端修改這個 pointer，不會保存回 repository。API 看起來像能修改，實際不能，語意不清楚。</p>
<p>更清楚的做法是回傳 value，並提供明確 update method：</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">UpdateEmail</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">id</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">email</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kt">bool</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">Lock</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">Unlock</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">user</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">id</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="p">!</span><span class="nx">ok</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="kc">false</span><span class="p">,</span> <span class="kc">nil</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></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">user</span><span class="p">.</span><span class="nx">Email</span> <span class="p">=</span> <span class="nx">email</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span> <span class="p">=</span> <span class="nx">user</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">return</span> <span class="kc">true</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>修改行為集中在 repository 內，lock、驗證與狀態一致性也留在同一個地方。</p>
<h2 id="策略mutex-和-channel-owner-要按資料形狀選擇">【策略】mutex 和 channel owner 要按資料形狀選擇</h2>
<p>狀態保護的核心選擇是 mutex owner 或 goroutine owner。兩者都符合 Go 的精神，差異在資料存取模式。</p>
<table>
  <thead>
      <tr>
          <th>方法</th>
          <th>適用情境</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>mutex owner</td>
          <td>多個方法需要同步讀寫狀態</td>
          <td>要維護 lock 與 copy boundary</td>
      </tr>
      <tr>
          <td>goroutine owner</td>
          <td>所有修改都能表示成訊息</td>
          <td>要設計 command、reply、shutdown</td>
      </tr>
  </tbody>
</table>
<p>Mutex 版本：</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">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></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">user</span><span class="p">,</span> <span class="nx">ok</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">Find</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">id</span><span class="p">)</span></span></span></code></pre></div><p>Goroutine owner 版本：</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">command</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">kind</span>  <span class="kt">string</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">user</span>  <span class="nx">User</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">reply</span> <span class="kd">chan</span> <span class="nx">result</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>不要為了避免 mutex 而把簡單狀態硬改成複雜訊息系統。也不要在需要嚴格順序與單一事件流時到處加 lock。選擇應該來自資料形狀與讀寫模式。</p>
<h2 id="測試copy-boundary-要用外部修改驗證">【測試】copy boundary 要用外部修改驗證</h2>
<p>Copy boundary 的測試核心是呼叫 getter 後修改回傳值，再確認 owner 內部資料沒有被改動。</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">TestUsersReturnsCopy</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></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</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 class="nx">Email</span><span class="p">:</span> <span class="s">&#34;a@example.com&#34;</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"> 6</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;save user: %v&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <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="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">Users</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</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">11</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;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">12</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nb">delete</span><span class="p">(</span><span class="nx">users</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">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="nx">_</span><span class="p">,</span> <span class="nx">ok</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">Find</span><span class="p">(</span><span class="nx">ctx</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">16</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">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;find user: %v&#34;</span><span class="p">,</span> <span class="nx">err</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="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</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 should not be modified through returned map&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Slice 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">TestEventListReturnsCopy</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">events</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">RecentEvents</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></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">events</span><span class="p">.</span><span class="nf">Append</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">Event</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;evt_1&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">got</span> <span class="o">:=</span> <span class="nx">events</span><span class="p">.</span><span class="nf">List</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="nx">got</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"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">again</span> <span class="o">:=</span> <span class="nx">events</span><span class="p">.</span><span class="nf">List</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</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;evt_1&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</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;internal event was modified through returned slice&#34;</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>這類測試能直接防止未來有人為了「省 copy」而破壞狀態邊界。</p>
<h2 id="測試race-detector-驗證同步邊界">【測試】race detector 驗證同步邊界</h2>
<p>Race detector 的核心用途是找出未同步的共享記憶體存取。對含有 goroutine、map、slice、repository 的測試，應定期執行：</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 <span class="nb">test</span> -race ./...</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">TestRepositoryConcurrentAccess</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></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="kd">var</span> <span class="nx">wg</span> <span class="nx">sync</span><span class="p">.</span><span class="nx">WaitGroup</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">for</span> <span class="nx">i</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="p">&lt;</span> <span class="mi">100</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">i</span> <span class="o">:=</span> <span class="nx">i</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">wg</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</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">10</span><span class="cl">            <span class="k">defer</span> <span class="nx">wg</span><span class="p">.</span><span class="nf">Done</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="nx">id</span> <span class="o">:=</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;user_%d&#34;</span><span class="p">,</span> <span class="nx">i</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</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="nx">id</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="nx">_</span><span class="p">,</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">_</span> <span class="p">=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">Find</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">id</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 class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="nx">wg</span><span class="p">.</span><span class="nf">Wait</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個測試本身不一定能證明沒有所有問題，但搭配 <code>-race</code> 可以檢查 repository 方法是否真的包住共享 map。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先聚焦單一 Go process 內的共享狀態保護；更外層的資料庫交易、快取一致性與資料複製邊界，會在下列章節再往外延伸：</p>
<ul>
<li><a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">Backend：資料庫與持久化</a></li>
<li><a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">Backend：快取與 Redis</a></li>
<li><a href="/blog/go/02-types-data/pointers-copy/" data-link-title="2.5 指標與資料複製邊界" data-link-desc="理解指標、slice 與共享狀態的防護策略">Go 入門：指標與資料複製邊界</a></li>
</ul>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 repository、copy boundary 與 state owner；如果你要先回看語言教材，可以讀：</p>
<ul>
<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/state-fields/" data-link-title="6.3 如何擴展狀態投影欄位" data-link-desc="更新狀態模型、repository 與 API 輸出">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>
<li><a href="/blog/go/07-refactoring/interface-boundary/" data-link-title="7.2 用 interface 隔離外部依賴" data-link-desc="建立小而穩定的測試替身">Go：用 interface 隔離外部依賴</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>共享狀態的安全邊界由 owner、lock、copy 與明確修改方法組成。Map/slice 讀寫要經過同一個 owner；getter 不應暴露內部可變資料；含 map、slice、pointer 的 struct 要考慮 deep copy；修改行為應集中在方法內。這些規則能讓長時間運行的 Go 服務避開 data race、外部突變與難以重現的狀態錯誤。</p>
]]></content:encoded></item><item><title>4.4 sync.RWMutex：保護共享狀態</title><link>https://tarrragon.github.io/blog/go/04-concurrency/rwmutex/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/04-concurrency/rwmutex/</guid><description>&lt;p>&lt;code>sync.RWMutex&lt;/code> 是 Go 用來保護共享狀態的讀寫鎖。它的核心用途是允許多個讀取者同時讀取，但寫入者必須獨占資料，避免 goroutine 同時讀寫 map、slice 或 struct 時產生資料競爭。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>理解 data race 的風險&lt;/li>
&lt;li>區分 &lt;code>Mutex&lt;/code> 與 &lt;code>RWMutex&lt;/code>&lt;/li>
&lt;li>用 &lt;code>RLock&lt;/code> / &lt;code>RUnlock&lt;/code> 保護讀取&lt;/li>
&lt;li>用 &lt;code>Lock&lt;/code> / &lt;code>Unlock&lt;/code> 保護寫入&lt;/li>
&lt;li>避免回傳內部 map 或 slice 破壞鎖邊界&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察共享-map-不能被多個-goroutine-無保護地讀寫">【觀察】共享 map 不能被多個 goroutine 無保護地讀寫&lt;/h2>
&lt;p>共享狀態的核心規則是：只要多個 goroutine 可能同時讀寫同一份資料，就必須用同步機制保護。以下程式同時讀寫 map，存在 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">type&lt;/span> &lt;span class="nx">UserRepository&lt;/span> &lt;span class="kd">struct&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">users&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&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="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">Set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">id&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span> &lt;span class="nx">User&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"> 6&lt;/span>&lt;span class="cl"> &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 class="nx">id&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">user&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;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&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">Get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">id&lt;/span> &lt;span class="kt">string&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">bool&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">10&lt;/span>&lt;span class="cl"> &lt;span class="nx">user&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ok&lt;/span> &lt;span class="o">:=&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 class="nx">id&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="k">return&lt;/span> &lt;span class="nx">user&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ok&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;/code>&lt;/pre>&lt;/div>&lt;p>如果 &lt;code>Set&lt;/code> 和 &lt;code>Get&lt;/code> 從不同 goroutine 同時執行，map 可能被同時讀寫。Go 的 map 不保證這種情境安全。&lt;/p>
&lt;h2 id="判讀rwmutex-區分讀取與寫入">【判讀】RWMutex 區分讀取與寫入&lt;/h2>
&lt;p>&lt;code>RWMutex&lt;/code> 的核心規則是：讀取使用 &lt;code>RLock&lt;/code>，寫入使用 &lt;code>Lock&lt;/code>；多個讀取可並行，寫入會排他。&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">type&lt;/span> &lt;span class="nx">UserRepository&lt;/span> &lt;span class="kd">struct&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">users&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&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">mu&lt;/span> &lt;span class="nx">sync&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">RWMutex&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="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">Set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">id&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span> &lt;span class="nx">User&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"> 7&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">Lock&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">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">Unlock&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="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 class="nx">id&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">user&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="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">Get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">id&lt;/span> &lt;span class="kt">string&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">bool&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">13&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">14&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">15&lt;/span>&lt;span class="cl"> &lt;span class="nx">user&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ok&lt;/span> &lt;span class="o">:=&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 class="nx">id&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">user&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ok&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&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>Set&lt;/code> 修改 map，所以用 &lt;code>Lock&lt;/code>。&lt;code>Get&lt;/code> 只讀 map，所以用 &lt;code>RLock&lt;/code>。&lt;/p>
&lt;h2 id="策略鎖保護的是資料不變式">【策略】鎖保護的是資料不變式&lt;/h2>
&lt;p>鎖範圍的核心規則是：鎖要包住所有需要一致觀察或一致修改的資料。鎖的邊界應涵蓋完整不變式，慢速 I/O、網路呼叫與和共享資料無關的計算則應放在鎖外。&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="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">Add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">user&lt;/span> &lt;span class="nx">User&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">Lock&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">Unlock&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">r&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="nx">ID&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">user&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">count&lt;/span>&lt;span class="o">++&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;code>users&lt;/code> 和 &lt;code>count&lt;/code> 分開鎖，讀者可能看到 map 已更新但 count 還沒更新的中間狀態。&lt;/p>
&lt;h2 id="執行回傳資料時要保留-copy-boundary">【執行】回傳資料時要保留 copy boundary&lt;/h2>
&lt;p>鎖邊界的核心規則是：鎖只能保護鎖內操作；回傳內部 map 會讓呼叫者在鎖外修改資料，破壞 repository 對狀態的控制權。&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="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">Users&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&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">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 class="k">return&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&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;/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="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">Users&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&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"> 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">result&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&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">id&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">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">id&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">user&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">result&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>呼叫者拿到的是複本，不能繞過 &lt;code>UserRepository&lt;/code> 修改內部狀態。&lt;/p>
&lt;h2 id="mutex-還是-rwmutex">Mutex 還是 RWMutex？&lt;/h2>
&lt;p>選擇鎖的核心規則是：讀多寫少且讀操作可並行時用 &lt;code>RWMutex&lt;/code>；不確定時先用 &lt;code>Mutex&lt;/code>，設計更簡單。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>鎖&lt;/th>
 &lt;th>適合情境&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>sync.Mutex&lt;/code>&lt;/td>
 &lt;td>狀態小、讀寫都簡單、沒有明顯讀多寫少&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>sync.RWMutex&lt;/code>&lt;/td>
 &lt;td>讀取頻繁、寫入較少、讀操作可安全並行&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>sync.Mutex&lt;/code> 的核心優勢是簡單。若狀態很小、讀寫都很快，或讀寫比例尚未明確，先使用 &lt;code>Mutex&lt;/code> 通常更容易維護。它讓每次存取都走同一條鎖路徑，讀者也比較容易確認資料何時被保護。&lt;/p>
&lt;p>&lt;code>sync.RWMutex&lt;/code> 的核心優勢是讀多寫少時可以讓多個讀取並行。它適合像 in-memory cache、狀態查詢 repository 或連線註冊表這類讀取頻繁的資料結構。使用它時，寫入仍然要用 &lt;code>Lock&lt;/code>，因為 &lt;code>RLock&lt;/code> 只適合保護純讀取。&lt;/p>
&lt;p>鎖選擇的判斷重點是資料不變式與讀寫比例。若讀取本身會組裝複雜資料、需要複製大型 map，或很快就會呼叫外部 I/O，&lt;code>RWMutex&lt;/code> 帶來的並行讀取收益可能被複雜度抵消。&lt;/p>
&lt;h2 id="替代方案什麼時候不用-rwmutex">替代方案：什麼時候不用 RWMutex&lt;/h2>
&lt;p>&lt;code>RWMutex&lt;/code> 不是共享狀態保護的唯一選擇。三類替代方案各有適用條件：&lt;/p></description><content:encoded><![CDATA[<p><code>sync.RWMutex</code> 是 Go 用來保護共享狀態的讀寫鎖。它的核心用途是允許多個讀取者同時讀取，但寫入者必須獨占資料，避免 goroutine 同時讀寫 map、slice 或 struct 時產生資料競爭。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>理解 data race 的風險</li>
<li>區分 <code>Mutex</code> 與 <code>RWMutex</code></li>
<li>用 <code>RLock</code> / <code>RUnlock</code> 保護讀取</li>
<li>用 <code>Lock</code> / <code>Unlock</code> 保護寫入</li>
<li>避免回傳內部 map 或 slice 破壞鎖邊界</li>
</ol>
<hr>
<h2 id="觀察共享-map-不能被多個-goroutine-無保護地讀寫">【觀察】共享 map 不能被多個 goroutine 無保護地讀寫</h2>
<p>共享狀態的核心規則是：只要多個 goroutine 可能同時讀寫同一份資料，就必須用同步機制保護。以下程式同時讀寫 map，存在 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">type</span> <span class="nx">UserRepository</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">users</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><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="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">Set</span><span class="p">(</span><span class="nx">id</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">user</span> <span class="nx">User</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span> <span class="p">=</span> <span class="nx">user</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><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="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">Get</span><span class="p">(</span><span class="nx">id</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="nx">User</span><span class="p">,</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">user</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">return</span> <span class="nx">user</span><span class="p">,</span> <span class="nx">ok</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>如果 <code>Set</code> 和 <code>Get</code> 從不同 goroutine 同時執行，map 可能被同時讀寫。Go 的 map 不保證這種情境安全。</p>
<h2 id="判讀rwmutex-區分讀取與寫入">【判讀】RWMutex 區分讀取與寫入</h2>
<p><code>RWMutex</code> 的核心規則是：讀取使用 <code>RLock</code>，寫入使用 <code>Lock</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">type</span> <span class="nx">UserRepository</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">users</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">mu</span>    <span class="nx">sync</span><span class="p">.</span><span class="nx">RWMutex</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="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">Set</span><span class="p">(</span><span class="nx">id</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">user</span> <span class="nx">User</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</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">Lock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 8</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">Unlock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span> <span class="p">=</span> <span class="nx">user</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="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">Get</span><span class="p">(</span><span class="nx">id</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="nx">User</span><span class="p">,</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</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">14</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">15</span><span class="cl">    <span class="nx">user</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">return</span> <span class="nx">user</span><span class="p">,</span> <span class="nx">ok</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>Set</code> 修改 map，所以用 <code>Lock</code>。<code>Get</code> 只讀 map，所以用 <code>RLock</code>。</p>
<h2 id="策略鎖保護的是資料不變式">【策略】鎖保護的是資料不變式</h2>
<p>鎖範圍的核心規則是：鎖要包住所有需要一致觀察或一致修改的資料。鎖的邊界應涵蓋完整不變式，慢速 I/O、網路呼叫與和共享資料無關的計算則應放在鎖外。</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="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">Add</span><span class="p">(</span><span class="nx">user</span> <span class="nx">User</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">Lock</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">Unlock</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">r</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="nx">ID</span><span class="p">]</span> <span class="p">=</span> <span class="nx">user</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">count</span><span class="o">++</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>如果 <code>users</code> 和 <code>count</code> 分開鎖，讀者可能看到 map 已更新但 count 還沒更新的中間狀態。</p>
<h2 id="執行回傳資料時要保留-copy-boundary">【執行】回傳資料時要保留 copy boundary</h2>
<p>鎖邊界的核心規則是：鎖只能保護鎖內操作；回傳內部 map 會讓呼叫者在鎖外修改資料，破壞 repository 對狀態的控制權。</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="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">Users</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="nx">User</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 class="k">return</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</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>





<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">Users</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="nx">User</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">result</span> <span class="o">:=</span> <span class="nb">make</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="nx">User</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">id</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">result</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span> <span class="p">=</span> <span class="nx">user</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">result</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>呼叫者拿到的是複本，不能繞過 <code>UserRepository</code> 修改內部狀態。</p>
<h2 id="mutex-還是-rwmutex">Mutex 還是 RWMutex？</h2>
<p>選擇鎖的核心規則是：讀多寫少且讀操作可並行時用 <code>RWMutex</code>；不確定時先用 <code>Mutex</code>，設計更簡單。</p>
<table>
  <thead>
      <tr>
          <th>鎖</th>
          <th>適合情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>sync.Mutex</code></td>
          <td>狀態小、讀寫都簡單、沒有明顯讀多寫少</td>
      </tr>
      <tr>
          <td><code>sync.RWMutex</code></td>
          <td>讀取頻繁、寫入較少、讀操作可安全並行</td>
      </tr>
  </tbody>
</table>
<p><code>sync.Mutex</code> 的核心優勢是簡單。若狀態很小、讀寫都很快，或讀寫比例尚未明確，先使用 <code>Mutex</code> 通常更容易維護。它讓每次存取都走同一條鎖路徑，讀者也比較容易確認資料何時被保護。</p>
<p><code>sync.RWMutex</code> 的核心優勢是讀多寫少時可以讓多個讀取並行。它適合像 in-memory cache、狀態查詢 repository 或連線註冊表這類讀取頻繁的資料結構。使用它時，寫入仍然要用 <code>Lock</code>，因為 <code>RLock</code> 只適合保護純讀取。</p>
<p>鎖選擇的判斷重點是資料不變式與讀寫比例。若讀取本身會組裝複雜資料、需要複製大型 map，或很快就會呼叫外部 I/O，<code>RWMutex</code> 帶來的並行讀取收益可能被複雜度抵消。</p>
<h2 id="替代方案什麼時候不用-rwmutex">替代方案：什麼時候不用 RWMutex</h2>
<p><code>RWMutex</code> 不是共享狀態保護的唯一選擇。三類替代方案各有適用條件：</p>
<table>
  <thead>
      <tr>
          <th>方案</th>
          <th>適用情境</th>
          <th>跟 RWMutex 對比</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>sync.Map</code></td>
          <td>key 集合大、entries 異步增減、讀寫分散在不同 key</td>
          <td>內建讀寫並行、無全域鎖；但語意不同（無 size、無 range 一致性）</td>
      </tr>
      <tr>
          <td><code>sync/atomic</code></td>
          <td>單一純量（counter、flag、pointer）</td>
          <td>無鎖、最快；但只能保護單一值、不能保護結構不變式</td>
      </tr>
      <tr>
          <td>Channel-based coordination</td>
          <td>狀態由單一 owner goroutine 持有、其他 goroutine 透過 channel 傳訊息</td>
          <td>用 ownership 取代 sharing；適合 producer / consumer pattern、見 <a href="../channel/">4.2 channel</a></td>
      </tr>
  </tbody>
</table>
<p>判別準則：</p>
<ul>
<li>保護<strong>多欄位不變式</strong>（如 <code>users</code> + <code>count</code> 同步）→ <code>RWMutex</code> 或 <code>Mutex</code></li>
<li>保護<strong>單一純量</strong>且操作可表達為 atomic op（CAS、increment）→ <code>sync/atomic</code></li>
<li>保護<strong>大量獨立 key</strong> 且無跨 key 不變式 → <code>sync.Map</code></li>
<li>狀態可由<strong>單一 owner</strong> 持有、外部用訊息驅動 → channel-based、見 <a href="../channel/">4.2</a> / <a href="../backpressure/">4.5 backpressure</a></li>
</ul>
<p>選錯方案的代價：用 <code>sync/atomic</code> 保護需要不變式的多欄位 → silent atomicity violation；用 <code>sync.Map</code> 期待 range 一致性 → 拿到 inconsistent snapshot；用 channel 處理需要嚴格 ordering 的 fan-in → 順序錯亂。</p>
<h2 id="rwmutex-不解的問題">RWMutex 不解的問題</h2>
<p><code>RWMutex</code> 解的是 <strong>data race</strong>（多 goroutine 同時讀寫同一份資料的 visible race）。下列問題<strong>不在 <code>RWMutex</code> 防護範圍</strong>、必須由其他機制處理：</p>
<table>
  <thead>
      <tr>
          <th>不防的問題</th>
          <th>為什麼不解</th>
          <th>該用什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Deadlock</td>
          <td>多把鎖的鎖順序不一致、<code>RWMutex</code> 沒有偵測能力</td>
          <td>鎖排序協議、<code>go test -race</code> 並非 deadlock detector</td>
      </tr>
      <tr>
          <td>Starvation</td>
          <td>RWMutex 設計上 reader 多時 writer 可能長期等不到（Go 實作有部分 fairness 保護）</td>
          <td>量測 lock 等待時間、讀多時切 channel-based 或 sharded 鎖</td>
      </tr>
      <tr>
          <td>Lock contention scaling</td>
          <td>goroutine 增多時、單把鎖的競爭成本可能 dominate；<code>RWMutex</code> 多核 scalability 弱</td>
          <td>sharded lock、sync.Map、無鎖結構</td>
      </tr>
      <tr>
          <td>Context cancellation</td>
          <td>reader 已經 hold RLock 時、context 取消不會強制釋放；reader 必須主動 check ctx</td>
          <td>lock 內快進快出、長操作放鎖外、check ctx</td>
      </tr>
      <tr>
          <td>Atomicity violation</td>
          <td>把多步操作拆到多次 Lock/Unlock 中間、其他 goroutine 可能看到中間狀態</td>
          <td>拉大鎖範圍、或改 transaction-like API</td>
      </tr>
      <tr>
          <td>Memory ordering（跨鎖）</td>
          <td>RWMutex 只保證鎖內 happens-before、跨鎖讀寫的 ordering 沒保證</td>
          <td>用 channel 傳遞 ordering、或 atomic load/store</td>
      </tr>
  </tbody>
</table>
<p>判讀訊號：</p>
<ul>
<li><code>go test -race</code> pass、production 仍偶發資料異常 → 可能 atomicity violation 或 ordering bug、不是 data race</li>
<li>多核 CPU 加倍但 throughput 不增 → lock contention dominate、考慮 shard</li>
<li>p99 latency 在高 concurrency 下爆炸 → reader 排隊或 starvation、查 lock 等待 metric</li>
<li>shutdown 時 goroutine 不退 → reader hold RLock + 未 check ctx、補 context 檢查</li>
</ul>
<h2 id="context-dependencescale-改變策略">Context dependence：scale 改變策略</h2>
<p><code>RWMutex</code> 的有效性會隨 deployment 條件變化：</p>
<ul>
<li><strong>Map 大小</strong>：copy 成本隨 entries 線性增長、1k entries 廉價、1M entries 每次 copy 都是 GC pressure 來源；大 map 改 <code>sync.Map</code> 或 sharded</li>
<li><strong>讀寫比例</strong>：90% 讀以下、<code>RWMutex</code> 收益不顯著、<code>Mutex</code> 簡單；讀寫接近時 RWMutex 的內部 atomic 操作成本可能反而比 Mutex 慢</li>
<li><strong>Goroutine 數量</strong>：少（&lt; 10）時 contention 微、多（&gt; 1000）時 RWMutex 不適合、要 shard 或換 lock-free 結構</li>
<li><strong>持鎖時間</strong>：鎖內 microsecond 級 OK、毫秒級會堆隊；鎖內絕不做 I/O / 網路呼叫</li>
</ul>
<h2 id="選擇-rwmutex-前先問四件事">選擇 RWMutex 前先問四件事</h2>
<p><code>RWMutex</code> 只解 data race subset——不解 deadlock / starvation / atomicity violation / context cancellation / 多核 contention scaling。狀態可表達為 atomic op、單 owner channel、或大量獨立 key 時、<code>sync/atomic</code> / channel-based / <code>sync.Map</code> 通常更合適。選擇前先問：「不變式跨幾個欄位？讀寫比例？goroutine 數量？持鎖時間？」</p>
]]></content:encoded></item><item><title>6.4 如何新增背景工作流程</title><link>https://tarrragon.github.io/blog/go/06-practical/new-background-worker/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/06-practical/new-background-worker/</guid><description>&lt;p>新增背景工作流程的核心規則是先定義生命週期，再定義資料流。worker 是有 context、輸入、輸出、錯誤處理與 shutdown 協定的長期元件。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>判斷一段工作是否適合做成 worker&lt;/li>
&lt;li>用 &lt;code>Run(ctx)&lt;/code> 設計 worker 生命週期&lt;/li>
&lt;li>用 channel 和 ticker 表達資料流與週期性工作&lt;/li>
&lt;li>處理 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> full、shutdown 與錯誤記錄&lt;/li>
&lt;li>分開測試 &lt;code>SyncOnce&lt;/code>、&lt;code>Run(ctx)&lt;/code> 與 channel 行為&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察worker-是有生命週期的元件">【觀察】worker 是有生命週期的元件&lt;/h2>
&lt;p>worker 的核心定義是長時間運行、可被啟動、可被取消、會消費輸入或定期執行工作的元件。任意程式碼包進 &lt;code>go func()&lt;/code> 只能產生背景 goroutine，還需要生命週期協定才會成為可維護的 worker。&lt;/p>
&lt;p>適合做成 worker 的工作通常有三種：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工作類型&lt;/th>
 &lt;th>範例&lt;/th>
 &lt;th>worker 責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>queue &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a>&lt;/td>
 &lt;td>從 channel 讀取外部事件&lt;/td>
 &lt;td>驗證、轉送 processor&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>periodic task&lt;/td>
 &lt;td>每 30 秒同步一次外部狀態&lt;/td>
 &lt;td>產生 command 或 event&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>cleanup task&lt;/td>
 &lt;td>定期清理過期資料&lt;/td>
 &lt;td>呼叫 repository 或 usecase 的清理方法&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>本章使用「通知同步 worker」作為範例。它定期向外部來源取得通知更新，轉成 domain event，再交給 &lt;code>EventProcessor&lt;/code> 處理。&lt;/p>
&lt;h2 id="判讀worker-責任要先寫清楚">【判讀】worker 責任要先寫清楚&lt;/h2>
&lt;p>worker 責任的核心問題是它消費什麼、產生什麼、交給誰處理。worker 應聚焦在資料取得、格式轉換與轉交處理器，業務規則、狀態更新與 client 推送要留給對應的 usecase 或 processor。&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">type&lt;/span> &lt;span class="nx">NotificationSource&lt;/span> &lt;span class="kd">interface&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="nf">FetchUpdates&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">RawNotificationUpdate&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"> 3&lt;/span>&lt;span class="cl">&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="kd">type&lt;/span> &lt;span class="nx">RawNotificationUpdate&lt;/span> &lt;span class="kd">struct&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="nx">ID&lt;/span> &lt;span class="kt">string&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">NotificationID&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">Topic&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">Title&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nx">OccurredAt&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Time&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;/code>&lt;/pre>&lt;/div>&lt;p>再定義 worker 會呼叫的處理器：&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">type&lt;/span> &lt;span class="nx">EventProcessor&lt;/span> &lt;span class="kd">interface&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="nf">Process&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">event&lt;/span> &lt;span class="nx">DomainEvent&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">error&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&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 的責任是把外部更新取回來、normalize 成 &lt;code>DomainEvent&lt;/code>、交給 processor。repository 寫入與推送規則仍然留在 processor 或 usecase 裡。&lt;/p>
&lt;h2 id="策略把單次工作獨立成-synconce">【策略】把單次工作獨立成 &lt;code>SyncOnce&lt;/code>&lt;/h2>
&lt;p>worker 的核心設計技巧是把「單次工作」和「長時間迴圈」分開。&lt;code>SyncOnce&lt;/code> 負責做一次同步，&lt;code>Run(ctx)&lt;/code> 負責週期性呼叫它。&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">type&lt;/span> &lt;span class="nx">SyncWorker&lt;/span> &lt;span class="kd">struct&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">source&lt;/span> &lt;span class="nx">NotificationSource&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">processor&lt;/span> &lt;span class="nx">EventProcessor&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">logger&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">slog&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Logger&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="kd">func&lt;/span> &lt;span class="nf">NewSyncWorker&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">source&lt;/span> &lt;span class="nx">NotificationSource&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">processor&lt;/span> &lt;span class="nx">EventProcessor&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">logger&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">slog&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Logger&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">SyncWorker&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 class="o">&amp;amp;&lt;/span>&lt;span class="nx">SyncWorker&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="nx">source&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">source&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="nx">processor&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">processor&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="nx">logger&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">logger&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>SyncOnce&lt;/code> 可以像普通函式一樣測試：&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">w&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">SyncWorker&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">SyncOnce&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="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">updates&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">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">source&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">FetchUpdates&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&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">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"> 4&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;fetch notification updates: %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"> 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="k">for&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">update&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">updates&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="nx">event&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">NormalizeNotificationUpdate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">update&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Now&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">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">10&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Warn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;skip invalid notification update&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">update&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ID&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;error&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">11&lt;/span>&lt;span class="cl"> &lt;span class="k">continue&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>&lt;/span>&lt;span class="line">&lt;span class="ln">14&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">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">processor&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Process&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">event&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">15&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;process notification update %s: %w&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">update&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ID&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">16&lt;/span>&lt;span class="cl"> &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="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&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">20&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>time.Now()&lt;/code> 先展示基本寫法；如果測試需要固定時間，可以把 clock 注入進 worker。時間注入會在後面測試章節更完整處理。&lt;/p>
&lt;h2 id="執行runctx-管理長時間生命週期">【執行】&lt;code>Run(ctx)&lt;/code> 管理長時間生命週期&lt;/h2>
&lt;p>&lt;code>Run(ctx)&lt;/code> 的核心責任是等待 ticker、呼叫單次工作、尊重取消訊號。它應該在 context 被取消時退出，並釋放 ticker。&lt;/p></description><content:encoded><![CDATA[<p>新增背景工作流程的核心規則是先定義生命週期，再定義資料流。worker 是有 context、輸入、輸出、錯誤處理與 shutdown 協定的長期元件。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>判斷一段工作是否適合做成 worker</li>
<li>用 <code>Run(ctx)</code> 設計 worker 生命週期</li>
<li>用 channel 和 ticker 表達資料流與週期性工作</li>
<li>處理 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> full、shutdown 與錯誤記錄</li>
<li>分開測試 <code>SyncOnce</code>、<code>Run(ctx)</code> 與 channel 行為</li>
</ol>
<hr>
<h2 id="觀察worker-是有生命週期的元件">【觀察】worker 是有生命週期的元件</h2>
<p>worker 的核心定義是長時間運行、可被啟動、可被取消、會消費輸入或定期執行工作的元件。任意程式碼包進 <code>go func()</code> 只能產生背景 goroutine，還需要生命週期協定才會成為可維護的 worker。</p>
<p>適合做成 worker 的工作通常有三種：</p>
<table>
  <thead>
      <tr>
          <th>工作類型</th>
          <th>範例</th>
          <th>worker 責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>queue <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a></td>
          <td>從 channel 讀取外部事件</td>
          <td>驗證、轉送 processor</td>
      </tr>
      <tr>
          <td>periodic task</td>
          <td>每 30 秒同步一次外部狀態</td>
          <td>產生 command 或 event</td>
      </tr>
      <tr>
          <td>cleanup task</td>
          <td>定期清理過期資料</td>
          <td>呼叫 repository 或 usecase 的清理方法</td>
      </tr>
  </tbody>
</table>
<p>本章使用「通知同步 worker」作為範例。它定期向外部來源取得通知更新，轉成 domain event，再交給 <code>EventProcessor</code> 處理。</p>
<h2 id="判讀worker-責任要先寫清楚">【判讀】worker 責任要先寫清楚</h2>
<p>worker 責任的核心問題是它消費什麼、產生什麼、交給誰處理。worker 應聚焦在資料取得、格式轉換與轉交處理器，業務規則、狀態更新與 client 推送要留給對應的 usecase 或 processor。</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">NotificationSource</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nf">FetchUpdates</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">RawNotificationUpdate</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><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="kd">type</span> <span class="nx">RawNotificationUpdate</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="nx">ID</span>             <span class="kt">string</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">NotificationID</span> <span class="kt">string</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">Topic</span>          <span class="kt">string</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">Title</span>          <span class="kt">string</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">OccurredAt</span>     <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>再定義 worker 會呼叫的處理器：</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">EventProcessor</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nf">Process</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">event</span> <span class="nx">DomainEvent</span><span class="p">)</span> <span class="kt">error</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>worker 的責任是把外部更新取回來、normalize 成 <code>DomainEvent</code>、交給 processor。repository 寫入與推送規則仍然留在 processor 或 usecase 裡。</p>
<h2 id="策略把單次工作獨立成-synconce">【策略】把單次工作獨立成 <code>SyncOnce</code></h2>
<p>worker 的核心設計技巧是把「單次工作」和「長時間迴圈」分開。<code>SyncOnce</code> 負責做一次同步，<code>Run(ctx)</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">type</span> <span class="nx">SyncWorker</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">source</span>    <span class="nx">NotificationSource</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">processor</span> <span class="nx">EventProcessor</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">logger</span>    <span class="o">*</span><span class="nx">slog</span><span class="p">.</span><span class="nx">Logger</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">NewSyncWorker</span><span class="p">(</span><span class="nx">source</span> <span class="nx">NotificationSource</span><span class="p">,</span> <span class="nx">processor</span> <span class="nx">EventProcessor</span><span class="p">,</span> <span class="nx">logger</span> <span class="o">*</span><span class="nx">slog</span><span class="p">.</span><span class="nx">Logger</span><span class="p">)</span> <span class="o">*</span><span class="nx">SyncWorker</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="o">&amp;</span><span class="nx">SyncWorker</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nx">source</span><span class="p">:</span>    <span class="nx">source</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="nx">processor</span><span class="p">:</span> <span class="nx">processor</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="nx">logger</span><span class="p">:</span>    <span class="nx">logger</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>SyncOnce</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="p">(</span><span class="nx">w</span> <span class="o">*</span><span class="nx">SyncWorker</span><span class="p">)</span> <span class="nf">SyncOnce</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="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">updates</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">w</span><span class="p">.</span><span class="nx">source</span><span class="p">.</span><span class="nf">FetchUpdates</span><span class="p">(</span><span class="nx">ctx</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">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;fetch notification updates: %w&#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></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">update</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">updates</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">event</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">NormalizeNotificationUpdate</span><span class="p">(</span><span class="nx">update</span><span class="p">,</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Now</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="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="nx">w</span><span class="p">.</span><span class="nx">logger</span><span class="p">.</span><span class="nf">Warn</span><span class="p">(</span><span class="s">&#34;skip invalid notification update&#34;</span><span class="p">,</span> <span class="s">&#34;id&#34;</span><span class="p">,</span> <span class="nx">update</span><span class="p">.</span><span class="nx">ID</span><span class="p">,</span> <span class="s">&#34;error&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="k">continue</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></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">w</span><span class="p">.</span><span class="nx">processor</span><span class="p">.</span><span class="nf">Process</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">event</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">15</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;process notification update %s: %w&#34;</span><span class="p">,</span> <span class="nx">update</span><span class="p">.</span><span class="nx">ID</span><span class="p">,</span> <span class="nx">err</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 class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這裡的 <code>time.Now()</code> 先展示基本寫法；如果測試需要固定時間，可以把 clock 注入進 worker。時間注入會在後面測試章節更完整處理。</p>
<h2 id="執行runctx-管理長時間生命週期">【執行】<code>Run(ctx)</code> 管理長時間生命週期</h2>
<p><code>Run(ctx)</code> 的核心責任是等待 ticker、呼叫單次工作、尊重取消訊號。它應該在 context 被取消時退出，並釋放 ticker。</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">w</span> <span class="o">*</span><span class="nx">SyncWorker</span><span class="p">)</span> <span class="nf">Run</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">interval</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Duration</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">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">interval</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 class="nx">ctx</span><span class="p">.</span><span class="nf">Err</span><span class="p">()</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="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">w</span><span class="p">.</span><span class="nf">SyncOnce</span><span class="p">(</span><span class="nx">ctx</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">11</span><span class="cl">                <span class="nx">w</span><span class="p">.</span><span class="nx">logger</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="s">&#34;sync worker failed&#34;</span><span class="p">,</span> <span class="s">&#34;error&#34;</span><span class="p">,</span> <span class="nx">err</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><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>這個版本把單次同步錯誤記錄下來，但不讓 worker 退出。這是策略選擇：若外部來源短暫失敗，worker 可以等待下一輪；若錯誤代表設定失效或授權失效，則可以選擇 return error 讓上層重啟或停止服務。</p>
<p>worker 錯誤策略應該明確。暫時性錯誤通常要記錄後等待下一輪；致命設定錯誤則可以回傳給上層，讓服務決定重啟或停止。</p>
<h2 id="判讀channel-worker-要設計-backpressure">【判讀】channel worker 要設計 backpressure</h2>
<p>channel worker 的核心問題是接收端跟不上時要怎麼辦。<a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 大小、blocking send、non-blocking send 都是在回答 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> 策略。</p>
<p>假設外部 HTTP callback 會把 raw update 送進 worker queue：</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">QueueWorker</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">updates</span>   <span class="kd">chan</span> <span class="nx">RawNotificationUpdate</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">processor</span> <span class="nx">EventProcessor</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">logger</span>    <span class="o">*</span><span class="nx">slog</span><span class="p">.</span><span class="nx">Logger</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">NewQueueWorker</span><span class="p">(</span><span class="nx">processor</span> <span class="nx">EventProcessor</span><span class="p">,</span> <span class="nx">logger</span> <span class="o">*</span><span class="nx">slog</span><span class="p">.</span><span class="nx">Logger</span><span class="p">,</span> <span class="nx">bufferSize</span> <span class="kt">int</span><span class="p">)</span> <span class="o">*</span><span class="nx">QueueWorker</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="o">&amp;</span><span class="nx">QueueWorker</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nx">updates</span><span class="p">:</span>   <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="nx">RawNotificationUpdate</span><span class="p">,</span> <span class="nx">bufferSize</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="nx">processor</span><span class="p">:</span> <span class="nx">processor</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="nx">logger</span><span class="p">:</span>    <span class="nx">logger</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>送入 queue 可以選擇 blocking 或 non-blocking。若呼叫端不能被背景處理拖慢，可以用 non-blocking send 並回傳錯誤：</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">ErrQueueFull</span> <span class="p">=</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;notification update queue full&#34;</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="p">(</span><span class="nx">w</span> <span class="o">*</span><span class="nx">QueueWorker</span><span class="p">)</span> <span class="nf">Enqueue</span><span class="p">(</span><span class="nx">update</span> <span class="nx">RawNotificationUpdate</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">case</span> <span class="nx">w</span><span class="p">.</span><span class="nx">updates</span> <span class="o">&lt;-</span> <span class="nx">update</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">default</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">ErrQueueFull</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="p">}</span></span></span></code></pre></div><p>這個設計很誠實：queue 滿了就是系統忙碌。上層可以記錄 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>、回 <code>503</code>，或告訴 client 稍後重試。</p>
<h2 id="執行queue-worker-要同時監聽輸入與取消">【執行】queue worker 要同時監聽輸入與取消</h2>
<p>queue worker 的核心生命週期是等待 update 或 context cancel。<code>Run(ctx)</code> 裡應用 <code>select</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="p">(</span><span class="nx">w</span> <span class="o">*</span><span class="nx">QueueWorker</span><span class="p">)</span> <span class="nf">Run</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="kt">error</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 class="nx">ctx</span><span class="p">.</span><span class="nf">Err</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="nx">update</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">w</span><span class="p">.</span><span class="nx">updates</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 class="kc">nil</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="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">w</span><span class="p">.</span><span class="nf">handleUpdate</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">update</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">11</span><span class="cl">                <span class="nx">w</span><span class="p">.</span><span class="nx">logger</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="s">&#34;handle notification update failed&#34;</span><span class="p">,</span> <span class="s">&#34;id&#34;</span><span class="p">,</span> <span class="nx">update</span><span class="p">.</span><span class="nx">ID</span><span class="p">,</span> <span class="s">&#34;error&#34;</span><span class="p">,</span> <span class="nx">err</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><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><code>handleUpdate</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="p">(</span><span class="nx">w</span> <span class="o">*</span><span class="nx">QueueWorker</span><span class="p">)</span> <span class="nf">handleUpdate</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">update</span> <span class="nx">RawNotificationUpdate</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">event</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">NormalizeNotificationUpdate</span><span class="p">(</span><span class="nx">update</span><span class="p">,</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Now</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">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;normalize update: %w&#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">return</span> <span class="nx">w</span><span class="p">.</span><span class="nx">processor</span><span class="p">.</span><span class="nf">Process</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">event</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>這裡仍然遵守同一條邊界：worker 不直接改 repository，只把事件交給 processor。</p>
<h2 id="策略shutdown-是否-drain-queue-要先決定">【策略】shutdown 是否 drain queue 要先決定</h2>
<p>shutdown 的核心決策是取消時要立刻停止，還是處理完 queue 中既有資料。兩種策略都合理，但語意不同。</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>做法</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>立即停止</td>
          <td>收到 <code>ctx.Done()</code> 就 return</td>
          <td>即時通知、可重試資料</td>
      </tr>
      <tr>
          <td>drain queue</td>
          <td>停止接收新資料，處理已排隊資料</td>
          <td>不可輕易丟棄的資料</td>
      </tr>
  </tbody>
</table>
<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">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">2</span><span class="cl">    <span class="k">return</span> <span class="nx">ctx</span><span class="p">.</span><span class="nf">Err</span><span class="p">()</span></span></span></code></pre></div><p>drain queue 則需要另一個停止接收的協定，例如由擁有送出端的一方關閉 channel，再讓 worker range 到 channel 關閉。channel close 的所有權要留在送出端，因為送出端最清楚是否還會送資料。</p>
<p>這裡的核心區分是「取消 context」和「關閉 channel」代表不同訊號。context 表示這件工作該停了；channel close 表示不會再有新資料。兩者可以搭配，但語意不同。</p>
<h2 id="判讀worker-使用服務生命週期-context">【判讀】worker 使用服務生命週期 context</h2>
<p>worker context 的核心規則是：長時間 worker 使用服務生命週期 context，單次工作可以另外接收 request context。某個 HTTP request 的 context 只適合控制該次請求；request 結束後 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="p">(</span><span class="nx">w</span> <span class="o">*</span><span class="nx">QueueWorker</span><span class="p">)</span> <span class="nf">Enqueue</span><span class="p">(</span><span class="nx">update</span> <span class="nx">RawNotificationUpdate</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="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">case</span> <span class="nx">w</span><span class="p">.</span><span class="nx">updates</span> <span class="o">&lt;-</span> <span class="nx">update</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="k">default</span><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">ErrQueueFull</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <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>Enqueue</code> 不把 request context 存起來。真正處理 update 時，worker 使用自己的 <code>Run(ctx)</code> context 控制生命週期。</p>
<p>若某筆 update 需要保留 <a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request ID</a> 或 <a href="/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation ID</a>，應把它放進明確欄位，而不是依賴 context value 在背景工作中長期存在。</p>
<h2 id="執行synconce-測試要隔離時間與外部來源">【執行】<code>SyncOnce</code> 測試要隔離時間與外部來源</h2>
<p><code>SyncOnce</code> 測試的核心目標是確認單次工作會把外部資料交給 processor。測試重點放在單次同步，不需要等待真實 ticker。</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">fakeNotificationSource</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">updates</span> <span class="p">[]</span><span class="nx">RawNotificationUpdate</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">err</span>     <span class="kt">error</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="kd">func</span> <span class="p">(</span><span class="nx">f</span> <span class="nx">fakeNotificationSource</span><span class="p">)</span> <span class="nf">FetchUpdates</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">RawNotificationUpdate</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"> 7</span><span class="cl">    <span class="k">if</span> <span class="nx">f</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"> 8</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">f</span><span class="p">.</span><span class="nx">err</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="k">return</span> <span class="nx">f</span><span class="p">.</span><span class="nx">updates</span><span class="p">,</span> <span class="kc">nil</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></span><span class="line"><span class="ln">13</span><span class="cl"><span class="kd">type</span> <span class="nx">recordingProcessor</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nx">events</span> <span class="p">[]</span><span class="nx">DomainEvent</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></span><span class="line"><span class="ln">17</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">p</span> <span class="o">*</span><span class="nx">recordingProcessor</span><span class="p">)</span> <span class="nf">Process</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">event</span> <span class="nx">DomainEvent</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="nx">p</span><span class="p">.</span><span class="nx">events</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">events</span><span class="p">,</span> <span class="nx">event</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="kc">nil</span>
</span></span><span class="line"><span class="ln">20</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">TestSyncWorkerSyncOnce</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">source</span> <span class="o">:=</span> <span class="nx">fakeNotificationSource</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="nx">updates</span><span class="p">:</span> <span class="p">[]</span><span class="nx">RawNotificationUpdate</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="nx">ID</span><span class="p">:</span>             <span class="s">&#34;evt_1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">                <span class="nx">NotificationID</span><span class="p">:</span> <span class="s">&#34;ntf_1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">                <span class="nx">Topic</span><span class="p">:</span>          <span class="s">&#34;deployments&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">                <span class="nx">Title</span><span class="p">:</span>          <span class="s">&#34;Deploy finished&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">                <span class="nx">OccurredAt</span><span class="p">:</span>     <span class="nx">time</span><span class="p">.</span><span class="nf">Date</span><span class="p">(</span><span class="mi">2026</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">22</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">time</span><span class="p">.</span><span class="nx">UTC</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 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="nx">processor</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">recordingProcessor</span><span class="p">{}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nx">worker</span> <span class="o">:=</span> <span class="nf">NewSyncWorker</span><span class="p">(</span><span class="nx">source</span><span class="p">,</span> <span class="nx">processor</span><span class="p">,</span> <span class="nx">slog</span><span class="p">.</span><span class="nf">Default</span><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="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">worker</span><span class="p">.</span><span class="nf">SyncOnce</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</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">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;sync once: %v&#34;</span><span class="p">,</span> <span class="nx">err</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></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="nx">processor</span><span class="p">.</span><span class="nx">events</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">1</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">21</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;processed events = %d, want 1&#34;</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">processor</span><span class="p">.</span><span class="nx">events</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個測試不需要 goroutine。先把單次工作測清楚，再測長時間生命週期。</p>
<h2 id="執行runctx-測試要能快速取消">【執行】<code>Run(ctx)</code> 測試要能快速取消</h2>
<p><code>Run(ctx)</code> 測試的核心目標是確認 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">TestSyncWorkerRunStopsWhenContextCanceled</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">source</span> <span class="o">:=</span> <span class="nx">fakeNotificationSource</span><span class="p">{}</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">processor</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">recordingProcessor</span><span class="p">{}</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">worker</span> <span class="o">:=</span> <span class="nf">NewSyncWorker</span><span class="p">(</span><span class="nx">source</span><span class="p">,</span> <span class="nx">processor</span><span class="p">,</span> <span class="nx">slog</span><span class="p">.</span><span class="nf">Default</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">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"> 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="nx">err</span> <span class="o">:=</span> <span class="nx">worker</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Hour</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">if</span> <span class="p">!</span><span class="nx">errors</span><span class="p">.</span><span class="nf">Is</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Canceled</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</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;error = %v, want context.Canceled&#34;</span><span class="p">,</span> <span class="nx">err</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>time.Hour</code> 當 interval，因為 context 已經取消，<code>Run</code> 應該立刻退出，不需要等 ticker。</p>
<h2 id="執行queue-full-測試要固定-buffer">【執行】queue full 測試要固定 buffer</h2>
<p>queue full 測試的核心目標是確認 backpressure 策略。buffer 設成 1，先塞滿，再確認第二次 enqueue 回錯。</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">TestQueueWorkerEnqueueFull</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">worker</span> <span class="o">:=</span> <span class="nf">NewQueueWorker</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">recordingProcessor</span><span class="p">{},</span> <span class="nx">slog</span><span class="p">.</span><span class="nf">Default</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></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">err</span> <span class="o">:=</span> <span class="nx">worker</span><span class="p">.</span><span class="nf">Enqueue</span><span class="p">(</span><span class="nx">RawNotificationUpdate</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;evt_1&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln"> 5</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"> 6</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;first enqueue: %v&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <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="nx">err</span> <span class="p">=</span> <span class="nx">worker</span><span class="p">.</span><span class="nf">Enqueue</span><span class="p">(</span><span class="nx">RawNotificationUpdate</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;evt_2&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">if</span> <span class="p">!</span><span class="nx">errors</span><span class="p">.</span><span class="nf">Is</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">ErrQueueFull</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</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;error = %v, want ErrQueueFull&#34;</span><span class="p">,</span> <span class="nx">err</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>這個測試不啟動 worker，所以 channel 裡第一筆資料不會被消費，第二筆必然遇到 full。這比用 sleep 製造滿載狀態穩定。</p>
<h2 id="實作檢查清單">實作檢查清單</h2>
<p>新增 background worker 時，可以依序檢查：</p>
<ol>
<li>worker 責任是否明確：消費什麼，產生什麼，交給誰</li>
<li>是否有 <code>Run(ctx)</code> 作為生命週期入口</li>
<li>單次工作是否拆成 <code>SyncOnce</code> 或 <code>handleUpdate</code></li>
<li>worker 是否尊重 <code>ctx.Done()</code></li>
<li>ticker 是否 <code>defer Stop()</code></li>
<li>channel buffer 是否有明確 backpressure 策略</li>
<li>queue full 是否回錯或記錄，而不是靜默丟棄</li>
<li>worker 是否呼叫 usecase/processor，而不是直接改 repository</li>
<li>測試是否避免真實長時間 sleep</li>
</ol>
<h2 id="設計檢查">設計檢查</h2>
<h3 id="檢查一goroutine-要有停止條件">檢查一：goroutine 要有停止條件</h3>
<p>長時間 goroutine 需要 context、channel close 或其他退出條件。缺少停止條件時，服務運行越久，越容易累積難以診斷的資源問題。</p>
<h3 id="檢查二worker-透過-processor-或-usecase-修改狀態">檢查二：worker 透過 processor 或 usecase 修改狀態</h3>
<p>worker 透過 processor 或 usecase 修改狀態，可以讓背景流程和即時流程共用同一套規則。worker 直接改 repository 時，狀態規則容易分散。</p>
<h3 id="檢查三queue-full-要有明確策略">檢查三：queue full 要有明確策略</h3>
<p>資料可以丟時，應明確記錄 log 或 metric；資料需要保留時，應 blocking 或回錯。queue full 策略明確，後續追蹤才有依據。</p>
<h3 id="檢查四測試隔離真實時間">檢查四：測試隔離真實時間</h3>
<p>測試優先測 <code>SyncOnce</code>，再用已取消 context 測 <code>Run</code> 的退出行為。真實 ticker 等待數秒會讓測試慢且不穩。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先處理單一背景工作如何啟動、停止與回報；cron、queue、retry 與 outbox，會在下列章節再往外延伸：</p>
<ul>
<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>
<li><a href="/blog/go-advanced/07-distributed-operations/outbox-idempotency/" data-link-title="7.2 Durable queue、outbox 與 idempotency" data-link-desc="設計跨 process 事件傳遞的可靠性與去重邊界">Go 進階：Durable queue、outbox 與 idempotency</a></li>
<li><a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Backend：訊息佇列與事件傳遞</a></li>
</ul>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 goroutine 生命週期與 event processing；如果你要先回看語言教材，可以讀：</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/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/06-practical/new-event-type/" data-link-title="6.2 如何新增一種 domain event" data-link-desc="擴展事件常數、輸入驗證與處理流程">Go：如何新增一種 domain event</a></li>
</ul>
]]></content:encoded></item><item><title>7.4 狀態管理的安全邊界</title><link>https://tarrragon.github.io/blog/go/07-refactoring/state-boundary/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/07-refactoring/state-boundary/</guid><description>&lt;p>狀態管理重構的核心目標是集中寫入、保護 map、回傳複製資料，並避免讓 handler、背景工作或即時連線直接操作內部狀態。本章用一般 repository 範例說明如何建立安全邊界。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>辨識共享狀態外洩的程式碼壞味道&lt;/li>
&lt;li>用 repository 或 state owner 集中寫入&lt;/li>
&lt;li>用 &lt;code>sync.RWMutex&lt;/code> 保護 map、slice 與狀態不變式&lt;/li>
&lt;li>用 copy boundary 防止呼叫端修改內部資料&lt;/li>
&lt;li>用行為測試與 &lt;code>go test -race&lt;/code> 驗證並發狀態&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察共享狀態外洩會讓規則分散">【觀察】共享狀態外洩會讓規則分散&lt;/h2>
&lt;p>共享狀態外洩的核心問題是多個元件可以繞過同一套規則直接修改資料。當 handler、worker、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> client manager 都能改同一個 map，狀態不一致與 data race 會變得很難追蹤。&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">type&lt;/span> &lt;span class="nx">Server&lt;/span> &lt;span class="kd">struct&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">jobs&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">JobProjection&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&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="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">s&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Server&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">handleJobStarted&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ResponseWriter&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">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Request&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"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">id&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">URL&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Query&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nf">Get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;id&amp;#34;&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">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">jobs&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">id&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">JobProjection&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="nx">ID&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">id&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="nx">Status&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">JobStatusRunning&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="nx">UpdatedAt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Now&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="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteHeader&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">StatusAccepted&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="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">s&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Server&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">handleJobList&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ResponseWriter&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">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Request&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">16&lt;/span>&lt;span class="cl"> &lt;span class="nx">_&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">json&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewEncoder&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nf">Encode&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">s&lt;/span>&lt;span class="p">.&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">17&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段程式有三個問題：handler 直接改 map，map 沒有 lock，查詢直接輸出內部資料。只要另一個 goroutine 同時讀寫 &lt;code>jobs&lt;/code>，就可能產生 data race。&lt;/p>
&lt;h2 id="判讀state-owner-是唯一寫入入口">【判讀】state owner 是唯一寫入入口&lt;/h2>
&lt;p>state owner 的核心責任是擁有資料與狀態轉移規則。它可以叫 repository、store、manager；名稱不是重點，重點是所有寫入都經過同一組方法。&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">type&lt;/span> &lt;span class="nx">JobRepository&lt;/span> &lt;span class="kd">struct&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">mu&lt;/span> &lt;span class="nx">sync&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">RWMutex&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">jobs&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">JobProjection&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="kd">func&lt;/span> &lt;span class="nf">NewJobRepository&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">JobRepository&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">return&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">JobRepository&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="nx">jobs&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">JobProjection&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;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>handler 不再直接改 map，而是呼叫 repository 方法：&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">JobRepository&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">MarkRunning&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">id&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">now&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Time&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="k">if&lt;/span> &lt;span class="nx">strings&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">TrimSpace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">id&lt;/span>&lt;span class="p">)&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"> 3&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;job id is required&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="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="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">Lock&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">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">Unlock&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">job&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">jobs&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">id&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="nx">job&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ID&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">id&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">job&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Status&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">JobStatusRunning&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nx">job&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">UpdatedAt&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">now&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">jobs&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">id&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">job&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&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">15&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個方法把「running 狀態怎麼寫入」集中起來。未來如果 running 只能從 pending 轉移，規則也加在這裡。&lt;/p>
&lt;h2 id="策略鎖保護的是不變式">【策略】鎖保護的是不變式&lt;/h2>
&lt;p>lock 的核心責任是保護完整狀態不變式，不只是保護某一行 map assignment。若一次狀態轉移要同時更新 current、history、updated time，就要在同一把鎖內完成。&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">type&lt;/span> &lt;span class="nx">JobRecord&lt;/span> &lt;span class="kd">struct&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">Current&lt;/span> &lt;span class="nx">JobProjection&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">History&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="nx">JobProjection&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="kd">type&lt;/span> &lt;span class="nx">JobRepository&lt;/span> &lt;span class="kd">struct&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">mu&lt;/span> &lt;span class="nx">sync&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">RWMutex&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> &lt;span class="nx">records&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">JobRecord&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>寫入時同時更新 summary 與 history：&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">JobRepository&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Apply&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">event&lt;/span> &lt;span class="nx">JobEvent&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">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">Lock&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">Unlock&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">record&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">records&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">JobID&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="nx">next&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">record&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Current&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">next&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ID&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">JobID&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">next&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">UpdatedAt&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">OccurredAt&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="k">switch&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Type&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="k">case&lt;/span> &lt;span class="s">&amp;#34;job.started&amp;#34;&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="nx">next&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Status&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">JobStatusRunning&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">case&lt;/span> &lt;span class="s">&amp;#34;job.succeeded&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="nx">next&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Status&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">JobStatusSucceeded&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="nx">next&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Progress&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">100&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="s">&amp;#34;job.failed&amp;#34;&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="nx">next&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Status&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">JobStatusFailed&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="k">default&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&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;unsupported job event type %q&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Type&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> &lt;span class="nx">record&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Current&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">next&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> &lt;span class="nx">record&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">History&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">record&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">History&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl"> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">records&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">JobID&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">record&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&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">26&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段程式讓 current 與 history 保持一致。若分散在不同 handler 或不同鎖裡，就可能留下「current 已更新但 history 沒有記錄」的中間狀態。&lt;/p></description><content:encoded><![CDATA[<p>狀態管理重構的核心目標是集中寫入、保護 map、回傳複製資料，並避免讓 handler、背景工作或即時連線直接操作內部狀態。本章用一般 repository 範例說明如何建立安全邊界。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>辨識共享狀態外洩的程式碼壞味道</li>
<li>用 repository 或 state owner 集中寫入</li>
<li>用 <code>sync.RWMutex</code> 保護 map、slice 與狀態不變式</li>
<li>用 copy boundary 防止呼叫端修改內部資料</li>
<li>用行為測試與 <code>go test -race</code> 驗證並發狀態</li>
</ol>
<hr>
<h2 id="觀察共享狀態外洩會讓規則分散">【觀察】共享狀態外洩會讓規則分散</h2>
<p>共享狀態外洩的核心問題是多個元件可以繞過同一套規則直接修改資料。當 handler、worker、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> client manager 都能改同一個 map，狀態不一致與 data race 會變得很難追蹤。</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">Server</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">jobs</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">JobProjection</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><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="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">Server</span><span class="p">)</span> <span class="nf">handleJobStarted</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">id</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">URL</span><span class="p">.</span><span class="nf">Query</span><span class="p">().</span><span class="nf">Get</span><span class="p">(</span><span class="s">&#34;id&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">s</span><span class="p">.</span><span class="nx">jobs</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span> <span class="p">=</span> <span class="nx">JobProjection</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">ID</span><span class="p">:</span>        <span class="nx">id</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nx">Status</span><span class="p">:</span>    <span class="nx">JobStatusRunning</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="nx">UpdatedAt</span><span class="p">:</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Now</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="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusAccepted</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></span><span class="line"><span class="ln">15</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">Server</span><span class="p">)</span> <span class="nf">handleJobList</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="nx">_</span> <span class="p">=</span> <span class="nx">json</span><span class="p">.</span><span class="nf">NewEncoder</span><span class="p">(</span><span class="nx">w</span><span class="p">).</span><span class="nf">Encode</span><span class="p">(</span><span class="nx">s</span><span class="p">.</span><span class="nx">jobs</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段程式有三個問題：handler 直接改 map，map 沒有 lock，查詢直接輸出內部資料。只要另一個 goroutine 同時讀寫 <code>jobs</code>，就可能產生 data race。</p>
<h2 id="判讀state-owner-是唯一寫入入口">【判讀】state owner 是唯一寫入入口</h2>
<p>state owner 的核心責任是擁有資料與狀態轉移規則。它可以叫 repository、store、manager；名稱不是重點，重點是所有寫入都經過同一組方法。</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">JobRepository</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">mu</span>   <span class="nx">sync</span><span class="p">.</span><span class="nx">RWMutex</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">jobs</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">JobProjection</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="kd">func</span> <span class="nf">NewJobRepository</span><span class="p">()</span> <span class="o">*</span><span class="nx">JobRepository</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="o">&amp;</span><span class="nx">JobRepository</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">jobs</span><span class="p">:</span> <span class="nb">make</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="nx">JobProjection</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="p">}</span></span></span></code></pre></div><p>handler 不再直接改 map，而是呼叫 repository 方法：</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">JobRepository</span><span class="p">)</span> <span class="nf">MarkRunning</span><span class="p">(</span><span class="nx">id</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">now</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</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="k">if</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">TrimSpace</span><span class="p">(</span><span class="nx">id</span><span class="p">)</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="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;job id is required&#34;</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></span><span class="line"><span class="ln"> 6</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">Lock</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="nx">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Unlock</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="nx">job</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">jobs</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">job</span><span class="p">.</span><span class="nx">ID</span> <span class="p">=</span> <span class="nx">id</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nx">job</span><span class="p">.</span><span class="nx">Status</span> <span class="p">=</span> <span class="nx">JobStatusRunning</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nx">job</span><span class="p">.</span><span class="nx">UpdatedAt</span> <span class="p">=</span> <span class="nx">now</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">jobs</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span> <span class="p">=</span> <span class="nx">job</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個方法把「running 狀態怎麼寫入」集中起來。未來如果 running 只能從 pending 轉移，規則也加在這裡。</p>
<h2 id="策略鎖保護的是不變式">【策略】鎖保護的是不變式</h2>
<p>lock 的核心責任是保護完整狀態不變式，不只是保護某一行 map assignment。若一次狀態轉移要同時更新 current、history、updated time，就要在同一把鎖內完成。</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">JobRecord</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">Current</span> <span class="nx">JobProjection</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">History</span> <span class="p">[]</span><span class="nx">JobProjection</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="kd">type</span> <span class="nx">JobRepository</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nx">mu</span>      <span class="nx">sync</span><span class="p">.</span><span class="nx">RWMutex</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="nx">records</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">JobRecord</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>寫入時同時更新 summary 與 history：</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">JobRepository</span><span class="p">)</span> <span class="nf">Apply</span><span class="p">(</span><span class="nx">event</span> <span class="nx">JobEvent</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">r</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Lock</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">Unlock</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">record</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">records</span><span class="p">[</span><span class="nx">event</span><span class="p">.</span><span class="nx">JobID</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">next</span> <span class="o">:=</span> <span class="nx">record</span><span class="p">.</span><span class="nx">Current</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">next</span><span class="p">.</span><span class="nx">ID</span> <span class="p">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">JobID</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">next</span><span class="p">.</span><span class="nx">UpdatedAt</span> <span class="p">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">OccurredAt</span>
</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">    <span class="k">switch</span> <span class="nx">event</span><span class="p">.</span><span class="nx">Type</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="s">&#34;job.started&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="nx">next</span><span class="p">.</span><span class="nx">Status</span> <span class="p">=</span> <span class="nx">JobStatusRunning</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">case</span> <span class="s">&#34;job.succeeded&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="nx">next</span><span class="p">.</span><span class="nx">Status</span> <span class="p">=</span> <span class="nx">JobStatusSucceeded</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="nx">next</span><span class="p">.</span><span class="nx">Progress</span> <span class="p">=</span> <span class="mi">100</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">case</span> <span class="s">&#34;job.failed&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="nx">next</span><span class="p">.</span><span class="nx">Status</span> <span class="p">=</span> <span class="nx">JobStatusFailed</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="k">default</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">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;unsupported job event type %q&#34;</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">Type</span><span class="p">)</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></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="nx">record</span><span class="p">.</span><span class="nx">Current</span> <span class="p">=</span> <span class="nx">next</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="nx">record</span><span class="p">.</span><span class="nx">History</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">record</span><span class="p">.</span><span class="nx">History</span><span class="p">,</span> <span class="nx">next</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">records</span><span class="p">[</span><span class="nx">event</span><span class="p">.</span><span class="nx">JobID</span><span class="p">]</span> <span class="p">=</span> <span class="nx">record</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段程式讓 current 與 history 保持一致。若分散在不同 handler 或不同鎖裡，就可能留下「current 已更新但 history 沒有記錄」的中間狀態。</p>
<h2 id="執行讀取要回傳-copy">【執行】讀取要回傳 copy</h2>
<p>copy boundary 的核心目標是避免呼叫端拿到內部可變資料。鎖只保護鎖內操作；一旦把內部 slice 或 pointer 回傳出去，呼叫端就可以在鎖外修改資料。</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="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">JobRepository</span><span class="p">)</span> <span class="nf">Get</span><span class="p">(</span><span class="nx">id</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="nx">JobProjection</span><span class="p">,</span> <span class="kt">bool</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">record</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">records</span><span class="p">[</span><span class="nx">id</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="p">!</span><span class="nx">ok</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="nx">JobProjection</span><span class="p">{},</span> <span class="kc">false</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="nf">cloneJobProjection</span><span class="p">(</span><span class="nx">record</span><span class="p">.</span><span class="nx">Current</span><span class="p">),</span> <span class="kc">true</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>history 查詢：</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">JobRepository</span><span class="p">)</span> <span class="nf">History</span><span class="p">(</span><span class="nx">id</span> <span class="kt">string</span><span class="p">)</span> <span class="p">[]</span><span class="nx">JobProjection</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">history</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">records</span><span class="p">[</span><span class="nx">id</span><span class="p">].</span><span class="nx">History</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">result</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="nx">JobProjection</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">history</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">for</span> <span class="nx">i</span><span class="p">,</span> <span class="nx">item</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">history</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">result</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="p">=</span> <span class="nf">cloneJobProjection</span><span class="p">(</span><span class="nx">item</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="k">return</span> <span class="nx">result</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>clone 函式處理 pointer 欄位：</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">cloneJobProjection</span><span class="p">(</span><span class="nx">job</span> <span class="nx">JobProjection</span><span class="p">)</span> <span class="nx">JobProjection</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">cloned</span> <span class="o">:=</span> <span class="nx">job</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">if</span> <span class="nx">job</span><span class="p">.</span><span class="nx">FinishedAt</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">finishedAt</span> <span class="o">:=</span> <span class="o">*</span><span class="nx">job</span><span class="p">.</span><span class="nx">FinishedAt</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="nx">cloned</span><span class="p">.</span><span class="nx">FinishedAt</span> <span class="p">=</span> <span class="o">&amp;</span><span class="nx">finishedAt</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="k">return</span> <span class="nx">cloned</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>如果 struct 未來新增 slice、map 或 pointer 欄位，clone 函式也要跟著更新。這是資料擁有權邊界的一部分。</p>
<h2 id="判讀state-和-projection-要分清楚">【判讀】state 和 projection 要分清楚</h2>
<p>state/<a href="/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection</a> 分離的核心原因是寫入規則與讀取需求不同。domain state 保存規則，projection 服務查詢與顯示。</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">JobState</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">ID</span>        <span class="kt">string</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">Status</span>    <span class="nx">JobStatus</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">UpdatedAt</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</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">type</span> <span class="nx">JobProjection</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">ID</span>          <span class="kt">string</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">Status</span>      <span class="nx">JobStatus</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">Progress</span>    <span class="kt">int</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nx">UpdatedAt</span>   <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nx">DisplayText</span> <span class="kt">string</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>DisplayText</code> 不應參與狀態轉移，它是 response 或 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a> 的資料。若把顯示文字混進核心 state，前端文案改動就會牽動業務規則測試。</p>
<p>重構時不一定要一次拆出兩個 struct。可以先在程式碼中標記哪些欄位是 state，哪些欄位是 projection；等壓力變大，再正式拆型別。</p>
<h2 id="策略handler-只請求狀態更新">【策略】handler 只請求狀態更新</h2>
<p>handler 的核心責任是把 HTTP request 轉成狀態更新請求，而不是自己修改狀態。</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">JobStarter</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nf">MarkRunning</span><span class="p">(</span><span class="nx">id</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">now</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="kt">error</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><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="kd">type</span> <span class="nx">JobHandler</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="nx">jobs</span> <span class="nx">JobStarter</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">now</span>  <span class="kd">func</span><span class="p">()</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</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></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">JobHandler</span><span class="p">)</span> <span class="nf">Start</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nx">id</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">URL</span><span class="p">.</span><span class="nf">Query</span><span class="p">().</span><span class="nf">Get</span><span class="p">(</span><span class="s">&#34;id&#34;</span><span class="p">)</span>
</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">h</span><span class="p">.</span><span class="nx">jobs</span><span class="p">.</span><span class="nf">MarkRunning</span><span class="p">(</span><span class="nx">id</span><span class="p">,</span> <span class="nx">h</span><span class="p">.</span><span class="nf">now</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="nx">http</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">&#34;start job&#34;</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusBadRequest</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="k">return</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="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusAccepted</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>handler 不知道 repository 內部用 map、slice、mutex 還是資料庫。它只知道「可以把 job 標記為 running」。</p>
<h2 id="策略為未來資料庫保留邊界但不提前綁死">【策略】為未來資料庫保留邊界，但不提前綁死</h2>
<p><a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database</a>-ready 邊界的核心是 context、error 與一致性語意，不是提早引入 ORM。memory repository 可以先存在，但方法簽名可以保留未來 I/O 的可能。</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">JobRepositoryPort</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nf">Apply</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">event</span> <span class="nx">JobEvent</span><span class="p">)</span> <span class="kt">error</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nf">Get</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">id</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="nx">JobProjection</span><span class="p">,</span> <span class="kt">bool</span><span class="p">,</span> <span class="kt">error</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>memory implementation 可以忽略 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="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">JobRepository</span><span class="p">)</span> <span class="nf">Get</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">id</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="nx">JobProjection</span><span class="p">,</span> <span class="kt">bool</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">record</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">records</span><span class="p">[</span><span class="nx">id</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="p">!</span><span class="nx">ok</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="nx">JobProjection</span><span class="p">{},</span> <span class="kc">false</span><span class="p">,</span> <span class="kc">nil</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="nf">cloneJobProjection</span><span class="p">(</span><span class="nx">record</span><span class="p">.</span><span class="nx">Current</span><span class="p">),</span> <span class="kc">true</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>未來換成資料庫時，context 可以傳給 query；error 可以包上資料庫錯誤。<a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a> 則等到一個 usecase 真的需要多筆寫入一致性時再設計。</p>
<h2 id="執行state-transition-測試鎖定規則">【執行】state transition 測試鎖定規則</h2>
<p>state transition 測試的核心目標是確認事件會產生正確狀態與 history。這類測試不需要 HTTP，也不需要 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">TestJobRepositoryApplyRecordsHistory</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="o">&amp;</span><span class="nx">JobRepository</span><span class="p">{</span><span class="nx">records</span><span class="p">:</span> <span class="nb">make</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="nx">JobRecord</span><span class="p">)}</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">startedAt</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Date</span><span class="p">(</span><span class="mi">2026</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">22</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">time</span><span class="p">.</span><span class="nx">UTC</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">err</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">Apply</span><span class="p">(</span><span class="nx">JobEvent</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="nx">JobID</span><span class="p">:</span>      <span class="s">&#34;job_1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">Type</span><span class="p">:</span>       <span class="s">&#34;job.started&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">OccurredAt</span><span class="p">:</span> <span class="nx">startedAt</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="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">11</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;apply event: %v&#34;</span><span class="p">,</span> <span class="nx">err</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></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nx">job</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="s">&#34;job_1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</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">16</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;job should exist&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="k">if</span> <span class="nx">job</span><span class="p">.</span><span class="nx">Status</span> <span class="o">!=</span> <span class="nx">JobStatusRunning</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">19</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;status = %q, want %q&#34;</span><span class="p">,</span> <span class="nx">job</span><span class="p">.</span><span class="nx">Status</span><span class="p">,</span> <span class="nx">JobStatusRunning</span><span class="p">)</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></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="nx">history</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">History</span><span class="p">(</span><span class="s">&#34;job_1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="nx">history</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">1</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">24</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;history length = %d, want 1&#34;</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">history</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個測試鎖定的是狀態規則，而不是鎖本身。</p>
<h2 id="執行copy-boundary-測試要嘗試破壞資料">【執行】copy boundary 測試要嘗試破壞資料</h2>
<p>copy boundary 測試的核心目標是證明呼叫端拿到的資料不能修改 repository 內部狀態。</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">TestJobRepositoryHistoryReturnsCopy</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="o">&amp;</span><span class="nx">JobRepository</span><span class="p">{</span><span class="nx">records</span><span class="p">:</span> <span class="nb">make</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="nx">JobRecord</span><span class="p">)}</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">occurredAt</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Date</span><span class="p">(</span><span class="mi">2026</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">22</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">time</span><span class="p">.</span><span class="nx">UTC</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">_</span> <span class="p">=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">Apply</span><span class="p">(</span><span class="nx">JobEvent</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="nx">JobID</span><span class="p">:</span>      <span class="s">&#34;job_1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">Type</span><span class="p">:</span>       <span class="s">&#34;job.started&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">OccurredAt</span><span class="p">:</span> <span class="nx">occurredAt</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="nx">history</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">History</span><span class="p">(</span><span class="s">&#34;job_1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nx">history</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">Status</span> <span class="p">=</span> <span class="nx">JobStatusFailed</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nx">again</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">History</span><span class="p">(</span><span class="s">&#34;job_1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</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">Status</span> <span class="o">!=</span> <span class="nx">JobStatusRunning</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</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 history was modified through returned slice&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">}</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="執行並發測試配合-race-detector">【執行】並發測試配合 race detector</h2>
<p>並發測試的核心目標是讓 race detector 執行到共享狀態路徑。測試本身可以只檢查不 panic 或基本結果，真正的 data race 由 <code>go test -race</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">TestJobRepositoryConcurrentAccess</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="o">&amp;</span><span class="nx">JobRepository</span><span class="p">{</span><span class="nx">records</span><span class="p">:</span> <span class="nb">make</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="nx">JobRecord</span><span class="p">)}</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="kd">var</span> <span class="nx">wg</span> <span class="nx">sync</span><span class="p">.</span><span class="nx">WaitGroup</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="nx">i</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="p">&lt;</span> <span class="mi">100</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="nx">wg</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">go</span> <span class="kd">func</span><span class="p">(</span><span class="nx">i</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="k">defer</span> <span class="nx">wg</span><span class="p">.</span><span class="nf">Done</span><span class="p">()</span>
</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">            <span class="nx">id</span> <span class="o">:=</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;job_%d&#34;</span><span class="p">,</span> <span class="nx">i</span><span class="o">%</span><span class="mi">10</span><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">repo</span><span class="p">.</span><span class="nf">Apply</span><span class="p">(</span><span class="nx">JobEvent</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">                <span class="nx">JobID</span><span class="p">:</span>      <span class="nx">id</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">                <span class="nx">Type</span><span class="p">:</span>       <span class="s">&#34;job.started&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">                <span class="nx">OccurredAt</span><span class="p">:</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Date</span><span class="p">(</span><span class="mi">2026</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">22</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">i</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">time</span><span class="p">.</span><span class="nx">UTC</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="nx">_</span><span class="p">,</span> <span class="nx">_</span> <span class="p">=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="nx">id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">            <span class="nx">_</span> <span class="p">=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">History</span><span class="p">(</span><span class="nx">id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="p">}(</span><span class="nx">i</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="nx">wg</span><span class="p">.</span><span class="nf">Wait</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">22</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-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">go <span class="nb">test</span> -race ./...</span></span></code></pre></div><p>race detector 只能檢查測試實際跑到的路徑。若並發讀寫沒有被測試覆蓋，它也不會發現問題。</p>
<h2 id="重構步驟">重構步驟</h2>
<p>從共享狀態外洩重構到安全邊界，可以按這個順序：</p>
<ol>
<li>找出所有直接讀寫 map、slice 或 projection 的地方。</li>
<li>建立 state owner 或 repository。</li>
<li>把最常用的寫入流程搬成方法。</li>
<li>在方法內加入 lock，保護完整不變式。</li>
<li>把讀取方法改成回傳 copy。</li>
<li>讓 handler、worker、publisher 改呼叫方法，不直接碰資料。</li>
<li>補 state transition 與 copy boundary 測試。</li>
<li>補並發測試並執行 <code>go test -race ./...</code>。</li>
</ol>
<p>不要一開始就重寫所有狀態模型。先把寫入集中，再逐步整理 state/projection 與資料庫邊界。</p>
<h2 id="設計檢查">設計檢查</h2>
<h3 id="檢查一加鎖後仍要保護回傳資料">檢查一：加鎖後仍要保護回傳資料</h3>
<p>鎖只保護鎖內操作。回傳內部 map 或 slice 後，呼叫端可以在鎖外修改資料，狀態邊界仍然失效。</p>
<h3 id="檢查二讀取鎖只保護讀取">檢查二：讀取鎖只保護讀取</h3>
<p><code>RLock</code> 只適合讀取。只要會修改 map、slice、pointer 指向的值或 struct 欄位，就必須使用 <code>Lock</code>。</p>
<h3 id="檢查三狀態副本需要明確-owner">檢查三：狀態副本需要明確 owner</h3>
<p>多份狀態副本會造成 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a> 混亂。handler 應該請求同一個 state owner 更新或查詢。</p>
<h3 id="檢查四持久化替換跟著需求前進">檢查四：持久化替換跟著需求前進</h3>
<p>狀態邊界是程式碼架構的責任；資料庫只負責持久化。把 memory repository 換成 ORM 只解決「資料存在哪裡」，沒有解決「誰有權利寫、怎麼寫才一致」。</p>
<p>引入資料庫後，清楚的寫入方法、交易語意、copy/DTO 邊界與測試仍要留在程式碼設計中。這些規則決定狀態如何被修改，不能交給資料庫連線本身代勞。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先處理 state owner、lock boundary 與 copy boundary；資料庫 transaction 與分散式一致性，會在下列章節再往外延伸：</p>
<ul>
<li><a href="/blog/go-advanced/07-distributed-operations/database-transactions/" data-link-title="7.1 資料庫 transaction 與 schema migration" data-link-desc="把 repository 邊界延伸到資料庫交易、migration 與一致性語意">Go 進階：資料庫 transaction 與 schema migration</a></li>
<li><a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">Go 進階：Source of Truth：狀態邊界</a></li>
<li><a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">Backend：資料庫與持久化</a></li>
</ul>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 repository、read model 與 shared state 的邊界；如果你要先回看語言教材，可以讀：</p>
<ul>
<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/state-fields/" data-link-title="6.3 如何擴展狀態投影欄位" data-link-desc="更新狀態模型、repository 與 API 輸出">Go：如何擴展狀態投影欄位</a></li>
<li><a href="/blog/go/07-refactoring/interface-boundary/" data-link-title="7.2 用 interface 隔離外部依賴" data-link-desc="建立小而穩定的測試替身">Go：用 interface 隔離外部依賴</a></li>
<li><a href="/blog/go/07-refactoring/domain-packages/" data-link-title="7.5 以 domain 重新整理 package" data-link-desc="讓 account、job、event、workflow 這類領域邊界在目錄中可見">Go：以 domain 重新整理 package</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>狀態管理重構的重點是建立資料擁有者。寫入集中在 repository 或 state owner，lock 保護完整不變式，讀取回傳 copy，handler 和 worker 只請求狀態更新。當狀態邊界清楚時，race detector 才有意義，未來換成資料庫也只是 adapter 變化，不會改變核心狀態規則。</p>
]]></content:encoded></item><item><title>模組四：並發模型</title><link>https://tarrragon.github.io/blog/go/04-concurrency/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/04-concurrency/</guid><description>&lt;p>Go 的並發不是只會寫 &lt;code>go func()&lt;/code>。Go 的並發模型包含工作如何啟動、資料如何傳遞、取消如何傳播、共享狀態如何保護。本模組從語言機制出發，再延伸到 worker、事件處理與網路服務情境。&lt;/p>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&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;a href="https://tarrragon.github.io/blog/go/04-concurrency/concurrency-model/" data-link-title="4.0 Go 並發模型總覽" data-link-desc="先理解 goroutine、OS thread 與 runtime 排程，再看高併發應用怎麼設計">4.0&lt;/a>&lt;/td>
 &lt;td>Go 並發模型總覽&lt;/td>
 &lt;td>看懂 goroutine、thread 與 runtime 的關係&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go/04-concurrency/goroutine/" data-link-title="4.1 goroutine：輕量並發工作" data-link-desc="用 goroutine 啟動並發工作，並設計清楚的退出條件">4.1&lt;/a>&lt;/td>
 &lt;td>goroutine：輕量並發工作&lt;/td>
 &lt;td>啟動並發工作並設計退出條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go/04-concurrency/channel/" data-link-title="4.2 channel：資料傳遞與 backpressure " data-link-desc="理解 channel 如何在 goroutine 之間傳遞資料並形成 backpressure ">4.2&lt;/a>&lt;/td>
 &lt;td>channel：資料傳遞與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a>&lt;/td>
 &lt;td>用 channel 在 goroutine 之間傳遞資料&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go/04-concurrency/select/" data-link-title="4.3 select：同時等待多種事件" data-link-desc="用 select 建立事件迴圈">4.3&lt;/a>&lt;/td>
 &lt;td>select：同時等待多種事件&lt;/td>
 &lt;td>實作 event loop&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go/04-concurrency/rwmutex/" data-link-title="4.4 sync.RWMutex：保護共享狀態" data-link-desc="用讀寫鎖保護共享狀態">4.4&lt;/a>&lt;/td>
 &lt;td>sync.RWMutex：保護共享狀態&lt;/td>
 &lt;td>安全讀寫共享資料&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go/04-concurrency/backpressure/" data-link-title="4.5 高併發控制與 backpressure " data-link-desc="用 bounded concurrency、backpressure 與 cancellation 控制 goroutine 的成長">4.5&lt;/a>&lt;/td>
 &lt;td>高併發控制與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a>&lt;/td>
 &lt;td>用 bounded concurrency 與 cancellation 控制壓力&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="本模組使用的範例主題">本模組使用的範例主題&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool&lt;/a> 與背景工作&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/producer/" data-link-title="Producer" data-link-desc="說明 producer 如何把工作、事件或資料送入後續處理路徑">producer&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a> 資料流&lt;/li>
&lt;li>ticker 與取消訊號&lt;/li>
&lt;li>共享狀態的讀寫鎖&lt;/li>
&lt;li>非阻塞送出與 backpressure&lt;/li>
&lt;/ul>
&lt;h2 id="學習時間">學習時間&lt;/h2>
&lt;p>預計 90-120 分鐘&lt;/p></description><content:encoded><![CDATA[<p>Go 的並發不是只會寫 <code>go func()</code>。Go 的並發模型包含工作如何啟動、資料如何傳遞、取消如何傳播、共享狀態如何保護。本模組從語言機制出發，再延伸到 worker、事件處理與網路服務情境。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/go/04-concurrency/concurrency-model/" data-link-title="4.0 Go 並發模型總覽" data-link-desc="先理解 goroutine、OS thread 與 runtime 排程，再看高併發應用怎麼設計">4.0</a></td>
          <td>Go 並發模型總覽</td>
          <td>看懂 goroutine、thread 與 runtime 的關係</td>
      </tr>
      <tr>
          <td><a href="/blog/go/04-concurrency/goroutine/" data-link-title="4.1 goroutine：輕量並發工作" data-link-desc="用 goroutine 啟動並發工作，並設計清楚的退出條件">4.1</a></td>
          <td>goroutine：輕量並發工作</td>
          <td>啟動並發工作並設計退出條件</td>
      </tr>
      <tr>
          <td><a href="/blog/go/04-concurrency/channel/" data-link-title="4.2 channel：資料傳遞與 backpressure " data-link-desc="理解 channel 如何在 goroutine 之間傳遞資料並形成 backpressure ">4.2</a></td>
          <td>channel：資料傳遞與 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a></td>
          <td>用 channel 在 goroutine 之間傳遞資料</td>
      </tr>
      <tr>
          <td><a href="/blog/go/04-concurrency/select/" data-link-title="4.3 select：同時等待多種事件" data-link-desc="用 select 建立事件迴圈">4.3</a></td>
          <td>select：同時等待多種事件</td>
          <td>實作 event loop</td>
      </tr>
      <tr>
          <td><a href="/blog/go/04-concurrency/rwmutex/" data-link-title="4.4 sync.RWMutex：保護共享狀態" data-link-desc="用讀寫鎖保護共享狀態">4.4</a></td>
          <td>sync.RWMutex：保護共享狀態</td>
          <td>安全讀寫共享資料</td>
      </tr>
      <tr>
          <td><a href="/blog/go/04-concurrency/backpressure/" data-link-title="4.5 高併發控制與 backpressure " data-link-desc="用 bounded concurrency、backpressure 與 cancellation 控制 goroutine 的成長">4.5</a></td>
          <td>高併發控制與 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a></td>
          <td>用 bounded concurrency 與 cancellation 控制壓力</td>
      </tr>
  </tbody>
</table>
<h2 id="本模組使用的範例主題">本模組使用的範例主題</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool</a> 與背景工作</li>
<li><a href="/blog/backend/knowledge-cards/producer/" data-link-title="Producer" data-link-desc="說明 producer 如何把工作、事件或資料送入後續處理路徑">producer</a> / <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 資料流</li>
<li>ticker 與取消訊號</li>
<li>共享狀態的讀寫鎖</li>
<li>非阻塞送出與 backpressure</li>
</ul>
<h2 id="學習時間">學習時間</h2>
<p>預計 90-120 分鐘</p>
]]></content:encoded></item><item><title>0.5 Go 和其他並發語言的差異</title><link>https://tarrragon.github.io/blog/go/00-philosophy/concurrency-language-position/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/00-philosophy/concurrency-language-position/</guid><description>&lt;p>Go 在並發語言中的核心定位是「用較低語言複雜度寫出可部署、可維護的高併發服務」。現代語言大多能處理並發；Go 的特色在於 goroutine、channel、context、標準庫與單一 binary 共同形成一套服務工程模型。&lt;/p>
&lt;p>語言比較的核心判斷是「哪一種並發模型會讓目前服務更容易寫清楚、部署簡單、長期維護」。Java、C#、Rust、Node.js、Python async、Erlang/Elixir 都能處理並發；這一章要比較的是它們各自把並發、生命週期與服務交付放在哪一種工程模型裡。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>區分「能並發」和「適合某種並發服務」的差異&lt;/li>
&lt;li>看懂 Go 的 goroutine 模型和 thread pool、async/await、actor model 的工程差異&lt;/li>
&lt;li>判斷 Go 與 Java/C#、Rust、Node.js、Python async、Erlang/Elixir 的選型邊界&lt;/li>
&lt;li>用工作負載、團隊維護成本與部署形態來比較語言&lt;/li>
&lt;li>把語言比較轉回工程問題，形成可檢查的選型依據&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察現代語言大多有並發能力">【觀察】現代語言大多有並發能力&lt;/h2>
&lt;p>並發能力已經是現代後端語言的基本能力。Java 有 thread pool、virtual threads 與成熟框架；C# 有 Task 與 async/await；Rust 有 async runtime 與底層控制；Node.js 和 Python 有事件迴圈與 async 生態；Erlang/Elixir 有 actor 與 supervision tree。&lt;/p>
&lt;p>因此，Go 的選型問題應該聚焦在「哪一種並發模型符合目前工作負載」。更有用的問題是：&lt;/p>
&lt;ol>
&lt;li>這個服務主要是大量等待 I/O，還是大量 CPU 計算？&lt;/li>
&lt;li>團隊希望用同步風格寫流程，還是接受 async callback / async function 傳播？&lt;/li>
&lt;li>服務是否需要大量長生命週期工作單元？&lt;/li>
&lt;li>部署是否重視單一 binary、啟動速度與少量 runtime 依賴？&lt;/li>
&lt;li>團隊是否更重視語言簡單度、企業框架、底層控制或容錯模型？&lt;/li>
&lt;/ol>
&lt;p>這些問題會把語言比較轉成工程比較。語言本身只是工具，工作負載與團隊約束才是選型依據。&lt;/p>
&lt;h2 id="判讀go-的差異是服務工程模型">【判讀】Go 的差異是服務工程模型&lt;/h2>
&lt;p>Go 的並發模型把「工作單位」表達成 goroutine，把「取消與逾時」表達成 context，把「協調訊號」表達成 channel 或同步原語。這讓大量等待型工作可以長得像普通函式流程。&lt;/p>
&lt;p>典型 Go 服務會長成這樣：&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">handle&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">request&lt;/span> &lt;span class="nx">Request&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">result&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">client&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Fetch&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">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ID&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">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"> 4&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;fetch data: %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"> 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="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">repository&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Save&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">result&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"> 8&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;save result: %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"> 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>&lt;/span>&lt;span class="line">&lt;span class="ln">11&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">12&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段程式沒有展示 goroutine，但它已經承接 Go 並發服務的核心語意：每個 request 有自己的 context，外部 I/O 接受取消，錯誤沿著呼叫鏈回傳。當這段流程被 HTTP handler、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool&lt;/a> 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a> 呼叫時，生命週期仍然清楚。&lt;/p>
&lt;p>Go 的優勢通常出現在三個地方：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>Go 的工程特性&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>並發工作單位&lt;/td>
 &lt;td>goroutine 成本低，適合大量等待型工作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>生命週期控制&lt;/td>
 &lt;td>&lt;code>context&lt;/code> 讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a>、cancel、request-scoped value 有共同傳遞方式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務交付&lt;/td>
 &lt;td>編譯成單一 binary，container、CLI、sidecar 與小型服務部署簡單&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表只是索引。下面幾節會把 Go 放到不同語言模型旁邊比較，重點是辨識每種模型適合的服務形狀。&lt;/p>
&lt;h2 id="判讀go-vs-java--c輕量服務模型與成熟平台模型">【判讀】Go vs Java / C#：輕量服務模型與成熟平台模型&lt;/h2>
&lt;p>Java 與 C# 的核心優勢是成熟平台、企業框架、完整工具鏈與大型組織生態。當系統需要完整 ORM、生態整合、企業身份驗證、複雜業務框架、長期平台治理時，Java / C# 經常是穩定選擇。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>大型銀行或保險系統，需要完整交易、稽核、權限與企業整合&lt;/li>
&lt;li>企業內部 ERP、CRM、供應鏈系統，需要成熟框架與長期治理&lt;/li>
&lt;li>使用 Spring、ASP.NET、Entity Framework 等框架已經形成團隊標準的組織&lt;/li>
&lt;/ul>
&lt;p>Go 的差異在於服務模型更輕。當服務主要是 HTTP/gRPC API、background worker、gateway、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> server、CLI 或基礎設施元件時，Go 可以用較少框架建立清楚邊界。程式啟動、部署、容器化和交接通常也比較直接。&lt;/p>
&lt;p>判斷問題可以這樣問：這個系統的主要價值在成熟企業平台與框架整合，還是在小型服務、簡單部署、清楚並發生命週期？前者常偏向 Java/C# 生態，後者常讓 Go 更有吸引力。&lt;/p>
&lt;h2 id="判讀go-vs-rust服務工程與底層控制">【判讀】Go vs Rust：服務工程與底層控制&lt;/h2>
&lt;p>Rust 的核心優勢是記憶體安全、零成本抽象、所有權模型與底層控制。當系統需要精細控制記憶體、避免 GC pause、處理高效能底層元件或安全敏感邊界時，Rust 的能力很強。&lt;/p></description><content:encoded><![CDATA[<p>Go 在並發語言中的核心定位是「用較低語言複雜度寫出可部署、可維護的高併發服務」。現代語言大多能處理並發；Go 的特色在於 goroutine、channel、context、標準庫與單一 binary 共同形成一套服務工程模型。</p>
<p>語言比較的核心判斷是「哪一種並發模型會讓目前服務更容易寫清楚、部署簡單、長期維護」。Java、C#、Rust、Node.js、Python async、Erlang/Elixir 都能處理並發；這一章要比較的是它們各自把並發、生命週期與服務交付放在哪一種工程模型裡。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>區分「能並發」和「適合某種並發服務」的差異</li>
<li>看懂 Go 的 goroutine 模型和 thread pool、async/await、actor model 的工程差異</li>
<li>判斷 Go 與 Java/C#、Rust、Node.js、Python async、Erlang/Elixir 的選型邊界</li>
<li>用工作負載、團隊維護成本與部署形態來比較語言</li>
<li>把語言比較轉回工程問題，形成可檢查的選型依據</li>
</ol>
<hr>
<h2 id="觀察現代語言大多有並發能力">【觀察】現代語言大多有並發能力</h2>
<p>並發能力已經是現代後端語言的基本能力。Java 有 thread pool、virtual threads 與成熟框架；C# 有 Task 與 async/await；Rust 有 async runtime 與底層控制；Node.js 和 Python 有事件迴圈與 async 生態；Erlang/Elixir 有 actor 與 supervision tree。</p>
<p>因此，Go 的選型問題應該聚焦在「哪一種並發模型符合目前工作負載」。更有用的問題是：</p>
<ol>
<li>這個服務主要是大量等待 I/O，還是大量 CPU 計算？</li>
<li>團隊希望用同步風格寫流程，還是接受 async callback / async function 傳播？</li>
<li>服務是否需要大量長生命週期工作單元？</li>
<li>部署是否重視單一 binary、啟動速度與少量 runtime 依賴？</li>
<li>團隊是否更重視語言簡單度、企業框架、底層控制或容錯模型？</li>
</ol>
<p>這些問題會把語言比較轉成工程比較。語言本身只是工具，工作負載與團隊約束才是選型依據。</p>
<h2 id="判讀go-的差異是服務工程模型">【判讀】Go 的差異是服務工程模型</h2>
<p>Go 的並發模型把「工作單位」表達成 goroutine，把「取消與逾時」表達成 context，把「協調訊號」表達成 channel 或同步原語。這讓大量等待型工作可以長得像普通函式流程。</p>
<p>典型 Go 服務會長成這樣：</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">handle</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">request</span> <span class="nx">Request</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">result</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">client</span><span class="p">.</span><span class="nf">Fetch</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">request</span><span class="p">.</span><span class="nx">ID</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">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;fetch data: %w&#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></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="nx">repository</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">result</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"> 8</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;save result: %w&#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></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段程式沒有展示 goroutine，但它已經承接 Go 並發服務的核心語意：每個 request 有自己的 context，外部 I/O 接受取消，錯誤沿著呼叫鏈回傳。當這段流程被 HTTP handler、<a href="/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool</a> 或 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 呼叫時，生命週期仍然清楚。</p>
<p>Go 的優勢通常出現在三個地方：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Go 的工程特性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>並發工作單位</td>
          <td>goroutine 成本低，適合大量等待型工作</td>
      </tr>
      <tr>
          <td>生命週期控制</td>
          <td><code>context</code> 讓 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a>、cancel、request-scoped value 有共同傳遞方式</td>
      </tr>
      <tr>
          <td>服務交付</td>
          <td>編譯成單一 binary，container、CLI、sidecar 與小型服務部署簡單</td>
      </tr>
  </tbody>
</table>
<p>這張表只是索引。下面幾節會把 Go 放到不同語言模型旁邊比較，重點是辨識每種模型適合的服務形狀。</p>
<h2 id="判讀go-vs-java--c輕量服務模型與成熟平台模型">【判讀】Go vs Java / C#：輕量服務模型與成熟平台模型</h2>
<p>Java 與 C# 的核心優勢是成熟平台、企業框架、完整工具鏈與大型組織生態。當系統需要完整 ORM、生態整合、企業身份驗證、複雜業務框架、長期平台治理時，Java / C# 經常是穩定選擇。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>大型銀行或保險系統，需要完整交易、稽核、權限與企業整合</li>
<li>企業內部 ERP、CRM、供應鏈系統，需要成熟框架與長期治理</li>
<li>使用 Spring、ASP.NET、Entity Framework 等框架已經形成團隊標準的組織</li>
</ul>
<p>Go 的差異在於服務模型更輕。當服務主要是 HTTP/gRPC API、background worker、gateway、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> server、CLI 或基礎設施元件時，Go 可以用較少框架建立清楚邊界。程式啟動、部署、容器化和交接通常也比較直接。</p>
<p>判斷問題可以這樣問：這個系統的主要價值在成熟企業平台與框架整合，還是在小型服務、簡單部署、清楚並發生命週期？前者常偏向 Java/C# 生態，後者常讓 Go 更有吸引力。</p>
<h2 id="判讀go-vs-rust服務工程與底層控制">【判讀】Go vs Rust：服務工程與底層控制</h2>
<p>Rust 的核心優勢是記憶體安全、零成本抽象、所有權模型與底層控制。當系統需要精細控制記憶體、避免 GC pause、處理高效能底層元件或安全敏感邊界時，Rust 的能力很強。</p>
<p>接近真實網路服務與系統元件的例子包括：</p>
<ul>
<li>高效能 proxy、資料處理引擎或邊緣運算元件</li>
<li>需要控制記憶體配置與延遲尖峰的低層服務</li>
<li>瀏覽器、資料庫、區塊鏈節點、嵌入式或安全敏感元件</li>
</ul>
<p>Go 的差異在於它把 GC、簡單型別、顯式錯誤處理和 goroutine 組成服務工程預設值。團隊通常可以更快建立 HTTP service、worker、<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 或內部平台工具。Go 會讓你接受 runtime 管理記憶體，換取較低心智負擔與較快服務交付。</p>
<p>判斷問題可以這樣問：主要風險是記憶體控制與極致效能，還是服務生命週期、部署、可讀性與交付速度？前者常讓 Rust 更合理，後者常讓 Go 更直接。</p>
<h2 id="判讀go-vs-nodejs--python-async同步風格與事件迴圈模型">【判讀】Go vs Node.js / Python async：同步風格與事件迴圈模型</h2>
<p>Node.js 與 Python async 的核心優勢是事件迴圈模型、豐富應用生態與快速產品整合。當服務以 I/O 為主，且團隊已經在 JavaScript、TypeScript 或 Python 生態中累積大量工具，async/await 可以建立高產能工作流。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>以 Next.js、Remix、FastAPI、Django 或 Flask 為核心的產品服務</li>
<li>需要快速串接 SaaS API、資料處理腳本、內容管理與前端整合的系統</li>
<li>團隊主要技能集中在 JavaScript/TypeScript 或 Python 的新產品</li>
</ul>
<p>Go 的差異在於 goroutine 讓等待型流程看起來更接近普通同步程式。當一個 request 需要呼叫多個下游、寫入狀態、處理 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a>、再把錯誤回傳，Go 通常能把控制流程維持在直線式函式中。多核心 CPU 使用、長時間 worker、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> 連線與 <a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">shutdown</a> 流程也能用同一套 goroutine/context 模型處理。</p>
<p>判斷問題可以這樣問：主要價值在前端/資料/腳本生態和快速整合，還是在長時間服務、清楚生命週期與單一部署產物？前者常偏向 Node.js 或 Python async 生態，後者常讓 Go 更自然。</p>
<h2 id="判讀go-vs-erlang--elixir通用服務與-actor-容錯模型">【判讀】Go vs Erlang / Elixir：通用服務與 actor 容錯模型</h2>
<p>Erlang / Elixir 的核心優勢是 actor model、supervision tree、熱更新文化與分散式容錯思想。當系統需要大量獨立 actor、強調隔離、復原和訊息傳遞時，BEAM 生態有非常成熟的模型。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>即時通訊與 presence 系統</li>
<li>大量獨立 session、room、process 的通訊服務</li>
<li>需要 supervision tree 管理故障復原的長時間系統</li>
</ul>
<p>Go 的差異在於它更像通用後端與基礎設施語言。你可以用 goroutine 和 channel 建立 actor-like 結構，但 Go 的標準模型更偏向明確組裝：handler、worker、repository、publisher、context cancellation、<a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a>。這讓 Go 在一般 API service、worker、CLI、gateway、sidecar、平台工具中更容易被多數後端團隊採用。</p>
<p>判斷問題可以這樣問：系統核心是否需要 actor supervision 與 fault-tolerant messaging 作為主要模型？如果答案是肯定的，Erlang / Elixir 值得認真評估；如果系統是一般後端服務與平台元件，Go 的採用門檻與部署模型通常更直接。</p>
<h2 id="策略用比較軸選語言">【策略】用比較軸選語言</h2>
<p>語言比較應該回到可觀察的工程條件。下面這張表可以當成選型索引：</p>
<table>
  <thead>
      <tr>
          <th>條件</th>
          <th>更常見的候選方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>大量 I/O、長連線、worker、簡單部署</td>
          <td>Go</td>
      </tr>
      <tr>
          <td>大型企業框架、成熟平台治理、完整商業系統生態</td>
          <td>Java / C#</td>
      </tr>
      <tr>
          <td>記憶體控制、底層效能、安全敏感元件</td>
          <td>Rust</td>
      </tr>
      <tr>
          <td>前端整合、SaaS 串接、資料腳本、產品快速整合</td>
          <td>Node.js / Python async</td>
      </tr>
      <tr>
          <td>actor、supervision、分散式容錯模型</td>
          <td>Erlang / Elixir</td>
      </tr>
  </tbody>
</table>
<p>這張表的用途是建立第一輪比較方向。實際選型還要看團隊經驗、既有系統、部署平台、觀測工具、人才供給與維護週期。</p>
<p>若一個服務需要同時支援 HTTP API、背景 worker、<a href="/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">Webhook</a> callback、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> 推送與簡單容器部署，Go 的整體組合很強。若一個產品主要依賴企業框架、動態產品流程、底層控制或 actor 容錯，其他語言可能更貼近主要問題。</p>
<h2 id="執行把語言比較寫成工程判斷">【執行】把語言比較寫成工程判斷</h2>
<p>好的語言比較結論應該包含工作負載、主要風險與取捨。語言名稱是結論，前面的工程條件才是判斷依據。</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">這個服務主要是 [webhook](/backend/knowledge-cards/webhook/) receiver、[queue](/backend/knowledge-cards/queue/) [consumer](/backend/knowledge-cards/consumer/) 與 [WebSocket](/backend/knowledge-cards/websocket/) 推送。
</span></span><span class="line"><span class="ln">2</span><span class="cl">主要風險是大量 I/O、[timeout](/backend/knowledge-cards/timeout/)、[backpressure](/go/backend/knowledge-cards/backpressure/) 與 [graceful shutdown](/backend/knowledge-cards/graceful-shutdown/)。
</span></span><span class="line"><span class="ln">3</span><span class="cl">Go 的 goroutine/context 模型和單一 binary 部署符合這些條件，所以 Go 是好候選。</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">這個系統主要是企業內部資料管理、權限、報表與工作流。
</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">Java / C# 生態可能比 Go 更貼近主問題。</span></span></code></pre></div><p>語言選型的核心輸出是「為什麼這個工作負載適合某個模型」。當比較句能說清楚工作負載、風險與取捨，團隊未來也能在條件改變時重新評估。</p>
<h2 id="和本模組的關係">和本模組的關係</h2>
<p>這一章承接 <a href="/blog/go/00-philosophy/selecting-go/" data-link-title="0.4 什麼時候選 Go" data-link-desc="用選型條件判斷 Go 是否適合高併發服務、背景工作與長連線場景">0.4 什麼時候選 Go</a>。0.4 先判斷工作場景是否適合 Go；0.5 再把 Go 放到其他並發語言旁邊，理解它的工程定位。</p>
<p>讀完本章後，可以回到：</p>
<ul>
<li><a href="/blog/go/00-philosophy/simplicity/" data-link-title="0.1 Go 的簡單哲學與認知負擔" data-link-desc="理解 Go 為什麼偏好顯式、直線流程與少量語法">Go 的簡單哲學與認知負擔</a></li>
<li><a href="/blog/go/00-philosophy/composition/" data-link-title="0.2 組合優先：小介面與明確依賴" data-link-desc="用小介面與 struct 組合取代大型繼承結構">組合優先：小介面與明確依賴</a></li>
<li><a href="/blog/go/04-concurrency/" data-link-title="模組四：並發模型" data-link-desc="從 goroutine、channel、select 與 RWMutex 理解 Go 並發模型">Go 並發模型</a></li>
</ul>
]]></content:encoded></item><item><title>4.5 高併發控制與 backpressure</title><link>https://tarrragon.github.io/blog/go/04-concurrency/backpressure/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/04-concurrency/backpressure/</guid><description>&lt;p>這一章處理的是一個比「會不會開 goroutine」更重要的問題：當系統真的進入高併發狀態時，怎麼讓工作量保持可控。Go 很容易啟動大量並發工作，但如果沒有邊界，goroutine、channel、下游連線與記憶體都會一起膨脹。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>理解 bounded concurrency 的用途&lt;/li>
&lt;li>用 semaphore 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool&lt;/a> 限制同時工作數&lt;/li>
&lt;li>看懂 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a> 為什麼能保護下游&lt;/li>
&lt;li>在併發流程中保留 cancellation 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a>&lt;/li>
&lt;li>辨認什麼時候該拒絕新工作&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察高併發需要容量邊界">【觀察】高併發需要容量邊界&lt;/h2>
&lt;p>goroutine 很便宜，但每個工作仍會消耗下游連線、記憶體、排隊時間與錯誤處理能力。當所有工作都直接丟進 &lt;code>go func()&lt;/code>，被放大的通常是：&lt;/p>
&lt;ul>
&lt;li>連線數&lt;/li>
&lt;li>記憶體&lt;/li>
&lt;li>排隊延遲&lt;/li>
&lt;li>下游壓力&lt;/li>
&lt;li>故障面積&lt;/li>
&lt;/ul>
&lt;p>高併發設計的第一原則是「可控」。系統需要知道同時有多少工作在跑、多少工作在排隊、滿載時如何回應。&lt;/p>
&lt;h2 id="判讀bounded-concurrency-是基本保護">【判讀】bounded concurrency 是基本保護&lt;/h2>
&lt;p>bounded concurrency 的核心規則是：同一時間只允許有限數量的工作進行。這可以用 worker pool、semaphore 或排隊系統達成。&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="nx">sem&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="kd">struct&lt;/span>&lt;span class="p">{},&lt;/span> &lt;span class="mi">16&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>&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">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="nx">sem&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="kd">struct&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&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="nx">job&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"> 7&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 class="o">&amp;lt;-&lt;/span>&lt;span class="nx">sem&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="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"> 9&lt;/span>&lt;span class="cl"> &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">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>這段程式限制同時只有 16 個工作在執行。當工作量暴增時，新的工作會自然排隊，而不是把整台機器一次推爆。&lt;/p>
&lt;h2 id="策略backpressure-保護的是下游">【策略】backpressure 保護的是下游&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a> 的核心規則是：當系統處理不過來時，不要無限累積工作。這可以表現成：&lt;/p>
&lt;ul>
&lt;li>channel 滿了就阻塞&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> 有上限&lt;/li>
&lt;li>goroutine pool 有上限&lt;/li>
&lt;li>佇列滿時直接拒絕請求&lt;/li>
&lt;/ul>
&lt;p>例如 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a>、event &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a> 或 background worker 如果沒有 backpressure ，輸入端一快，下游就會被放大成連鎖問題。&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="k">select&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">case&lt;/span> &lt;span class="nx">jobs&lt;/span> &lt;span class="o">&amp;lt;-&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">3&lt;/span>&lt;span class="cl"> &lt;span class="c1">// accepted&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">default&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 class="nx">ErrQueueFull&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;/code>&lt;/pre>&lt;/div>&lt;p>這種寫法的重點是明確表達滿載策略：系統在某些壓力下會拒絕新工作，因為保護整體健康比接住所有請求更重要。&lt;/p>
&lt;h2 id="執行cancellation-與-timeout-不能少">【執行】cancellation 與 timeout 不能少&lt;/h2>
&lt;p>bounded concurrency 只控制數量，不能解決卡死工作。每個工作都應該保留取消訊號與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a>，否則即使數量受限，資源也會被慢工作一直占著。&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="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">cancel&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WithTimeout&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&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="nf">cancel&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>&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">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">doWork&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">job&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">5&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">err&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;/code>&lt;/pre>&lt;/div>&lt;p>這樣可以讓每一筆工作都有自己的時間邊界，避免整體系統因單一慢點而拖垮。&lt;/p>
&lt;h2 id="判讀拒絕工作也是容量策略">【判讀】拒絕工作也是容量策略&lt;/h2>
&lt;p>拒絕新工作是保護容量邊界的一種策略。當以下條件成立時，拒絕通常比勉強接受更合理：&lt;/p>
&lt;ul>
&lt;li>queue 已滿&lt;/li>
&lt;li>下游連線池耗盡&lt;/li>
&lt;li>timeout 已明顯增加&lt;/li>
&lt;li>系統已進入明顯積壓&lt;/li>
&lt;/ul>
&lt;p>這時候回傳 &lt;code>429&lt;/code>、&lt;code>503&lt;/code> 或 domain-level rejection，往往比讓請求默默堆積更健康。&lt;/p></description><content:encoded><![CDATA[<p>這一章處理的是一個比「會不會開 goroutine」更重要的問題：當系統真的進入高併發狀態時，怎麼讓工作量保持可控。Go 很容易啟動大量並發工作，但如果沒有邊界，goroutine、channel、下游連線與記憶體都會一起膨脹。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>理解 bounded concurrency 的用途</li>
<li>用 semaphore 或 <a href="/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool</a> 限制同時工作數</li>
<li>看懂 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> 為什麼能保護下游</li>
<li>在併發流程中保留 cancellation 與 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a></li>
<li>辨認什麼時候該拒絕新工作</li>
</ol>
<hr>
<h2 id="觀察高併發需要容量邊界">【觀察】高併發需要容量邊界</h2>
<p>goroutine 很便宜，但每個工作仍會消耗下游連線、記憶體、排隊時間與錯誤處理能力。當所有工作都直接丟進 <code>go func()</code>，被放大的通常是：</p>
<ul>
<li>連線數</li>
<li>記憶體</li>
<li>排隊延遲</li>
<li>下游壓力</li>
<li>故障面積</li>
</ul>
<p>高併發設計的第一原則是「可控」。系統需要知道同時有多少工作在跑、多少工作在排隊、滿載時如何回應。</p>
<h2 id="判讀bounded-concurrency-是基本保護">【判讀】bounded concurrency 是基本保護</h2>
<p>bounded concurrency 的核心規則是：同一時間只允許有限數量的工作進行。這可以用 worker pool、semaphore 或排隊系統達成。</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">sem</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 class="mi">16</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="k">for</span> <span class="nx">_</span><span class="p">,</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="nx">sem</span> <span class="o">&lt;-</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="kd">func</span><span class="p">(</span><span class="nx">job</span> <span class="nx">Job</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="kd">func</span><span class="p">()</span> <span class="p">{</span> <span class="o">&lt;-</span><span class="nx">sem</span> <span class="p">}()</span>
</span></span><span class="line"><span class="ln"> 8</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"> 9</span><span class="cl">    <span class="p">}(</span><span class="nx">job</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段程式限制同時只有 16 個工作在執行。當工作量暴增時，新的工作會自然排隊，而不是把整台機器一次推爆。</p>
<h2 id="策略backpressure-保護的是下游">【策略】backpressure 保護的是下游</h2>
<p><a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> 的核心規則是：當系統處理不過來時，不要無限累積工作。這可以表現成：</p>
<ul>
<li>channel 滿了就阻塞</li>
<li><a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> 有上限</li>
<li>goroutine pool 有上限</li>
<li>佇列滿時直接拒絕請求</li>
</ul>
<p>例如 <a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a>、event <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 或 background worker 如果沒有 backpressure ，輸入端一快，下游就會被放大成連鎖問題。</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">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">case</span> <span class="nx">jobs</span> <span class="o">&lt;-</span> <span class="nx">job</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="c1">// accepted</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span> <span class="nx">ErrQueueFull</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這種寫法的重點是明確表達滿載策略：系統在某些壓力下會拒絕新工作，因為保護整體健康比接住所有請求更重要。</p>
<h2 id="執行cancellation-與-timeout-不能少">【執行】cancellation 與 timeout 不能少</h2>
<p>bounded concurrency 只控制數量，不能解決卡死工作。每個工作都應該保留取消訊號與 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a>，否則即使數量受限，資源也會被慢工作一直占著。</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">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">WithTimeout</span><span class="p">(</span><span class="nx">parent</span><span class="p">,</span> <span class="mi">3</span><span class="o">*</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">2</span><span class="cl"><span class="k">defer</span> <span class="nf">cancel</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">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">doWork</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">job</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">5</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這樣可以讓每一筆工作都有自己的時間邊界，避免整體系統因單一慢點而拖垮。</p>
<h2 id="判讀拒絕工作也是容量策略">【判讀】拒絕工作也是容量策略</h2>
<p>拒絕新工作是保護容量邊界的一種策略。當以下條件成立時，拒絕通常比勉強接受更合理：</p>
<ul>
<li>queue 已滿</li>
<li>下游連線池耗盡</li>
<li>timeout 已明顯增加</li>
<li>系統已進入明顯積壓</li>
</ul>
<p>這時候回傳 <code>429</code>、<code>503</code> 或 domain-level rejection，往往比讓請求默默堆積更健康。</p>
]]></content:encoded></item><item><title>8.5 Twitch：直播與聊天室系統</title><link>https://tarrragon.github.io/blog/go/08-case-studies/twitch/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/08-case-studies/twitch/</guid><description>&lt;p>Twitch 的案例幾乎就是 Go 教材裡高併發與即時系統的縮影。官方說法很直接：Go 被用在很多 busiest systems，上下文是 live video 與 chat，重點是 simplicity、safety、performance 與 readability。&lt;/p>
&lt;h2 id="你應該看什麼">你應該看什麼&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://go.dev/solutions/twitch">Twitch - Go’s march to low latency GC&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="這個案例告訴我們什麼">這個案例告訴我們什麼&lt;/h2>
&lt;ol>
&lt;li>Go 很適合低延遲、高事件量的即時系統。&lt;/li>
&lt;li>直播與聊天室會大量依賴長連線與狀態協調。&lt;/li>
&lt;li>可讀性在高壓力服務中仍然重要，因為維護者需要快速定位問題。&lt;/li>
&lt;/ol>
&lt;h2 id="可對照的公開原始碼">可對照的公開原始碼&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://go.dev/solutions/case-studies">Go case studies page&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>Twitch 的核心系統原始碼不是公開教學重點，所以這一章更適合把官方案例本身當成第一手材料，再回到你的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a>、channel 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a> 章節對照。&lt;/p></description><content:encoded><![CDATA[<p>Twitch 的案例幾乎就是 Go 教材裡高併發與即時系統的縮影。官方說法很直接：Go 被用在很多 busiest systems，上下文是 live video 與 chat，重點是 simplicity、safety、performance 與 readability。</p>
<h2 id="你應該看什麼">你應該看什麼</h2>
<ul>
<li><a href="https://go.dev/solutions/twitch">Twitch - Go’s march to low latency GC</a></li>
</ul>
<h2 id="這個案例告訴我們什麼">這個案例告訴我們什麼</h2>
<ol>
<li>Go 很適合低延遲、高事件量的即時系統。</li>
<li>直播與聊天室會大量依賴長連線與狀態協調。</li>
<li>可讀性在高壓力服務中仍然重要，因為維護者需要快速定位問題。</li>
</ol>
<h2 id="可對照的公開原始碼">可對照的公開原始碼</h2>
<ul>
<li><a href="https://go.dev/solutions/case-studies">Go case studies page</a></li>
</ul>
<p>Twitch 的核心系統原始碼不是公開教學重點，所以這一章更適合把官方案例本身當成第一手材料，再回到你的 <a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a>、channel 與 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> 章節對照。</p>
]]></content:encoded></item><item><title>1.5 bounded worker pool</title><link>https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/worker-pool/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/worker-pool/</guid><description>&lt;p>bounded &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool&lt;/a> 的核心責任是限制同時執行的工作量。goroutine 很便宜，但工作本身可能佔用 CPU、memory、file descriptor、外部 API quota 或資料庫連線；worker pool 讓容量限制成為程式設計的一部分。&lt;/p>
&lt;h2 id="預計補充內容">預計補充內容&lt;/h2>
&lt;p>這些工作量邊界會在下列章節展開：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/04-concurrency/goroutine/" data-link-title="4.1 goroutine：輕量並發工作" data-link-desc="用 goroutine 啟動並發工作，並設計清楚的退出條件">Go 入門：goroutine：背景工作與服務生命週期&lt;/a>：先理解 goroutine 的啟動和結束方式，才知道 worker pool 為什麼要限制並發數。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/04-concurrency/channel/" data-link-title="4.2 channel：資料傳遞與 backpressure " data-link-desc="理解 channel 如何在 goroutine 之間傳遞資料並形成 backpressure ">Go 入門：channel：事件流與 backpressure &lt;/a>：job channel 的容量和阻塞行為，會直接影響 pool 的整體策略。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/select-loop/" data-link-title="1.2 select loop 的生命週期設計" data-link-desc="理解長時間運行 goroutine 如何同時處理事件、ticker 與取消">Go 進階：select loop 的生命週期設計&lt;/a>：worker 的停止、排空與關閉，通常都要回到 select loop 來說明。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Backend：訊息佇列與事件傳遞&lt;/a>：真正跨 process 的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a>、retry 與 dead-letter 行為屬於這裡。&lt;/li>
&lt;/ul>
&lt;h2 id="本章不處理">本章不處理&lt;/h2>
&lt;p>本章先把單一 process 內的工作量上限、停止與排空講清楚；跨 process 的 consumer 與 retry 機制，會放在 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Backend：訊息佇列與事件傳遞&lt;/a>。&lt;/p>
&lt;h2 id="與-backend-教材的分工">與 Backend 教材的分工&lt;/h2>
&lt;p>本章只處理單一 Go process 內的工作量限制。跨 process 的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition&lt;/a>、[dead-letter &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a>](/go-advanced/backend/knowledge-cards/dead-letter-queue) 與重試政策會放在 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Backend：訊息佇列與事件傳遞&lt;/a>。&lt;/p>
&lt;h2 id="和-go-教材的關係">和 Go 教材的關係&lt;/h2>
&lt;p>這一章承接的是 goroutine、channel 與 select loop；如果你要先回看語言教材，可以讀：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/04-concurrency/goroutine/" data-link-title="4.1 goroutine：輕量並發工作" data-link-desc="用 goroutine 啟動並發工作，並設計清楚的退出條件">Go：goroutine：輕量並發工作&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/04-concurrency/channel/" data-link-title="4.2 channel：資料傳遞與 backpressure " data-link-desc="理解 channel 如何在 goroutine 之間傳遞資料並形成 backpressure ">Go：channel：資料傳遞與 backpressure &lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/select-loop/" data-link-title="1.2 select loop 的生命週期設計" data-link-desc="理解長時間運行 goroutine 如何同時處理事件、ticker 與取消">Go：select：同時等待多種事件&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/06-practical/new-background-worker/" data-link-title="6.4 如何新增背景工作流程" data-link-desc="接入 context、channel 與 shutdown">Go：如何新增背景工作流程&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>bounded <a href="/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool</a> 的核心責任是限制同時執行的工作量。goroutine 很便宜，但工作本身可能佔用 CPU、memory、file descriptor、外部 API quota 或資料庫連線；worker pool 讓容量限制成為程式設計的一部分。</p>
<h2 id="預計補充內容">預計補充內容</h2>
<p>這些工作量邊界會在下列章節展開：</p>
<ul>
<li><a href="/blog/go/04-concurrency/goroutine/" data-link-title="4.1 goroutine：輕量並發工作" data-link-desc="用 goroutine 啟動並發工作，並設計清楚的退出條件">Go 入門：goroutine：背景工作與服務生命週期</a>：先理解 goroutine 的啟動和結束方式，才知道 worker pool 為什麼要限制並發數。</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>：job channel 的容量和阻塞行為，會直接影響 pool 的整體策略。</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>：worker 的停止、排空與關閉，通常都要回到 select loop 來說明。</li>
<li><a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Backend：訊息佇列與事件傳遞</a>：真正跨 process 的 <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a>、retry 與 dead-letter 行為屬於這裡。</li>
</ul>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先把單一 process 內的工作量上限、停止與排空講清楚；跨 process 的 consumer 與 retry 機制，會放在 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Backend：訊息佇列與事件傳遞</a>。</p>
<h2 id="與-backend-教材的分工">與 Backend 教材的分工</h2>
<p>本章只處理單一 Go process 內的工作量限制。跨 process 的 <a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group</a>、<a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> <a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition</a>、[dead-letter <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a>](/go-advanced/backend/knowledge-cards/dead-letter-queue) 與重試政策會放在 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Backend：訊息佇列與事件傳遞</a>。</p>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 goroutine、channel 與 select loop；如果你要先回看語言教材，可以讀：</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-advanced/01-concurrency-patterns/select-loop/" data-link-title="1.2 select loop 的生命週期設計" data-link-desc="理解長時間運行 goroutine 如何同時處理事件、ticker 與取消">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>
]]></content:encoded></item><item><title>1.6 rate limiting 與 backpressure</title><link>https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/rate-limit/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/rate-limit/</guid><description>&lt;p>rate limiting 的核心責任是把過量輸入轉成可預期的服務行為。服務可以等待、排隊、拒絕、降級或取樣，但這些策略應由程式明確決定，而不是讓 goroutine、channel 或 memory 自行失控。&lt;/p>
&lt;h2 id="預計補充內容">預計補充內容&lt;/h2>
&lt;p>這些 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a> 邊界會在下列章節展開：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/04-concurrency/channel/" data-link-title="4.2 channel：資料傳遞與 backpressure " data-link-desc="理解 channel 如何在 goroutine 之間傳遞資料並形成 backpressure ">Go 入門：channel：事件流與 backpressure &lt;/a>：先理解 channel &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;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/non-blocking-send/" data-link-title="1.3 非阻塞送出與事件丟棄策略" data-link-desc="設計 channel 滿載時的服務行為">Go 進階：非阻塞送出與事件丟棄策略&lt;/a>：當系統必須在滿載時做出明確選擇，這裡會處理 drop、覆蓋與回錯的語意。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口&lt;/a>：跨節點流量治理、gateway 與 quota，屬於平台層責任。&lt;/li>
&lt;/ul>
&lt;h2 id="本章不處理">本章不處理&lt;/h2>
&lt;p>本章先處理單一 process 內的輸入控制與 backpressure ；跨節點流量治理、gateway 與 quota 的平台責任，會放在 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口&lt;/a>。&lt;/p>
&lt;h2 id="與-backend-教材的分工">與 Backend 教材的分工&lt;/h2>
&lt;p>本章只處理 Go process 內的速率控制。API gateway、load balancer、service mesh、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> quota 與跨節點流量治理會放在 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口&lt;/a>。&lt;/p>
&lt;h2 id="和-go-教材的關係">和 Go 教材的關係&lt;/h2>
&lt;p>這一章承接的是 channel backpressure 、non-blocking send 與 worker capacity；如果你要先回看語言教材，可以讀：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/04-concurrency/channel/" data-link-title="4.2 channel：資料傳遞與 backpressure " data-link-desc="理解 channel 如何在 goroutine 之間傳遞資料並形成 backpressure ">Go：channel：資料傳遞與 backpressure &lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/04-concurrency/select/" data-link-title="4.3 select：同時等待多種事件" data-link-desc="用 select 建立事件迴圈">Go：select：同時等待多種事件&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/non-blocking-send/" data-link-title="1.3 非阻塞送出與事件丟棄策略" data-link-desc="設計 channel 滿載時的服務行為">Go：非阻塞送出與事件丟棄策略&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">Go：bounded worker pool&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>rate limiting 的核心責任是把過量輸入轉成可預期的服務行為。服務可以等待、排隊、拒絕、降級或取樣，但這些策略應由程式明確決定，而不是讓 goroutine、channel 或 memory 自行失控。</p>
<h2 id="預計補充內容">預計補充內容</h2>
<p>這些 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> 邊界會在下列章節展開：</p>
<ul>
<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>：先理解 channel <a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 和等待機制，才知道限流不是只有一種做法。</li>
<li><a href="/blog/go-advanced/01-concurrency-patterns/non-blocking-send/" data-link-title="1.3 非阻塞送出與事件丟棄策略" data-link-desc="設計 channel 滿載時的服務行為">Go 進階：非阻塞送出與事件丟棄策略</a>：當系統必須在滿載時做出明確選擇，這裡會處理 drop、覆蓋與回錯的語意。</li>
<li><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口</a>：跨節點流量治理、gateway 與 quota，屬於平台層責任。</li>
</ul>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先處理單一 process 內的輸入控制與 backpressure ；跨節點流量治理、gateway 與 quota 的平台責任，會放在 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口</a>。</p>
<h2 id="與-backend-教材的分工">與 Backend 教材的分工</h2>
<p>本章只處理 Go process 內的速率控制。API gateway、load balancer、service mesh、<a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> quota 與跨節點流量治理會放在 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口</a>。</p>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 channel backpressure 、non-blocking send 與 worker capacity；如果你要先回看語言教材，可以讀：</p>
<ul>
<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-advanced/01-concurrency-patterns/non-blocking-send/" data-link-title="1.3 非阻塞送出與事件丟棄策略" data-link-desc="設計 channel 滿載時的服務行為">Go：非阻塞送出與事件丟棄策略</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>
</ul>
]]></content:encoded></item><item><title>5.6 並發行為測試</title><link>https://tarrragon.github.io/blog/go/05-error-testing/concurrency-test/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/05-error-testing/concurrency-test/</guid><description>&lt;p>並發測試的核心目標是驗證可觀察的同步行為，而不是猜測 goroutine 的執行順序。Go 的 goroutine 由 scheduler 安排，測試應該用 channel、context、WaitGroup 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 表達「什麼結果必須發生」。&lt;/p>
&lt;h2 id="並發測試應等待明確訊號">並發測試應等待明確訊號&lt;/h2>
&lt;p>並發程式的核心限制是執行順序不穩定。測試如果假設某個 goroutine 一定先跑，通常會變成偶發失敗。&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">sendAsync&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ch&lt;/span> &lt;span class="kd">chan&lt;/span>&lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="kt">string&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="nx">ch&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="s">&amp;#34;ready&amp;#34;&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 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">TestSendAsync&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">t&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">testing&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">T&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">ch&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nf">sendAsync&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ch&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&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"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="nx">got&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">ch&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">got&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s">&amp;#34;ready&amp;#34;&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="nx">t&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Fatalf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;message = %q, want %q&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">got&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;ready&amp;#34;&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 class="k">case&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">After&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&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="nx">t&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Fatalf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;timeout waiting for message&amp;#34;&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="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;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>select&lt;/code> 加 timeout 可以避免測試永久卡住。timeout 不應該用來證明程式正確，只是測試失敗時的保護機制。&lt;/p>
&lt;h2 id="channel-測試要驗證傳遞結果">channel 測試要驗證傳遞結果&lt;/h2>
&lt;p>channel 測試的核心問題是資料是否被送到預期位置。測試應該觀察 channel 收到的值，或觀察 channel 關閉後的狀態。&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">Produce&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ids&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="kd">chan&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">out&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&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"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&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"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nb">close&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">out&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">id&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">ids&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">out&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">id&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="p">}()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">out&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;/code>&lt;/pre>&lt;/div>&lt;p>這個函式回傳只讀 channel，呼叫端可以 range 讀取直到 channel 關閉。&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">TestProduce&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">t&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">testing&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">T&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">out&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">Produce&lt;/span>&lt;span class="p">([]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s">&amp;#34;a&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;b&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">got&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"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">id&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">out&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="nx">got&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">got&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">id&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="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">want&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s">&amp;#34;a&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;b&amp;#34;&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="k">if&lt;/span> &lt;span class="p">!&lt;/span>&lt;span class="nx">reflect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">DeepEqual&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">got&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">want&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">11&lt;/span>&lt;span class="cl"> &lt;span class="nx">t&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Fatalf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Produce() = %#v, want %#v&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">got&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">want&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>這個測試沒有使用 sleep。channel 關閉就是明確完成訊號，測試可以自然結束。&lt;/p>
&lt;h2 id="context-用來測試退出">context 用來測試退出&lt;/h2>
&lt;p>goroutine 退出測試的核心做法是提供可取消的 &lt;code>context.Context&lt;/code>，再等待函式發出完成訊號。沒有退出訊號的 goroutine 很難可靠測試。&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="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">done&lt;/span> &lt;span class="kd">chan&lt;/span>&lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="kd">struct&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="nb">close&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">done&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&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"> 5&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"> 6&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"> 7&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"> 8&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">jobs&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="c1">// process job&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="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;/code>&lt;/pre>&lt;/div>&lt;p>測試可以取消 context，然後確認 &lt;code>done&lt;/code> 被關閉。&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">TestRunWorkerStops&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">t&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">testing&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">T&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">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">cancel&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WithCancel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Background&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">jobs&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&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"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">done&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="kd">struct&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">go&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="p">,&lt;/span> &lt;span class="nx">jobs&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">done&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="nf">cancel&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&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">10&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">done&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="k">case&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">After&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&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="nx">t&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Fatalf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;worker did not stop&amp;#34;&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="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;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>done&lt;/code> channel 是測試與 goroutine 之間的完成協定。若程式沒有這種協定，測試只能猜測 goroutine 是否已經退出。&lt;/p>
&lt;h2 id="syncwaitgroup-適合等待一組工作完成">&lt;code>sync.WaitGroup&lt;/code> 適合等待一組工作完成&lt;/h2>
&lt;p>&lt;code>WaitGroup&lt;/code> 的核心用途是等待已知數量的 goroutine 完成。它適合 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out&lt;/a> 工作、批次處理與測試中需要等多個背景任務結束的情境。&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">ProcessAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">items&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">process&lt;/span> &lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">string&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">var&lt;/span> &lt;span class="nx">wg&lt;/span> &lt;span class="nx">sync&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">WaitGroup&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&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">item&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">items&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="nx">item&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">item&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">wg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&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">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"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">wg&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"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nf">process&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">item&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 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>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">wg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Wait&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;/code>&lt;/pre>&lt;/div>&lt;p>測試可以用 mutex 保護共享 slice，並在函式回傳後檢查結果。&lt;/p></description><content:encoded><![CDATA[<p>並發測試的核心目標是驗證可觀察的同步行為，而不是猜測 goroutine 的執行順序。Go 的 goroutine 由 scheduler 安排，測試應該用 channel、context、WaitGroup 與 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 表達「什麼結果必須發生」。</p>
<h2 id="並發測試應等待明確訊號">並發測試應等待明確訊號</h2>
<p>並發程式的核心限制是執行順序不穩定。測試如果假設某個 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">sendAsync</span><span class="p">(</span><span class="nx">ch</span> <span class="kd">chan</span><span class="o">&lt;-</span> <span class="kt">string</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="nx">ch</span> <span class="o">&lt;-</span> <span class="s">&#34;ready&#34;</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></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">TestSendAsync</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">ch</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kt">string</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></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nf">sendAsync</span><span class="p">(</span><span class="nx">ch</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">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="nx">got</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">ch</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">got</span> <span class="o">!=</span> <span class="s">&#34;ready&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</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;message = %q, want %q&#34;</span><span class="p">,</span> <span class="nx">got</span><span class="p">,</span> <span class="s">&#34;ready&#34;</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 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;timeout waiting for message&#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><code>select</code> 加 timeout 可以避免測試永久卡住。timeout 不應該用來證明程式正確，只是測試失敗時的保護機制。</p>
<h2 id="channel-測試要驗證傳遞結果">channel 測試要驗證傳遞結果</h2>
<p>channel 測試的核心問題是資料是否被送到預期位置。測試應該觀察 channel 收到的值，或觀察 channel 關閉後的狀態。</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">Produce</span><span class="p">(</span><span class="nx">ids</span> <span class="p">[]</span><span class="kt">string</span><span class="p">)</span> <span class="o">&lt;-</span><span class="kd">chan</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">out</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kt">string</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">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="k">defer</span> <span class="nb">close</span><span class="p">(</span><span class="nx">out</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">id</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">ids</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="nx">out</span> <span class="o">&lt;-</span> <span class="nx">id</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><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="k">return</span> <span class="nx">out</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個函式回傳只讀 channel，呼叫端可以 range 讀取直到 channel 關閉。</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">TestProduce</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">out</span> <span class="o">:=</span> <span class="nf">Produce</span><span class="p">([]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;a&#34;</span><span class="p">,</span> <span class="s">&#34;b&#34;</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="kd">var</span> <span class="nx">got</span> <span class="p">[]</span><span class="kt">string</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">for</span> <span class="nx">id</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">out</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="nx">got</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">got</span><span class="p">,</span> <span class="nx">id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <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="nx">want</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;a&#34;</span><span class="p">,</span> <span class="s">&#34;b&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">if</span> <span class="p">!</span><span class="nx">reflect</span><span class="p">.</span><span class="nf">DeepEqual</span><span class="p">(</span><span class="nx">got</span><span class="p">,</span> <span class="nx">want</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</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;Produce() = %#v, want %#v&#34;</span><span class="p">,</span> <span class="nx">got</span><span class="p">,</span> <span class="nx">want</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>這個測試沒有使用 sleep。channel 關閉就是明確完成訊號，測試可以自然結束。</p>
<h2 id="context-用來測試退出">context 用來測試退出</h2>
<p>goroutine 退出測試的核心做法是提供可取消的 <code>context.Context</code>，再等待函式發出完成訊號。沒有退出訊號的 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">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="kt">string</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="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="c1">// process job</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="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>測試可以取消 context，然後確認 <code>done</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">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="kt">string</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><code>done</code> channel 是測試與 goroutine 之間的完成協定。若程式沒有這種協定，測試只能猜測 goroutine 是否已經退出。</p>
<h2 id="syncwaitgroup-適合等待一組工作完成"><code>sync.WaitGroup</code> 適合等待一組工作完成</h2>
<p><code>WaitGroup</code> 的核心用途是等待已知數量的 goroutine 完成。它適合 <a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a> 工作、批次處理與測試中需要等多個背景任務結束的情境。</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">ProcessAll</span><span class="p">(</span><span class="nx">items</span> <span class="p">[]</span><span class="kt">string</span><span class="p">,</span> <span class="nx">process</span> <span class="kd">func</span><span class="p">(</span><span class="kt">string</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">var</span> <span class="nx">wg</span> <span class="nx">sync</span><span class="p">.</span><span class="nx">WaitGroup</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="nx">_</span><span class="p">,</span> <span class="nx">item</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">items</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="nx">item</span> <span class="o">:=</span> <span class="nx">item</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="nx">wg</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</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"> 8</span><span class="cl">            <span class="k">defer</span> <span class="nx">wg</span><span class="p">.</span><span class="nf">Done</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="nf">process</span><span class="p">(</span><span class="nx">item</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 class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nx">wg</span><span class="p">.</span><span class="nf">Wait</span><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>測試可以用 mutex 保護共享 slice，並在函式回傳後檢查結果。</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">TestProcessAll</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="kd">var</span> <span class="nx">mu</span> <span class="nx">sync</span><span class="p">.</span><span class="nx">Mutex</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="kd">var</span> <span class="nx">got</span> <span class="p">[]</span><span class="kt">string</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="nf">ProcessAll</span><span class="p">([]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;a&#34;</span><span class="p">,</span> <span class="s">&#34;b&#34;</span><span class="p">},</span> <span class="kd">func</span><span class="p">(</span><span class="nx">item</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="nx">mu</span><span class="p">.</span><span class="nf">Lock</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="nx">mu</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">got</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">got</span><span class="p">,</span> <span class="nx">item</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="nx">sort</span><span class="p">.</span><span class="nf">Strings</span><span class="p">(</span><span class="nx">got</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nx">want</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;a&#34;</span><span class="p">,</span> <span class="s">&#34;b&#34;</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="p">!</span><span class="nx">reflect</span><span class="p">.</span><span class="nf">DeepEqual</span><span class="p">(</span><span class="nx">got</span><span class="p">,</span> <span class="nx">want</span><span class="p">)</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;processed = %#v, want %#v&#34;</span><span class="p">,</span> <span class="nx">got</span><span class="p">,</span> <span class="nx">want</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="p">}</span></span></span></code></pre></div><p>因為 goroutine 執行順序不固定，測試先排序再比較。這表示測試關心「所有項目都有被處理」，不關心處理順序。</p>
<h2 id="race-detector-檢查共享狀態">race detector 檢查共享狀態</h2>
<p>共享狀態測試的核心風險是 data race。Go 提供 race detector，可以在測試時檢查多個 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">go <span class="nb">test</span> -race ./...</span></span></code></pre></div><p><code>-race</code> 會讓測試變慢，但能抓出許多一般斷言看不見的並發錯誤。只要程式有 goroutine 與共享資料，定期跑 race test 就很有價值。</p>
<p>race detector 不是邏輯正確性的完整證明。它能檢查資料競爭，但不能保證事件順序、<a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 策略或 timeout 行為都符合需求；這些仍然要靠明確測試案例。</p>
]]></content:encoded></item><item><title>3.7 context：取消、逾時與生命週期</title><link>https://tarrragon.github.io/blog/go/03-stdlib/context/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/03-stdlib/context/</guid><description>&lt;p>&lt;code>context.Context&lt;/code> 是 Go 用來傳遞取消訊號、逾時與 request-scoped 資訊的標準機制。它的核心用途是讓一串呼叫知道「這件工作是否應該停止」。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>理解 context 的取消語義&lt;/li>
&lt;li>使用 &lt;code>context.WithCancel&lt;/code>&lt;/li>
&lt;li>使用 &lt;code>context.WithTimeout&lt;/code>&lt;/li>
&lt;li>在 goroutine 和函式呼叫鏈中傳遞 context&lt;/li>
&lt;li>避免把 context 當成一般資料容器&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察context-表示工作生命週期">【觀察】context 表示工作生命週期&lt;/h2>
&lt;p>context 的核心規則是：被取消的 context 代表這件工作不應繼續進行。長時間工作應定期檢查 &lt;code>ctx.Done()&lt;/code>：&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">Run&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="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="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 class="nx">ctx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Err&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">default&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="nf">doOneStep&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="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;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>ctx.Done()&lt;/code> 是一個 channel。當 context 被取消或逾時，這個 channel 會被關閉。&lt;/p>
&lt;h2 id="判讀取消是由上層傳給下層">【判讀】取消是由上層傳給下層&lt;/h2>
&lt;p>context 的方向規則是：上層建立 context，下層接收 context；下層不應保存 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">main&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">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">cancel&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WithCancel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Background&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="nf">cancel&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="k">go&lt;/span> &lt;span class="nf">worker&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&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>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="nf">waitForSignal&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="nf">cancel&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>&lt;code>context.Background()&lt;/code> 是根 context。&lt;code>context.WithCancel&lt;/code> 回傳子 context 和 cancel 函式。當 &lt;code>cancel()&lt;/code> 被呼叫，所有使用該 context 的下層工作都會收到停止訊號。&lt;/p>
&lt;h2 id="策略逾時用-withtimeout主動停止用-withcancel">【策略】逾時用 WithTimeout，主動停止用 WithCancel&lt;/h2>
&lt;p>context 建立方式的核心規則是：不知道何時停止但需要手動停止，用 &lt;code>WithCancel&lt;/code>；有明確時間限制，用 &lt;code>WithTimeout&lt;/code>。&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="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">cancel&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WithTimeout&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Background&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&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="nf">cancel&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>&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">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">fetchData&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">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">5&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">err&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;/code>&lt;/pre>&lt;/div>&lt;p>下層函式應該接收 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">fetchData&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="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">req&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">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewRequestWithContext&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">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">MethodGet&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;https://example.com&amp;#34;&lt;/span>&lt;span class="p">,&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"> 3&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"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">err&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">resp&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">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">DefaultClient&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Do&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">req&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">err&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="k">defer&lt;/span> &lt;span class="nx">resp&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Close&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="k">return&lt;/span> &lt;span class="kc">nil&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;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 到達，HTTP request 會被取消。&lt;/p>
&lt;h2 id="執行讓背景-goroutine-有序退出">【執行】讓背景 goroutine 有序退出&lt;/h2>
&lt;p>背景 goroutine 的核心規則是：啟動時接收 context，迴圈中用 &lt;code>select&lt;/code> 同時等待工作與取消訊號。&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">worker&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">handleJob&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 有兩種退出路徑：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>退出原因&lt;/th>
 &lt;th>對應 case&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>上層取消&lt;/td>
 &lt;td>&lt;code>&amp;lt;-ctx.Done()&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>job channel 關閉&lt;/td>
 &lt;td>&lt;code>ok == false&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這比讓 goroutine 無限跑更安全，也比較容易測試。&lt;/p>
&lt;h2 id="設計檢查">設計檢查&lt;/h2>
&lt;h3 id="把-context-存進-struct">把 context 存進 struct&lt;/h3>
&lt;p>context 的生命週期屬於單次操作，不應長期存在 struct 裡。通常把 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="p">(&lt;/span>&lt;span class="nx">s&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Service&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Do&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">input&lt;/span> &lt;span class="nx">Input&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">error&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="忘記呼叫-cancel">忘記呼叫 cancel&lt;/h3>
&lt;p>&lt;code>WithCancel&lt;/code>、&lt;code>WithTimeout&lt;/code>、&lt;code>WithDeadline&lt;/code> 回傳的 cancel 應該被呼叫，釋放相關資源：&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="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">cancel&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WithTimeout&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&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="nf">cancel&lt;/span>&lt;span class="p">()&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="用-context-傳一般參數">用 context 傳一般參數&lt;/h3>
&lt;p>context value 適合 request-scoped metadata，例如 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request ID&lt;/a>。一般業務參數應放在函式參數或 struct 裡。&lt;/p></description><content:encoded><![CDATA[<p><code>context.Context</code> 是 Go 用來傳遞取消訊號、逾時與 request-scoped 資訊的標準機制。它的核心用途是讓一串呼叫知道「這件工作是否應該停止」。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>理解 context 的取消語義</li>
<li>使用 <code>context.WithCancel</code></li>
<li>使用 <code>context.WithTimeout</code></li>
<li>在 goroutine 和函式呼叫鏈中傳遞 context</li>
<li>避免把 context 當成一般資料容器</li>
</ol>
<hr>
<h2 id="觀察context-表示工作生命週期">【觀察】context 表示工作生命週期</h2>
<p>context 的核心規則是：被取消的 context 代表這件工作不應繼續進行。長時間工作應定期檢查 <code>ctx.Done()</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">Run</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="kt">error</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 class="nx">ctx</span><span class="p">.</span><span class="nf">Err</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="nf">doOneStep</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><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>ctx.Done()</code> 是一個 channel。當 context 被取消或逾時，這個 channel 會被關閉。</p>
<h2 id="判讀取消是由上層傳給下層">【判讀】取消是由上層傳給下層</h2>
<p>context 的方向規則是：上層建立 context，下層接收 context；下層不應保存 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">main</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="k">defer</span> <span class="nf">cancel</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">go</span> <span class="nf">worker</span><span class="p">(</span><span class="nx">ctx</span><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="nf">waitForSignal</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="nf">cancel</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><code>context.Background()</code> 是根 context。<code>context.WithCancel</code> 回傳子 context 和 cancel 函式。當 <code>cancel()</code> 被呼叫，所有使用該 context 的下層工作都會收到停止訊號。</p>
<h2 id="策略逾時用-withtimeout主動停止用-withcancel">【策略】逾時用 WithTimeout，主動停止用 WithCancel</h2>
<p>context 建立方式的核心規則是：不知道何時停止但需要手動停止，用 <code>WithCancel</code>；有明確時間限制，用 <code>WithTimeout</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="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">WithTimeout</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="mi">2</span><span class="o">*</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">2</span><span class="cl"><span class="k">defer</span> <span class="nf">cancel</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">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">fetchData</span><span class="p">(</span><span class="nx">ctx</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">5</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>下層函式應該接收 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">fetchData</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="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">req</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">http</span><span class="p">.</span><span class="nf">NewRequestWithContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">MethodGet</span><span class="p">,</span> <span class="s">&#34;https://example.com&#34;</span><span class="p">,</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">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></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">resp</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">http</span><span class="p">.</span><span class="nx">DefaultClient</span><span class="p">.</span><span class="nf">Do</span><span class="p">(</span><span class="nx">req</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">err</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="k">defer</span> <span class="nx">resp</span><span class="p">.</span><span class="nx">Body</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">return</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>當 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 到達，HTTP request 會被取消。</p>
<h2 id="執行讓背景-goroutine-有序退出">【執行】讓背景 goroutine 有序退出</h2>
<p>背景 goroutine 的核心規則是：啟動時接收 context，迴圈中用 <code>select</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">worker</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">handleJob</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 有兩種退出路徑：</p>
<table>
  <thead>
      <tr>
          <th>退出原因</th>
          <th>對應 case</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>上層取消</td>
          <td><code>&lt;-ctx.Done()</code></td>
      </tr>
      <tr>
          <td>job channel 關閉</td>
          <td><code>ok == false</code></td>
      </tr>
  </tbody>
</table>
<p>這比讓 goroutine 無限跑更安全，也比較容易測試。</p>
<h2 id="設計檢查">設計檢查</h2>
<h3 id="把-context-存進-struct">把 context 存進 struct</h3>
<p>context 的生命週期屬於單次操作，不應長期存在 struct 裡。通常把 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="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">Service</span><span class="p">)</span> <span class="nf">Do</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">input</span> <span class="nx">Input</span><span class="p">)</span> <span class="kt">error</span></span></span></code></pre></div><h3 id="忘記呼叫-cancel">忘記呼叫 cancel</h3>
<p><code>WithCancel</code>、<code>WithTimeout</code>、<code>WithDeadline</code> 回傳的 cancel 應該被呼叫，釋放相關資源：</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">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">WithTimeout</span><span class="p">(</span><span class="nx">parent</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">2</span><span class="cl"><span class="k">defer</span> <span class="nf">cancel</span><span class="p">()</span></span></span></code></pre></div><h3 id="用-context-傳一般參數">用 context 傳一般參數</h3>
<p>context value 適合 request-scoped metadata，例如 <a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request ID</a>。一般業務參數應放在函式參數或 struct 裡。</p>
]]></content:encoded></item><item><title>8.10 Go 的高併發服務案例</title><link>https://tarrragon.github.io/blog/go/08-case-studies/high-concurrency-services/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/08-case-studies/high-concurrency-services/</guid><description>&lt;p>高併發服務案例的核心判斷是「大量工作是否同時存在，且每個工作都需要清楚的生命週期」。Go 適合這類服務，因為 goroutine、channel、context、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 與標準網路庫可以共同描述工作如何開始、等待、取消與清理。&lt;/p>
&lt;h2 id="高併發型態">高併發型態&lt;/h2>
&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>大量 client、慢連線、斷線清理&lt;/td>
 &lt;td>Twitch、Stream、Cloudflare&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>網路代理與邊緣服務&lt;/td>
 &lt;td>timeout、連線管理、資源限制&lt;/td>
 &lt;td>Cloudflare、Kubernetes 生態工具&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>背景處理與 pipeline&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out&lt;/a>、排隊、取消、錯誤回報&lt;/td>
 &lt;td>PayPal、Dropbox&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>分散式資料服務&lt;/td>
 &lt;td>複製、一致性、節點協調&lt;/td>
 &lt;td>Cockroach Labs&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="長連線與即時推送先看-client-是否持續留在線上">長連線與即時推送：先看 client 是否持續留在線上&lt;/h3>
&lt;p>長連線服務的核心訊號是「request 結束後，server 仍然需要替 client 保留狀態」。聊天室、直播狀態、feed 更新與即時通知，都需要管理 client 註冊、訂閱、心跳、send &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a> 與清理流程。Go 的價值在於讓每條連線的讀取、寫入與取消責任能被拆成可讀的 goroutine 流程。&lt;/p>
&lt;p>對應章節：&lt;a href="https://tarrragon.github.io/blog/go-advanced/02-networking-websocket/" data-link-title="模組二：WebSocket 服務架構" data-link-desc="WebSocket client lifecycle、heartbeat、訂閱路由與慢客戶端管理">WebSocket 服務架構&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/go-advanced/02-networking-websocket/slow-client/" data-link-title="2.4 慢客戶端與 send buffer 管理" data-link-desc="控制推送佇列與記憶體風險">慢客戶端與 send buffer 管理&lt;/a>。&lt;/p>
&lt;h3 id="網路代理與邊緣服務先看邊界是否充滿-timeout">網路代理與邊緣服務：先看邊界是否充滿 timeout&lt;/h3>
&lt;p>網路代理與邊緣服務的核心訊號是「大量 I/O 邊界同時存在」。每個 request 都可能等待 DNS、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS&lt;/a>、上游服務、client body 或 downstream response。Go 的 &lt;code>net/http&lt;/code>、&lt;code>context&lt;/code> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">deadline&lt;/a> 設計讓 timeout 和 cancellation 可以沿著 request 傳遞。&lt;/p>
&lt;p>對應章節：&lt;a href="https://tarrragon.github.io/blog/go/03-stdlib/http-handler/" data-link-title="3.5 net/http 與 handler 設計" data-link-desc="用 net/http 建立健康檢查、API endpoint 與清楚的 handler 邊界">net/http 與 handler 設計&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/go/03-stdlib/context/" data-link-title="3.7 context：取消、逾時與生命週期" data-link-desc="用 context 傳遞取消、逾時與請求生命週期">context：取消、逾時與生命週期&lt;/a>。&lt;/p>
&lt;h3 id="背景處理與-pipeline先看工作是否可以從-request-中拆出">背景處理與 pipeline：先看工作是否可以從 request 中拆出&lt;/h3>
&lt;p>背景處理的核心訊號是「使用者請求只負責提交工作，真正處理需要在後面持續執行」。例如檔案轉換、通知寄送、資料同步、報表產生與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook&lt;/a> retry。Go 的 goroutine 和 channel 可以先建立單一 process 內的 worker 模型；當工作需要跨 process 保證時，再接到 Backend 的 message &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> 與 outbox 章節。&lt;/p>
&lt;p>對應章節：&lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/worker-pool/" data-link-title="1.5 bounded worker pool" data-link-desc="限制同時執行的 goroutine 數量，讓背景工作有明確容量邊界">bounded worker pool&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Backend：訊息佇列與事件傳遞&lt;/a>。&lt;/p>
&lt;h3 id="分散式資料服務先看狀態是否跨節點協調">分散式資料服務：先看狀態是否跨節點協調&lt;/h3>
&lt;p>分散式資料服務的核心訊號是「資料狀態需要跨節點維持一致」。這類服務會同時處理網路延遲、節點失效、複製、leader election、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction&lt;/a> 與觀測訊號。Go 提供的是可讀的並發與錯誤處理基礎；資料庫演算法、共識協定與持久化設計則需要專門章節或外部資料補足。&lt;/p>
&lt;p>對應章節：&lt;a href="https://tarrragon.github.io/blog/go-advanced/04-architecture-boundaries/source-of-truth/" data-link-title="4.3 Source of Truth：狀態邊界" data-link-desc="集中狀態更新、保護可變資料、設計查詢 projection">Source of Truth：狀態邊界&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/go-advanced/07-distributed-operations/database-transactions/" data-link-title="7.1 資料庫 transaction 與 schema migration" data-link-desc="把 repository 邊界延伸到資料庫交易、migration 與一致性語意">資料庫 transaction 與 schema migration&lt;/a>。&lt;/p>
&lt;h2 id="案例閱讀檢查">案例閱讀檢查&lt;/h2>
&lt;p>閱讀高併發案例時，先找出三個問題：工作如何被限制數量、失敗如何回到 owner、資源如何被清理。若案例只談速度而沒有談生命週期，就很難轉成可維護的 Go 設計。&lt;/p></description><content:encoded><![CDATA[<p>高併發服務案例的核心判斷是「大量工作是否同時存在，且每個工作都需要清楚的生命週期」。Go 適合這類服務，因為 goroutine、channel、context、<a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 與標準網路庫可以共同描述工作如何開始、等待、取消與清理。</p>
<h2 id="高併發型態">高併發型態</h2>
<table>
  <thead>
      <tr>
          <th>型態</th>
          <th>主要壓力</th>
          <th>相關案例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>長連線與即時推送</td>
          <td>大量 client、慢連線、斷線清理</td>
          <td>Twitch、Stream、Cloudflare</td>
      </tr>
      <tr>
          <td>網路代理與邊緣服務</td>
          <td>timeout、連線管理、資源限制</td>
          <td>Cloudflare、Kubernetes 生態工具</td>
      </tr>
      <tr>
          <td>背景處理與 pipeline</td>
          <td><a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a>、排隊、取消、錯誤回報</td>
          <td>PayPal、Dropbox</td>
      </tr>
      <tr>
          <td>分散式資料服務</td>
          <td>複製、一致性、節點協調</td>
          <td>Cockroach Labs</td>
      </tr>
  </tbody>
</table>
<h3 id="長連線與即時推送先看-client-是否持續留在線上">長連線與即時推送：先看 client 是否持續留在線上</h3>
<p>長連線服務的核心訊號是「request 結束後，server 仍然需要替 client 保留狀態」。聊天室、直播狀態、feed 更新與即時通知，都需要管理 client 註冊、訂閱、心跳、send <a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 與清理流程。Go 的價值在於讓每條連線的讀取、寫入與取消責任能被拆成可讀的 goroutine 流程。</p>
<p>對應章節：<a href="/blog/go-advanced/02-networking-websocket/" data-link-title="模組二：WebSocket 服務架構" data-link-desc="WebSocket client lifecycle、heartbeat、訂閱路由與慢客戶端管理">WebSocket 服務架構</a>、<a href="/blog/go-advanced/02-networking-websocket/slow-client/" data-link-title="2.4 慢客戶端與 send buffer 管理" data-link-desc="控制推送佇列與記憶體風險">慢客戶端與 send buffer 管理</a>。</p>
<h3 id="網路代理與邊緣服務先看邊界是否充滿-timeout">網路代理與邊緣服務：先看邊界是否充滿 timeout</h3>
<p>網路代理與邊緣服務的核心訊號是「大量 I/O 邊界同時存在」。每個 request 都可能等待 DNS、<a href="/blog/backend/knowledge-cards/tls-mtls/" data-link-title="TLS / mTLS" data-link-desc="說明傳輸加密與雙向憑證驗證如何保護跨邊界資料流">TLS</a>、上游服務、client body 或 downstream response。Go 的 <code>net/http</code>、<code>context</code> 與 <a href="/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">deadline</a> 設計讓 timeout 和 cancellation 可以沿著 request 傳遞。</p>
<p>對應章節：<a href="/blog/go/03-stdlib/http-handler/" data-link-title="3.5 net/http 與 handler 設計" data-link-desc="用 net/http 建立健康檢查、API endpoint 與清楚的 handler 邊界">net/http 與 handler 設計</a>、<a href="/blog/go/03-stdlib/context/" data-link-title="3.7 context：取消、逾時與生命週期" data-link-desc="用 context 傳遞取消、逾時與請求生命週期">context：取消、逾時與生命週期</a>。</p>
<h3 id="背景處理與-pipeline先看工作是否可以從-request-中拆出">背景處理與 pipeline：先看工作是否可以從 request 中拆出</h3>
<p>背景處理的核心訊號是「使用者請求只負責提交工作，真正處理需要在後面持續執行」。例如檔案轉換、通知寄送、資料同步、報表產生與 <a href="/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook</a> retry。Go 的 goroutine 和 channel 可以先建立單一 process 內的 worker 模型；當工作需要跨 process 保證時，再接到 Backend 的 message <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> 與 outbox 章節。</p>
<p>對應章節：<a href="/blog/go-advanced/01-concurrency-patterns/worker-pool/" data-link-title="1.5 bounded worker pool" data-link-desc="限制同時執行的 goroutine 數量，讓背景工作有明確容量邊界">bounded worker pool</a>、<a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Backend：訊息佇列與事件傳遞</a>。</p>
<h3 id="分散式資料服務先看狀態是否跨節點協調">分散式資料服務：先看狀態是否跨節點協調</h3>
<p>分散式資料服務的核心訊號是「資料狀態需要跨節點維持一致」。這類服務會同時處理網路延遲、節點失效、複製、leader election、<a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a> 與觀測訊號。Go 提供的是可讀的並發與錯誤處理基礎；資料庫演算法、共識協定與持久化設計則需要專門章節或外部資料補足。</p>
<p>對應章節：<a href="/blog/go-advanced/04-architecture-boundaries/source-of-truth/" data-link-title="4.3 Source of Truth：狀態邊界" data-link-desc="集中狀態更新、保護可變資料、設計查詢 projection">Source of Truth：狀態邊界</a>、<a href="/blog/go-advanced/07-distributed-operations/database-transactions/" data-link-title="7.1 資料庫 transaction 與 schema migration" data-link-desc="把 repository 邊界延伸到資料庫交易、migration 與一致性語意">資料庫 transaction 與 schema migration</a>。</p>
<h2 id="案例閱讀檢查">案例閱讀檢查</h2>
<p>閱讀高併發案例時，先找出三個問題：工作如何被限制數量、失敗如何回到 owner、資源如何被清理。若案例只談速度而沒有談生命週期，就很難轉成可維護的 Go 設計。</p>
]]></content:encoded></item><item><title>PostgreSQL MVCC + Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 &lt;em>MVCC + lock model&lt;/em> — PG 並行控制機制跟跟 MySQL lock-based 不同。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="pg-mvcc每次更新都-新增-tuple不改舊版">PG MVCC：每次更新都 &lt;em>新增 tuple&lt;/em>、不改舊版&lt;/h2>
&lt;p>PG 的並行控制核心是 &lt;em>Multi-Version Concurrency Control&lt;/em> — UPDATE 不修改原 row、是 &lt;em>新增&lt;/em> 一個 tuple version、舊 version 留在 table 直到 VACUUM 清理：&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">原 row: (id=1, status=&amp;#39;pending&amp;#39;, xmin=100, xmax=NULL)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ↓ UPDATE status=&amp;#39;shipped&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">新 tuple: (id=1, status=&amp;#39;shipped&amp;#39;, xmin=200, xmax=NULL)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">舊 tuple 標 xmax=200（不刪、給其他 transaction 看舊 version）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>xmin&lt;/code> / &lt;code>xmax&lt;/code> 是 &lt;em>creator transaction id&lt;/em> / &lt;em>destroyer transaction id&lt;/em>。每個 SELECT 用 &lt;em>snapshot&lt;/em>（含當下 active transaction list）判斷哪些 tuple 對自己可見：&lt;/p>
&lt;ul>
&lt;li>自己 transaction id &amp;gt; tuple.xmin 且 (tuple.xmax = NULL 或自己 transaction id &amp;lt; tuple.xmax) → 可見&lt;/li>
&lt;li>否則 → 看不到（過去 / 未來版本）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>結果&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;em>Readers 不 lock writers&lt;/em>：SELECT 看 snapshot、不 block UPDATE&lt;/li>
&lt;li>&lt;em>Writers 不 lock readers&lt;/em>：UPDATE 寫新 tuple、不影響正在跑的 SELECT snapshot&lt;/li>
&lt;li>&lt;em>Writers 只 lock 同一 row 的 writers&lt;/em>：兩個 UPDATE 同 row 才 conflict&lt;/li>
&lt;/ul>
&lt;p>跟 MySQL InnoDB &lt;em>lock-based&lt;/em>（&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/lock-contention/" data-link-title="MySQL Lock Contention：在 staging 重現的 deadlock、production 跑 6 個月才出現" data-link-desc="MySQL InnoDB 的 lock 是 row-level、但 *為什麼某些 row 莫名其妙也被 lock* 是 gap lock / next-key lock 設計造成的隱性行為。本文從一個 production case 開場（staging 重現 deadlock / production 6 個月後突然爆）、走 5 種 InnoDB lock 類型（record / gap / next-key / insert intention / auto-inc）、isolation level 對 lock 行為的決定性影響、deadlock detection / SHOW ENGINE INNODB STATUS 解讀、5 production 踩雷（gap lock 阻塞 INSERT / auto-inc lock contention / FK lock cascading / large transaction lock holding / READ COMMITTED 跟 binlog ROW 互動）">Lock Contention&lt;/a>）對比：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PG 在 OLTP 譜系的定位、本文聚焦 <em>MVCC + lock model</em> — PG 並行控制機制跟跟 MySQL lock-based 不同。</p></blockquote>
<hr>
<h2 id="pg-mvcc每次更新都-新增-tuple不改舊版">PG MVCC：每次更新都 <em>新增 tuple</em>、不改舊版</h2>
<p>PG 的並行控制核心是 <em>Multi-Version Concurrency Control</em> — UPDATE 不修改原 row、是 <em>新增</em> 一個 tuple version、舊 version 留在 table 直到 VACUUM 清理：</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">原 row:    (id=1, status=&#39;pending&#39;, xmin=100, xmax=NULL)
</span></span><span class="line"><span class="ln">2</span><span class="cl">                 ↓ UPDATE status=&#39;shipped&#39;
</span></span><span class="line"><span class="ln">3</span><span class="cl">新 tuple:  (id=1, status=&#39;shipped&#39;, xmin=200, xmax=NULL)
</span></span><span class="line"><span class="ln">4</span><span class="cl">舊 tuple 標 xmax=200（不刪、給其他 transaction 看舊 version）</span></span></code></pre></div><p><code>xmin</code> / <code>xmax</code> 是 <em>creator transaction id</em> / <em>destroyer transaction id</em>。每個 SELECT 用 <em>snapshot</em>（含當下 active transaction list）判斷哪些 tuple 對自己可見：</p>
<ul>
<li>自己 transaction id &gt; tuple.xmin 且 (tuple.xmax = NULL 或自己 transaction id &lt; tuple.xmax) → 可見</li>
<li>否則 → 看不到（過去 / 未來版本）</li>
</ul>
<p><strong>結果</strong>：</p>
<ul>
<li><em>Readers 不 lock writers</em>：SELECT 看 snapshot、不 block UPDATE</li>
<li><em>Writers 不 lock readers</em>：UPDATE 寫新 tuple、不影響正在跑的 SELECT snapshot</li>
<li><em>Writers 只 lock 同一 row 的 writers</em>：兩個 UPDATE 同 row 才 conflict</li>
</ul>
<p>跟 MySQL InnoDB <em>lock-based</em>（<a href="/blog/backend/01-database/vendors/mysql/lock-contention/" data-link-title="MySQL Lock Contention：在 staging 重現的 deadlock、production 跑 6 個月才出現" data-link-desc="MySQL InnoDB 的 lock 是 row-level、但 *為什麼某些 row 莫名其妙也被 lock* 是 gap lock / next-key lock 設計造成的隱性行為。本文從一個 production case 開場（staging 重現 deadlock / production 6 個月後突然爆）、走 5 種 InnoDB lock 類型（record / gap / next-key / insert intention / auto-inc）、isolation level 對 lock 行為的決定性影響、deadlock detection / SHOW ENGINE INNODB STATUS 解讀、5 production 踩雷（gap lock 阻塞 INSERT / auto-inc lock contention / FK lock cascading / large transaction lock holding / READ COMMITTED 跟 binlog ROW 互動）">Lock Contention</a>）對比：</p>
<ul>
<li>MySQL：SELECT FOR UPDATE 用 gap lock 防 phantom、deadlock 機率高</li>
<li>PG：MVCC + snapshot 自然防 phantom（read 看 snapshot）、deadlock 少</li>
</ul>
<p>但 PG 代價是 <em>VACUUM 治理</em> — dead tuple 不清理會佔 disk + 影響 query 效率。詳見 <a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">Autovacuum Tuning</a>。</p>
<h2 id="pg-4-種-lock">PG 4 種 lock</h2>
<p>PG 仍有 lock、但場景跟 MySQL 不同：</p>
<h3 id="1-row-level-lock--主要由-update--delete--select-for-update-取">1. Row-level lock — 主要由 UPDATE / DELETE / SELECT FOR UPDATE 取</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="k">UPDATE</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- 對 id=100 row 加 ROW EXCLUSIVE lock
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">-- 其他 transaction 試 UPDATE / DELETE id=100 必須等</span></span></span></code></pre></div><p>Row-level lock <em>不 block reader</em>（SELECT 看 snapshot、不檢查 lock）。</p>
<h3 id="2-table-level-lock--ddl-跟少數-select-for-場景">2. Table-level lock — DDL 跟少數 SELECT FOR 場景</h3>
<p>PG 有 8 種 table lock mode、嚴重程度遞增：</p>
<table>
  <thead>
      <tr>
          <th>Mode</th>
          <th>行為</th>
          <th>衝突</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ACCESS SHARE</td>
          <td>SELECT 跑</td>
          <td>跟 ACCESS EXCLUSIVE 衝突</td>
      </tr>
      <tr>
          <td>ROW SHARE</td>
          <td>SELECT FOR UPDATE / FOR SHARE</td>
          <td>跟 EXCLUSIVE 衝突</td>
      </tr>
      <tr>
          <td>ROW EXCLUSIVE</td>
          <td>UPDATE / DELETE / INSERT</td>
          <td>跟 SHARE 衝突</td>
      </tr>
      <tr>
          <td>SHARE UPDATE EXCLUSIVE</td>
          <td>VACUUM / ANALYZE / CREATE INDEX CONCURRENTLY</td>
          <td>跟同 mode + 高 mode 衝突</td>
      </tr>
      <tr>
          <td>SHARE</td>
          <td>CREATE INDEX（non-concurrent）</td>
          <td>跟 ROW EXCLUSIVE 衝突</td>
      </tr>
      <tr>
          <td>SHARE ROW EXCLUSIVE</td>
          <td>CREATE TRIGGER / 某些 ALTER</td>
          <td>跟 ROW EXCLUSIVE 衝突</td>
      </tr>
      <tr>
          <td>EXCLUSIVE</td>
          <td>REFRESH MATERIALIZED VIEW</td>
          <td>跟所有 + 自身衝突</td>
      </tr>
      <tr>
          <td>ACCESS EXCLUSIVE</td>
          <td>DROP / ALTER TABLE / VACUUM FULL</td>
          <td>跟所有衝突</td>
      </tr>
  </tbody>
</table>
<p>DDL（ALTER / DROP）拿 ACCESS EXCLUSIVE、跟所有衝突。Production 跑 ALTER 必須短時間或走 <a href="/blog/backend/01-database/vendors/postgresql/online-schema-change/" data-link-title="PostgreSQL Online Schema Change：先用 ALTER 內建特性、不能解才 pg_repack / pg-osc" data-link-desc="PostgreSQL ALTER TABLE 對多數變更已是 *fast catalog-only*（add column nullable / drop column / 改 default），不必走 ghost table tool。本文走 PG 內建 fast DDL 行為、何時必須走 pg_repack / pg-osc、兩工具機制對比（trigger-based vs WAL-shipping）、配置 step-by-step、5 production 踩雷（lock 升級 / VACUUM FULL 誤用 / pg_repack version mismatch / concurrent index 失敗清理 / generated stored column 不能 online）、跟 MySQL gh-ost / pt-osc sibling 對比">Online Schema Change</a>。</p>
<h3 id="3-advisory-lock--application-自己控">3. Advisory lock — Application 自己控</h3>
<p>PG 提供 <em>advisory lock</em> 給 application 用、不關 row / table 結構：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Session 1
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_advisory_lock</span><span class="p">(</span><span class="mi">12345</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- 跑 critical section
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_advisory_unlock</span><span class="p">(</span><span class="mi">12345</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">-- Session 2
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_try_advisory_lock</span><span class="p">(</span><span class="mi">12345</span><span class="p">);</span><span class="w">  </span><span class="c1">-- 試取、不阻塞、返回 false</span></span></span></code></pre></div><p>用途：</p>
<ul>
<li>Application-level 互斥（如：cron job 同時只跑一個）</li>
<li>跨 connection 同步（PG-managed mutex）</li>
<li>Distributed transaction coordinator（lightweight）</li>
</ul>
<p>跟 row lock 不同：advisory lock 不關 row、application 自定義 lock ID 語義。</p>
<h3 id="4-predicate-lock--serializable-isolation-才用">4. Predicate lock — SERIALIZABLE isolation 才用</h3>
<p>PG SERIALIZABLE 用 <em>Serializable Snapshot Isolation (SSI)</em>、追蹤 <em>predicate</em>（query 條件）而不是 <em>row</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="k">TRANSACTION</span><span class="w"> </span><span class="k">ISOLATION</span><span class="w"> </span><span class="k">LEVEL</span><span class="w"> </span><span class="k">SERIALIZABLE</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- Predicate lock 紀錄這個 query 看了哪些 predicate
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;pending&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- 其他 transaction INSERT pending order
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">-- 提交時：PG 偵測 anomaly、rollback 之一
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="k">COMMIT</span><span class="p">;</span></span></span></code></pre></div><p>跟 MySQL gap lock 不同：</p>
<ul>
<li>MySQL gap lock：<em>pre-lock</em>、防 phantom 在 query 期間</li>
<li>PG predicate lock：<em>post-detect</em>、commit 時偵測 anomaly、退回 transaction</li>
</ul>
<p>PG SSI 對 <em>寫入吞吐影響低</em>（不 pre-lock）、但 <em>transaction rollback 機率高</em>（要 application retry）。</p>
<h2 id="pg-預設-isolationread-committed">PG 預設 isolation：READ COMMITTED</h2>
<p>PG 預設 READ COMMITTED、跟 MySQL InnoDB 預設 REPEATABLE READ 不同：</p>
<table>
  <thead>
      <tr>
          <th>Isolation</th>
          <th>PG 行為</th>
          <th>MySQL InnoDB 對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>READ UNCOMMITTED</td>
          <td>PG 視為 READ COMMITTED（不真的支援 dirty read）</td>
          <td>MySQL 真支援</td>
      </tr>
      <tr>
          <td>READ COMMITTED</td>
          <td>每 statement 看當下 committed snapshot（PG 預設）</td>
          <td>一致</td>
      </tr>
      <tr>
          <td>REPEATABLE READ</td>
          <td>Transaction 內 fixed snapshot（純 MVCC）</td>
          <td>MVCC snapshot + gap lock 防 phantom（兩者都 MVCC、差在 phantom 防護機制：PG 靠 snapshot version visibility、InnoDB 加 gap lock pre-lock 範圍）</td>
      </tr>
      <tr>
          <td>SERIALIZABLE</td>
          <td>SSI、commit 時偵測 anomaly</td>
          <td>強 lock + gap</td>
      </tr>
  </tbody>
</table>
<p><strong>對 application code 含意</strong>：</p>
<ul>
<li>PG REPEATABLE READ 對 <em>寫入吞吐</em> 影響低（不 pre-lock、只 retry）</li>
<li>沒 gap lock → INSERT 不被 lock-induced 阻塞</li>
<li>Deadlock 機率比 MySQL 低數量級</li>
</ul>
<p>實務 PG production：用預設 READ COMMITTED 即可、SERIALIZABLE 留給 <em>strict consistency 需求</em>（金融 / 訂單）但接受 retry。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-idle-transaction-卡-vacuum--bloat-暴增">1. Idle transaction 卡 vacuum — Bloat 暴增</h3>
<p>PG MVCC 仰賴 <em>VACUUM 清理 dead tuple</em>。VACUUM 只清理 <em>沒 active transaction 看得到的 dead tuple</em>。如果有 <em>idle in transaction</em> session 持續開著（application connection pool 連線忘關 transaction）、VACUUM 看不到 <em>該 transaction snapshot 之後的 dead tuple</em>、累積 bloat。</p>
<p>修法：</p>
<ul>
<li>監控 <code>pg_stat_activity</code> 看 <code>state = 'idle in transaction'</code> 持續時間</li>
<li>設 <code>idle_in_transaction_session_timeout = '5min'</code> — 超時 PG 自動 kill 該 session</li>
<li>Application connection pool 配置 <em>不留 transaction 開著</em>（如：pgBouncer transaction pool 自動 commit / rollback）</li>
</ul>
<h3 id="2-select-for-update-跨-transaction--application-retry-麻煩">2. SELECT FOR UPDATE 跨 transaction — Application retry 麻煩</h3>
<p>跟 MySQL 不同：PG SELECT FOR UPDATE 不會 <em>block 其他 SELECT</em>（讀仍可繼續）、但 <em>block 其他 UPDATE / FOR UPDATE</em>。若 application 在 transaction 內 SELECT FOR UPDATE、其他 transaction 等。</p>
<p>如果 application 設計 <em>跨 transaction 持 lock</em>（如：取 lock + return UI + 等用戶操作 + commit）、容易撞 idle in transaction 跟其他 transaction wait。</p>
<p>修法：</p>
<ul>
<li><em>Transaction 短</em>：取 FOR UPDATE → 立刻處理 → commit、不跨 user interaction</li>
<li>跨 user interaction 用 <em>advisory lock</em> 或 application-level state machine、不依賴 row lock</li>
</ul>
<h3 id="3-advisory-lock-沒釋放--session-結束才自動釋放">3. Advisory lock 沒釋放 — Session 結束才自動釋放</h3>
<p><code>pg_advisory_lock()</code> 拿了、沒 <code>pg_advisory_unlock()</code>、lock 直到 <em>session 結束</em> 才自動釋放。Connection pool 重複使用同 connection、可能繼承前面留的 lock。</p>
<p>修法：</p>
<ul>
<li>用 <code>pg_advisory_lock</code> 必 <code>try/finally pg_advisory_unlock</code></li>
<li>或用 <em>session-level</em> 用 transaction-scoped：<code>pg_advisory_xact_lock()</code> — commit / rollback 自動釋放</li>
<li>監控 <code>pg_locks</code> 看 advisory lock count、長期累積是警訊</li>
</ul>
<h3 id="4-bloat-不只是-vacuum-沒跑是-active-transaction-阻擋-vacuum">4. Bloat 不只是 vacuum 沒跑、是 <em>active transaction 阻擋 vacuum</em></h3>
<p>第 #1 點延伸：vacuum 已跑、但 bloat 仍持續成長、原因不是 vacuum 不夠、是 <em>active transaction 阻擋 vacuum 看 dead tuple</em>。</p>
<p>修法：</p>
<ul>
<li>不只看 <code>last_vacuum</code>、看 <em>VACUUM 跑了但沒收回多少</em></li>
<li><code>SELECT * FROM pg_stat_progress_vacuum</code> 看 VACUUM 進度</li>
<li><code>SELECT * FROM pg_stat_activity WHERE backend_xmin IS NOT NULL ORDER BY backend_xmin</code> — 看誰阻擋 vacuum</li>
<li>詳見 <a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">Autovacuum Tuning</a></li>
</ul>
<h3 id="5-serializable-下-transaction-rollback--application-必須-retry">5. SERIALIZABLE 下 transaction rollback — Application 必須 retry</h3>
<p><code>SET TRANSACTION ISOLATION LEVEL SERIALIZABLE</code> 後、PG SSI 偵測到 anomaly 會 <em>rollback transaction</em>、application 看到 <code>serialization failure</code>、必須 retry。</p>
<p>對 <em>不知道要 retry</em> 的 application、SERIALIZABLE 變 production bug。</p>
<p>修法：</p>
<ul>
<li>Application code 加 <em>retry middleware</em>：catch <code>SQLSTATE 40001 (serialization_failure)</code> → exponential backoff retry</li>
<li>不必所有 transaction 走 SERIALIZABLE — 只對 <em>strict consistency 需求</em> 場景 set</li>
<li>高並發 SERIALIZABLE workload 容易 rollback storm、考慮拆 transaction 縮短時間</li>
</ul>
<h2 id="觀測-metric">觀測 metric</h2>
<p>Production 監控：</p>
<ul>
<li><code>pg_stat_activity</code>：active session / idle in transaction / wait_event</li>
<li><code>pg_locks</code>：當前 lock 列表、用 join 看誰 block 誰</li>
<li><code>pg_stat_database.deadlocks</code>：deadlock 計數（PG 較低、但仍要監控）</li>
<li><code>pg_stat_user_tables.n_dead_tup</code> / <code>n_live_tup</code>：dead tuple 比例 — bloat 指標</li>
<li><code>pg_stat_progress_vacuum</code>：VACUUM 進度</li>
</ul>
<h2 id="跟-mysql-lock-model-對比">跟 MySQL Lock Model 對比</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>PG MVCC</th>
          <th>MySQL InnoDB Lock</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主要機制</td>
          <td>MVCC + snapshot</td>
          <td>Lock-based + MVCC mixed</td>
      </tr>
      <tr>
          <td>Readers vs Writers</td>
          <td>不互 block</td>
          <td>預設 RR 下 gap lock 影響</td>
      </tr>
      <tr>
          <td>Deadlock 機率</td>
          <td>低（無 gap lock）</td>
          <td>中-高（gap lock 主要來源）</td>
      </tr>
      <tr>
          <td>Phantom 防護</td>
          <td>Snapshot 自然防 + SSI predicate lock</td>
          <td>Gap lock 預先 lock</td>
      </tr>
      <tr>
          <td>預設 isolation</td>
          <td>READ COMMITTED</td>
          <td>REPEATABLE READ</td>
      </tr>
      <tr>
          <td>成本</td>
          <td>Dead tuple + VACUUM 治理</td>
          <td>Lock contention 治理</td>
      </tr>
      <tr>
          <td>Application code</td>
          <td>SERIALIZABLE 需 retry</td>
          <td>寫得不錯多數時 OK</td>
      </tr>
  </tbody>
</table>
<p>兩者解決同一問題（並行控制）、用不同策略。PG 用 <em>空間換時間</em>（保留多版本 tuple、讀寫不互鎖、但需 VACUUM 清理）、MySQL 用 <em>時間換空間</em>（lock 等待、但不必清舊版本）。</p>
<p><strong>選擇判讀</strong>：</p>
<ul>
<li>High 並發 OLTP、寫 / 讀都重：PG MVCC 通常更好（讀不 block 寫）</li>
<li>簡單 OLTP + 不想管 VACUUM：MySQL InnoDB 對 ops 簡單</li>
<li>需要 SERIALIZABLE 強一致：PG SSI 對寫吞吐影響低</li>
<li>已有 MySQL 生態 / 工具鏈：MySQL Lock 知識可繼續用</li>
</ul>
<p>詳見 <a href="/blog/backend/01-database/vendors/mysql/lock-contention/" data-link-title="MySQL Lock Contention：在 staging 重現的 deadlock、production 跑 6 個月才出現" data-link-desc="MySQL InnoDB 的 lock 是 row-level、但 *為什麼某些 row 莫名其妙也被 lock* 是 gap lock / next-key lock 設計造成的隱性行為。本文從一個 production case 開場（staging 重現 deadlock / production 6 個月後突然爆）、走 5 種 InnoDB lock 類型（record / gap / next-key / insert intention / auto-inc）、isolation level 對 lock 行為的決定性影響、deadlock detection / SHOW ENGINE INNODB STATUS 解讀、5 production 踩雷（gap lock 阻塞 INSERT / auto-inc lock contention / FK lock cascading / large transaction lock holding / READ COMMITTED 跟 binlog ROW 互動）">MySQL Lock Contention</a> — 完整 MySQL lock 機制。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-autovacuum-tuning">跟 Autovacuum Tuning</h3>
<p>MVCC 仰賴 VACUUM、autovacuum 是 PG 並行控制的 <em>維護成本</em>。VACUUM 跑慢 / 沒跑 → bloat → query 慢。詳見 <a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">Autovacuum Tuning</a>。</p>
<h3 id="跟-replication-topology">跟 Replication Topology</h3>
<p><code>hot_standby_feedback = on</code> 讓 standby 上 long-running query 不被 vacuum 取消、但 <em>standby 把 oldest xmin 推回 primary</em>、primary autovacuum 變保守、增加 bloat。詳見 <a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">Replication Topology</a>。</p>
<h3 id="跟-connection-pool">跟 Connection Pool</h3>
<p>pgBouncer transaction pooling 模式下、advisory lock / SELECT FOR UPDATE 跨 transaction 行為 <em>broken</em>（不同 transaction 可能進不同 backend connection）。詳見 <a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgBouncer Config</a>。</p>
<h3 id="跟-query-optimization">跟 Query Optimization</h3>
<p>長 transaction 跑慢 query 期間、其他 transaction 看到 snapshot bloat、planner 估錯 dead tuple ratio。詳見 <a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">Query Optimization</a>。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">PG Autovacuum Tuning</a>（VACUUM 是 MVCC 必要成本）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/replication-topology/" data-link-title="PostgreSQL Replication Topology：async / sync / quorum 三模式跟 LSN &#43; replication slot 的三軸組合" data-link-desc="PostgreSQL streaming replication 不是「sync 或 async」、是 *durability / latency / consistency* 三軸組合 &#43; LSN-based 進度追蹤 &#43; replication slot 治理。本文走 3 軸取捨模型、async / sync / quorum-based sync 行為對比、LSN &#43; replication slot 機制、配置 step-by-step、5 production 踩雷（standby lag 暴衝 / sync standby 退回 async / orphan replication slot / cascading replication 雪崩 / failover 後 timeline 分歧）、跟 Patroni HA &#43; logical replication 整合">PG Replication Topology</a>（hot_standby_feedback 影響）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">PG pgBouncer</a>（transaction pooling 跟 lock 互動）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/online-schema-change/" data-link-title="PostgreSQL Online Schema Change：先用 ALTER 內建特性、不能解才 pg_repack / pg-osc" data-link-desc="PostgreSQL ALTER TABLE 對多數變更已是 *fast catalog-only*（add column nullable / drop column / 改 default），不必走 ghost table tool。本文走 PG 內建 fast DDL 行為、何時必須走 pg_repack / pg-osc、兩工具機制對比（trigger-based vs WAL-shipping）、配置 step-by-step、5 production 踩雷（lock 升級 / VACUUM FULL 誤用 / pg_repack version mismatch / concurrent index 失敗清理 / generated stored column 不能 online）、跟 MySQL gh-ost / pt-osc sibling 對比">PG Online Schema Change</a>（DDL lock 議題）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN ANALYZE / pg_hint_plan / auto_explain 三層工具跟 4 個 case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 4 個 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain 三層工具、pg_hint_plan extension 跟 planner GUC 取捨、5 production 踩雷（ANALYZE 過時 / multi-column statistics / cost-base setting 不對齊硬體 / random_page_cost SSD 沒調 / parallel query 配置）、跟 MySQL query-optimization sibling 對比">PG Query Optimization</a>（snapshot bloat 影響 planner）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/lock-contention/" data-link-title="MySQL Lock Contention：在 staging 重現的 deadlock、production 跑 6 個月才出現" data-link-desc="MySQL InnoDB 的 lock 是 row-level、但 *為什麼某些 row 莫名其妙也被 lock* 是 gap lock / next-key lock 設計造成的隱性行為。本文從一個 production case 開場（staging 重現 deadlock / production 6 個月後突然爆）、走 5 種 InnoDB lock 類型（record / gap / next-key / insert intention / auto-inc）、isolation level 對 lock 行為的決定性影響、deadlock detection / SHOW ENGINE INNODB STATUS 解讀、5 production 踩雷（gap lock 阻塞 INSERT / auto-inc lock contention / FK lock cascading / large transaction lock holding / READ COMMITTED 跟 binlog ROW 互動）">MySQL Lock Contention</a>（sibling、不同模型）</li>
<li><a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">Isolation Level 卡片</a></li>
<li>官方：<a href="https://www.postgresql.org/docs/current/mvcc.html">PG MVCC</a> / <a href="https://www.postgresql.org/docs/current/transaction-iso.html">PG Concurrency Control</a> / <a href="https://www.postgresql.org/docs/current/explicit-locking.html">Explicit Locking</a></li>
</ul>
]]></content:encoded></item><item><title>Go 進階指南</title><link>https://tarrragon.github.io/blog/go-advanced/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go-advanced/</guid><description>&lt;p>本系列是接在入門教學之後的延伸路線，目標是把 Go 的並發模式、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> 服務架構、runtime 診斷、狀態邊界與生產環境可觀測性講到能真正用在服務上。語法細節留在入門篇；進階篇聚焦長時間服務會遇到的設計壓力。&lt;/p>
&lt;h2 id="目標讀者">目標讀者&lt;/h2>
&lt;ul>
&lt;li>已完成 &lt;a href="https://tarrragon.github.io/blog/go/" data-link-title="Go 入門實戰指南" data-link-desc="理解 Go 語言精神與核心開發能力">Go 入門實戰指南&lt;/a> 的工程師&lt;/li>
&lt;li>想深入理解 Go 並發模型與 runtime 行為的開發者&lt;/li>
&lt;li>需要維護長時間運行服務的人&lt;/li>
&lt;li>想把 Go 服務從「能跑」提升到「可觀測、可測、可演進」的人&lt;/li>
&lt;/ul>
&lt;h2 id="學習目標">學習目標&lt;/h2>
&lt;ol>
&lt;li>掌握 goroutine、channel、mutex 的進階使用邊界&lt;/li>
&lt;li>理解 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> client lifecycle、heartbeat、&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;li>使用 pprof、runtime 記憶體限制與結構化日誌診斷服務&lt;/li>
&lt;li>設計 event-driven service 的資料邊界與去重策略&lt;/li>
&lt;li>建立並發測試、整合測試與可重現的時間控制&lt;/li>
&lt;li>能評估 Go 服務在生產環境的風險與操作策略&lt;/li>
&lt;li>知道單一 Go 服務延伸到跨節點與平台整合時，哪些責任會轉移到資料庫、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a>、observability pipeline 與部署平台&lt;/li>
&lt;/ol>
&lt;h2 id="共用術語">共用術語&lt;/h2>
&lt;p>進階篇延續入門篇的 action、command、domain event、repository、port、adapter、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection&lt;/a> 等詞彙。若你需要確認這些詞在這套教材中的責任邊界，可以先回到 &lt;a href="https://tarrragon.github.io/blog/go/glossary/" data-link-title="Go 教材核心術語" data-link-desc="整理 Go 入門與進階篇共用的架構、事件、狀態與邊界詞彙">Go 教材核心術語&lt;/a>。&lt;/p>
&lt;h2 id="與-backend-教材的分工">與 Backend 教材的分工&lt;/h2>
&lt;p>Go 進階篇處理單一 Go 服務內部的高階能力：goroutine lifecycle、WebSocket pump、runtime 診斷、event boundary、race test、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown&lt;/a> 與 diagnostics endpoint。當內容開始碰到資料庫、Redis、RabbitMQ、Kafka、OpenTelemetry、Kubernetes 或 CI 平台操作時，就應該轉到跨語言的 &lt;a href="https://tarrragon.github.io/blog/backend/" data-link-title="Backend 服務實務指南" data-link-desc="用跨語言教學路線整理資料庫、快取、訊息佇列、觀測、部署、可靠性、資安、事故與容量等後端服務能力">Backend 服務實務指南&lt;/a>。&lt;/p>
&lt;p>模組七保留在進階篇裡，因為它要回答「Go 服務在跨出去以前，還需要先把哪些 port、訊號與測試合約準備好」。外部系統本身的選型與部署細節，則放在 Backend，讓不同語言都能共用同一套實作知識。&lt;/p>
&lt;h2 id="教學模組">教學模組&lt;/h2>
&lt;h3 id="模組一進階並發模式">&lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/" data-link-title="模組一：進階並發模式" data-link-desc="channel ownership、select loop、非阻塞送出、共享狀態、worker pool 與 rate limiting">模組一：進階並發模式&lt;/a>&lt;/h3>
&lt;p>從服務實例理解 fan-in、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out&lt;/a>、取消傳播與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a> ，先把並發語意說清楚。&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/channel-ownership/" data-link-title="1.1 channel ownership 與關閉責任" data-link-desc="判斷誰能送出、接收與關閉 channel">channel ownership 與關閉責任&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/select-loop/" data-link-title="1.2 select loop 的生命週期設計" data-link-desc="理解長時間運行 goroutine 如何同時處理事件、ticker 與取消">select loop 的生命週期設計&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/non-blocking-send/" data-link-title="1.3 非阻塞送出與事件丟棄策略" data-link-desc="設計 channel 滿載時的服務行為">非阻塞送出與事件丟棄策略&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/shared-state/" data-link-title="1.4 共享狀態與複製邊界" data-link-desc="用 lock 與 copy 保護長期服務的狀態資料">共享狀態與複製邊界&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/worker-pool/" data-link-title="1.5 bounded worker pool" data-link-desc="限制同時執行的 goroutine 數量，讓背景工作有明確容量邊界">bounded worker pool&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/rate-limit/" data-link-title="1.6 rate limiting 與 backpressure " data-link-desc="用本地速率限制與 backpressure 策略保護服務入口與下游依賴">rate limiting 與 backpressure &lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="模組二websocket-服務架構">&lt;a href="https://tarrragon.github.io/blog/go-advanced/02-networking-websocket/" data-link-title="模組二：WebSocket 服務架構" data-link-desc="WebSocket client lifecycle、heartbeat、訂閱路由與慢客戶端管理">模組二：WebSocket 服務架構&lt;/a>&lt;/h3>
&lt;p>把 WebSocket server 的連線、訂閱、推送與錯誤處理拆成可維護的邊界。&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/02-networking-websocket/read-write-pump/" data-link-title="2.1 read pump / write pump 模式" data-link-desc="分離 WebSocket 讀取、寫入與心跳">read pump / write pump 模式&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/02-networking-websocket/heartbeat-deadline/" data-link-title="2.2 heartbeat、deadline 與連線清理" data-link-desc="用 ping/pong 和 deadline 偵測失效連線">heartbeat、deadline 與連線清理&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/02-networking-websocket/subscription-routing/" data-link-title="2.3 訂閱模型與訊息路由" data-link-desc="將 client action 對應到主題訂閱狀態">訂閱模型與訊息路由&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/02-networking-websocket/slow-client/" data-link-title="2.4 慢客戶端與 send buffer 管理" data-link-desc="控制推送佇列與記憶體風險">慢客戶端與 send buffer 管理&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="模組三runtime-與效能診斷">&lt;a href="https://tarrragon.github.io/blog/go-advanced/03-runtime-profiling/" data-link-title="模組三：Runtime 與效能診斷" data-link-desc="GC、memory limit、pprof、goroutine leak 與 allocation 壓力">模組三：Runtime 與效能診斷&lt;/a>&lt;/h3>
&lt;p>理解 Go runtime 如何在長時間運行服務中影響記憶體與排程行為。&lt;/p></description><content:encoded><![CDATA[<p>本系列是接在入門教學之後的延伸路線，目標是把 Go 的並發模式、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> 服務架構、runtime 診斷、狀態邊界與生產環境可觀測性講到能真正用在服務上。語法細節留在入門篇；進階篇聚焦長時間服務會遇到的設計壓力。</p>
<h2 id="目標讀者">目標讀者</h2>
<ul>
<li>已完成 <a href="/blog/go/" data-link-title="Go 入門實戰指南" data-link-desc="理解 Go 語言精神與核心開發能力">Go 入門實戰指南</a> 的工程師</li>
<li>想深入理解 Go 並發模型與 runtime 行為的開發者</li>
<li>需要維護長時間運行服務的人</li>
<li>想把 Go 服務從「能跑」提升到「可觀測、可測、可演進」的人</li>
</ul>
<h2 id="學習目標">學習目標</h2>
<ol>
<li>掌握 goroutine、channel、mutex 的進階使用邊界</li>
<li>理解 <a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> client lifecycle、heartbeat、<a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 與慢客戶端問題</li>
<li>使用 pprof、runtime 記憶體限制與結構化日誌診斷服務</li>
<li>設計 event-driven service 的資料邊界與去重策略</li>
<li>建立並發測試、整合測試與可重現的時間控制</li>
<li>能評估 Go 服務在生產環境的風險與操作策略</li>
<li>知道單一 Go 服務延伸到跨節點與平台整合時，哪些責任會轉移到資料庫、<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a>、<a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a>、observability pipeline 與部署平台</li>
</ol>
<h2 id="共用術語">共用術語</h2>
<p>進階篇延續入門篇的 action、command、domain event、repository、port、adapter、<a href="/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection</a> 等詞彙。若你需要確認這些詞在這套教材中的責任邊界，可以先回到 <a href="/blog/go/glossary/" data-link-title="Go 教材核心術語" data-link-desc="整理 Go 入門與進階篇共用的架構、事件、狀態與邊界詞彙">Go 教材核心術語</a>。</p>
<h2 id="與-backend-教材的分工">與 Backend 教材的分工</h2>
<p>Go 進階篇處理單一 Go 服務內部的高階能力：goroutine lifecycle、WebSocket pump、runtime 診斷、event boundary、race test、<a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a> 與 diagnostics endpoint。當內容開始碰到資料庫、Redis、RabbitMQ、Kafka、OpenTelemetry、Kubernetes 或 CI 平台操作時，就應該轉到跨語言的 <a href="/blog/backend/" data-link-title="Backend 服務實務指南" data-link-desc="用跨語言教學路線整理資料庫、快取、訊息佇列、觀測、部署、可靠性、資安、事故與容量等後端服務能力">Backend 服務實務指南</a>。</p>
<p>模組七保留在進階篇裡，因為它要回答「Go 服務在跨出去以前，還需要先把哪些 port、訊號與測試合約準備好」。外部系統本身的選型與部署細節，則放在 Backend，讓不同語言都能共用同一套實作知識。</p>
<h2 id="教學模組">教學模組</h2>
<h3 id="模組一進階並發模式"><a href="/blog/go-advanced/01-concurrency-patterns/" data-link-title="模組一：進階並發模式" data-link-desc="channel ownership、select loop、非阻塞送出、共享狀態、worker pool 與 rate limiting">模組一：進階並發模式</a></h3>
<p>從服務實例理解 fan-in、<a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a>、取消傳播與 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> ，先把並發語意說清楚。</p>
<ul>
<li><a href="/blog/go-advanced/01-concurrency-patterns/channel-ownership/" data-link-title="1.1 channel ownership 與關閉責任" data-link-desc="判斷誰能送出、接收與關閉 channel">channel ownership 與關閉責任</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 與取消">select loop 的生命週期設計</a></li>
<li><a href="/blog/go-advanced/01-concurrency-patterns/non-blocking-send/" data-link-title="1.3 非阻塞送出與事件丟棄策略" data-link-desc="設計 channel 滿載時的服務行為">非阻塞送出與事件丟棄策略</a></li>
<li><a href="/blog/go-advanced/01-concurrency-patterns/shared-state/" data-link-title="1.4 共享狀態與複製邊界" data-link-desc="用 lock 與 copy 保護長期服務的狀態資料">共享狀態與複製邊界</a></li>
<li><a href="/blog/go-advanced/01-concurrency-patterns/worker-pool/" data-link-title="1.5 bounded worker pool" data-link-desc="限制同時執行的 goroutine 數量，讓背景工作有明確容量邊界">bounded worker pool</a></li>
<li><a href="/blog/go-advanced/01-concurrency-patterns/rate-limit/" data-link-title="1.6 rate limiting 與 backpressure " data-link-desc="用本地速率限制與 backpressure 策略保護服務入口與下游依賴">rate limiting 與 backpressure </a></li>
</ul>
<h3 id="模組二websocket-服務架構"><a href="/blog/go-advanced/02-networking-websocket/" data-link-title="模組二：WebSocket 服務架構" data-link-desc="WebSocket client lifecycle、heartbeat、訂閱路由與慢客戶端管理">模組二：WebSocket 服務架構</a></h3>
<p>把 WebSocket server 的連線、訂閱、推送與錯誤處理拆成可維護的邊界。</p>
<ul>
<li><a href="/blog/go-advanced/02-networking-websocket/read-write-pump/" data-link-title="2.1 read pump / write pump 模式" data-link-desc="分離 WebSocket 讀取、寫入與心跳">read pump / write pump 模式</a></li>
<li><a href="/blog/go-advanced/02-networking-websocket/heartbeat-deadline/" data-link-title="2.2 heartbeat、deadline 與連線清理" data-link-desc="用 ping/pong 和 deadline 偵測失效連線">heartbeat、deadline 與連線清理</a></li>
<li><a href="/blog/go-advanced/02-networking-websocket/subscription-routing/" data-link-title="2.3 訂閱模型與訊息路由" data-link-desc="將 client action 對應到主題訂閱狀態">訂閱模型與訊息路由</a></li>
<li><a href="/blog/go-advanced/02-networking-websocket/slow-client/" data-link-title="2.4 慢客戶端與 send buffer 管理" data-link-desc="控制推送佇列與記憶體風險">慢客戶端與 send buffer 管理</a></li>
</ul>
<h3 id="模組三runtime-與效能診斷"><a href="/blog/go-advanced/03-runtime-profiling/" data-link-title="模組三：Runtime 與效能診斷" data-link-desc="GC、memory limit、pprof、goroutine leak 與 allocation 壓力">模組三：Runtime 與效能診斷</a></h3>
<p>理解 Go runtime 如何在長時間運行服務中影響記憶體與排程行為。</p>
<ul>
<li><a href="/blog/go-advanced/03-runtime-profiling/gc-memory-limit/" data-link-title="3.1 GC 與 memory limit" data-link-desc="理解 debug.SetMemoryLimit 在長時間服務中的用途">GC 與 memory limit</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 問題">pprof 基礎診斷流程</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 是否正確退出">goroutine leak 偵測</a></li>
<li><a href="/blog/go-advanced/03-runtime-profiling/allocation/" data-link-title="3.4 資料結構與 allocation 壓力" data-link-desc="分析列表、歷史資料與 WebSocket payload 的配置成本">資料結構與 allocation 壓力</a></li>
</ul>
<h3 id="模組四架構邊界與事件系統"><a href="/blog/go-advanced/04-architecture-boundaries/" data-link-title="模組四：架構邊界與事件系統" data-link-desc="用事件驅動架構拆解事件來源、處理流程、狀態邊界與即時推送">模組四：架構邊界與事件系統</a></h3>
<p>用事件驅動架構拆解服務責任，讓來源、處理與狀態不再混在一起。</p>
<ul>
<li><a href="/blog/go-advanced/04-architecture-boundaries/component-boundaries/" data-link-title="4.1 事件來源、處理流程與狀態邊界" data-link-desc="分辨事件來源、事件融合、處理流程、狀態真相與推送邊界">事件來源、處理流程與狀態邊界</a></li>
<li><a href="/blog/go-advanced/04-architecture-boundaries/dedup-key/" data-link-title="4.2 事件去重與語義鍵設計" data-link-desc="用 entity ID、event type、來源語意與時間窗口建立去重鍵">事件去重與語義鍵設計</a></li>
<li><a href="/blog/go-advanced/04-architecture-boundaries/source-of-truth/" data-link-title="4.3 Source of Truth：狀態邊界" data-link-desc="集中狀態更新、保護可變資料、設計查詢 projection">Source of Truth：狀態邊界</a></li>
<li><a href="/blog/go-advanced/04-architecture-boundaries/event-fusion/" data-link-title="4.4 多來源 event 融合" data-link-desc="合併 HTTP、queue、timer 與外部事件來源">多來源 event 融合</a></li>
</ul>
<h3 id="模組五測試與可靠性"><a href="/blog/go-advanced/05-testing-reliability/" data-link-title="模組五：測試與可靠性" data-link-desc="時間控制、WebSocket integration test、race check 與 table-driven test">模組五：測試與可靠性</a></h3>
<p>針對並發服務建立能真正揭露風險的測試，而不是只追求覆蓋率。</p>
<ul>
<li><a href="/blog/go-advanced/05-testing-reliability/time-control/" data-link-title="5.1 時間注入與狀態轉移測試" data-link-desc="讓時間相關邏輯可重現">時間注入與狀態轉移測試</a></li>
<li><a href="/blog/go-advanced/05-testing-reliability/websocket-integration/" data-link-title="5.2 WebSocket integration test" data-link-desc="驗證 client/server 實際互動">WebSocket integration test</a></li>
<li><a href="/blog/go-advanced/05-testing-reliability/race-check/" data-link-title="5.3 race condition 檢查" data-link-desc="用 go test -race 找資料競爭">race condition 檢查</a></li>
<li><a href="/blog/go-advanced/05-testing-reliability/table-tests/" data-link-title="5.4 table-driven test 的設計邊界" data-link-desc="避免測試資料混雜太多概念">table-driven test 的設計邊界</a></li>
</ul>
<h3 id="模組六生產操作"><a href="/blog/go-advanced/06-production-operations/" data-link-title="模組六：生產操作" data-link-desc="graceful shutdown、健康檢查、結構化日誌與 feature gate">模組六：生產操作</a></h3>
<p>把本地服務推向可維護、可診斷、可部署的操作狀態。</p>
<ul>
<li><a href="/blog/go-advanced/06-production-operations/graceful-shutdown/" data-link-title="6.1 graceful shutdown 與 signal handling" data-link-desc="用 signal 與 context 傳遞停止訊號">graceful shutdown 與 signal handling</a></li>
<li><a href="/blog/go-advanced/06-production-operations/health-diagnostics/" data-link-title="6.2 健康檢查與診斷 endpoint" data-link-desc="區分服務可用性與工程診斷入口">健康檢查與診斷 endpoint</a></li>
<li><a href="/blog/go-advanced/06-production-operations/log-fields/" data-link-title="6.3 結構化日誌欄位設計" data-link-desc="讓 log 可 grep、可聚合、可追蹤">結構化日誌欄位設計</a></li>
<li><a href="/blog/go-advanced/06-production-operations/feature-gate/" data-link-title="6.4 版本偵測與 feature gate" data-link-desc="依版本與環境能力啟用功能">版本偵測與 feature gate</a></li>
</ul>
<h3 id="模組七跨節點與平台整合"><a href="/blog/go-advanced/07-distributed-operations/" data-link-title="模組七：跨節點與平台整合" data-link-desc="把單一 Go 服務延伸到資料庫、queue、跨節點 WebSocket、可觀測性與部署平台">模組七：跨節點與平台整合</a></h3>
<p>承接各章「本章不處理」的延伸邊界，把單一服務往外擴張時必須補上的責任整理成一條清楚路線。</p>
<ul>
<li><a href="/blog/go-advanced/07-distributed-operations/database-transactions/" data-link-title="7.1 資料庫 transaction 與 schema migration" data-link-desc="把 repository 邊界延伸到資料庫交易、migration 與一致性語意">資料庫 transaction 與 schema migration</a></li>
<li><a href="/blog/go-advanced/07-distributed-operations/outbox-idempotency/" data-link-title="7.2 Durable queue、outbox 與 idempotency" data-link-desc="設計跨 process 事件傳遞的可靠性與去重邊界">Durable queue、outbox 與 idempotency</a></li>
<li><a href="/blog/go-advanced/07-distributed-operations/cross-node-websocket/" data-link-title="7.3 跨節點 WebSocket、presence 與重連協定" data-link-desc="把單一 server 的 WebSocket hub 擴展到多節點推送與連線狀態">跨節點 WebSocket、presence 與重連協定</a></li>
<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 組成可操作的診斷系統">Observability pipeline、metrics 與 tracing</a></li>
<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 與資源限制">Kubernetes、systemd 與 load balancer 合約</a></li>
<li><a href="/blog/go-advanced/07-distributed-operations/reliability-pipeline/" data-link-title="7.6 CI、fuzz、load test 與 chaos testing" data-link-desc="把單元測試與整合測試擴展成服務可靠性驗證流程">CI、fuzz、load test 與 chaos testing</a></li>
</ul>
<h2 id="學習路徑">學習路徑</h2>
<h3 id="路徑-a並發服務維護者">路徑 A：並發服務維護者</h3>





<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">模組一 → 模組四 → 模組五</span></span></code></pre></div><p>重點：事件流、共享狀態、並發測試。</p>
<h3 id="路徑-bwebsocketapi-開發者">路徑 B：WebSocket/API 開發者</h3>





<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">模組二 → 模組六 → 模組五</span></span></code></pre></div><p>重點：連線生命週期、訊息路由、操作診斷。</p>
<h3 id="路徑-c效能與可靠性工程師">路徑 C：效能與可靠性工程師</h3>





<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">模組三 → 模組五 → 模組六</span></span></code></pre></div><p>重點：pprof、goroutine leak、race check、服務操作。</p>
<h3 id="路徑-d完整學習">路徑 D：完整學習</h3>





<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">模組一 → 模組二 → 模組三 → 模組四 → 模組五 → 模組六 → 模組七</span></span></code></pre></div><p>按順序學習，建立完整的 Go 長時間運行服務模型。</p>
<h2 id="主題延伸地圖">主題延伸地圖</h2>
<p>進階篇的章節會反覆碰到 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>、time、state、event、WebSocket 與 testing。這些主題會在不同服務壓力下承擔不同責任；主題延伸地圖用來幫讀者辨識每一層的分工。</p>
<table>
  <thead>
      <tr>
          <th>主題</th>
          <th>單一 process 內的設計</th>
          <th>生產操作</th>
          <th>跨節點邊界</th>
          <th>Backend 實作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>並發與容量</td>
          <td><a href="/blog/go-advanced/01-concurrency-patterns/channel-ownership/" data-link-title="1.1 channel ownership 與關閉責任" data-link-desc="判斷誰能送出、接收與關閉 channel">channel ownership</a>、<a href="/blog/go-advanced/01-concurrency-patterns/select-loop/" data-link-title="1.2 select loop 的生命週期設計" data-link-desc="理解長時間運行 goroutine 如何同時處理事件、ticker 與取消">select loop</a>、<a href="/blog/go-advanced/01-concurrency-patterns/non-blocking-send/" data-link-title="1.3 非阻塞送出與事件丟棄策略" data-link-desc="設計 channel 滿載時的服務行為">非阻塞送出</a></td>
          <td><a href="/blog/go-advanced/05-testing-reliability/race-check/" data-link-title="5.3 race condition 檢查" data-link-desc="用 go test -race 找資料競爭">race condition 檢查</a>、<a href="/blog/go-advanced/06-production-operations/graceful-shutdown/" data-link-title="6.1 graceful shutdown 與 signal handling" data-link-desc="用 signal 與 context 傳遞停止訊號">graceful shutdown</a></td>
          <td><a href="/blog/go-advanced/07-distributed-operations/reliability-pipeline/" data-link-title="7.6 CI、fuzz、load test 與 chaos testing" data-link-desc="把單元測試與整合測試擴展成服務可靠性驗證流程">可靠性驗證流程</a></td>
          <td><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">Backend：可靠性驗證</a></td>
      </tr>
      <tr>
          <td>WebSocket</td>
          <td><a href="/blog/go-advanced/02-networking-websocket/read-write-pump/" data-link-title="2.1 read pump / write pump 模式" data-link-desc="分離 WebSocket 讀取、寫入與心跳">read/write pump</a>、<a href="/blog/go-advanced/02-networking-websocket/heartbeat-deadline/" data-link-title="2.2 heartbeat、deadline 與連線清理" data-link-desc="用 ping/pong 和 deadline 偵測失效連線">heartbeat</a>、<a href="/blog/go-advanced/02-networking-websocket/slow-client/" data-link-title="2.4 慢客戶端與 send buffer 管理" data-link-desc="控制推送佇列與記憶體風險">慢客戶端</a></td>
          <td><a href="/blog/go-advanced/05-testing-reliability/websocket-integration/" data-link-title="5.2 WebSocket integration test" data-link-desc="驗證 client/server 實際互動">WebSocket integration test</a>、<a href="/blog/go-advanced/06-production-operations/health-diagnostics/" data-link-title="6.2 健康檢查與診斷 endpoint" data-link-desc="區分服務可用性與工程診斷入口">health diagnostics</a></td>
          <td><a href="/blog/go-advanced/07-distributed-operations/cross-node-websocket/" data-link-title="7.3 跨節點 WebSocket、presence 與重連協定" data-link-desc="把單一 server 的 WebSocket hub 擴展到多節點推送與連線狀態">跨節點 WebSocket</a></td>
          <td><a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">Backend：快取與 Redis</a>、<a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">訊息佇列</a></td>
      </tr>
      <tr>
          <td>Runtime 診斷</td>
          <td><a href="/blog/go-advanced/03-runtime-profiling/gc-memory-limit/" data-link-title="3.1 GC 與 memory limit" data-link-desc="理解 debug.SetMemoryLimit 在長時間服務中的用途">GC 與 memory limit</a>、<a href="/blog/go-advanced/03-runtime-profiling/pprof/" data-link-title="3.2 pprof 基礎診斷流程" data-link-desc="用 pprof endpoint 診斷 heap、goroutine 與 CPU 問題">pprof</a>、<a href="/blog/go-advanced/03-runtime-profiling/goroutine-leak/" data-link-title="3.3 goroutine leak 偵測" data-link-desc="判斷背景工作與 client pump 是否正確退出">goroutine leak</a></td>
          <td><a href="/blog/go-advanced/06-production-operations/health-diagnostics/" data-link-title="6.2 健康檢查與診斷 endpoint" data-link-desc="區分服務可用性與工程診斷入口">diagnostics endpoint</a></td>
          <td><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 組成可操作的診斷系統">Observability pipeline</a>、<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 與資源限制">部署平台合約</a></td>
          <td><a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">Backend：可觀測性平台</a>、<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">部署平台</a></td>
      </tr>
      <tr>
          <td>事件與狀態</td>
          <td><a href="/blog/go-advanced/04-architecture-boundaries/component-boundaries/" data-link-title="4.1 事件來源、處理流程與狀態邊界" data-link-desc="分辨事件來源、事件融合、處理流程、狀態真相與推送邊界">component boundaries</a>、<a href="/blog/go-advanced/04-architecture-boundaries/source-of-truth/" data-link-title="4.3 Source of Truth：狀態邊界" data-link-desc="集中狀態更新、保護可變資料、設計查詢 projection">source of truth</a>、<a href="/blog/go-advanced/04-architecture-boundaries/event-fusion/" data-link-title="4.4 多來源 event 融合" data-link-desc="合併 HTTP、queue、timer 與外部事件來源">event fusion</a></td>
          <td><a href="/blog/go-advanced/06-production-operations/log-fields/" data-link-title="6.3 結構化日誌欄位設計" data-link-desc="讓 log 可 grep、可聚合、可追蹤">結構化日誌欄位</a></td>
          <td><a href="/blog/go-advanced/07-distributed-operations/outbox-idempotency/" data-link-title="7.2 Durable queue、outbox 與 idempotency" data-link-desc="設計跨 process 事件傳遞的可靠性與去重邊界">outbox 與 idempotency</a>、<a href="/blog/go-advanced/07-distributed-operations/database-transactions/" data-link-title="7.1 資料庫 transaction 與 schema migration" data-link-desc="把 repository 邊界延伸到資料庫交易、migration 與一致性語意">資料庫 transaction</a></td>
          <td><a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">Backend：資料庫</a>、<a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">訊息佇列</a></td>
      </tr>
      <tr>
          <td>測試分層</td>
          <td><a href="/blog/go-advanced/05-testing-reliability/time-control/" data-link-title="5.1 時間注入與狀態轉移測試" data-link-desc="讓時間相關邏輯可重現">時間控制</a>、<a href="/blog/go-advanced/05-testing-reliability/table-tests/" data-link-title="5.4 table-driven test 的設計邊界" data-link-desc="避免測試資料混雜太多概念">table-driven test</a></td>
          <td><a href="/blog/go-advanced/05-testing-reliability/race-check/" data-link-title="5.3 race condition 檢查" data-link-desc="用 go test -race 找資料競爭">race check</a>、<a href="/blog/go-advanced/05-testing-reliability/websocket-integration/" data-link-title="5.2 WebSocket integration test" data-link-desc="驗證 client/server 實際互動">integration test</a></td>
          <td><a href="/blog/go-advanced/07-distributed-operations/reliability-pipeline/" data-link-title="7.6 CI、fuzz、load test 與 chaos testing" data-link-desc="把單元測試與整合測試擴展成服務可靠性驗證流程">可靠性驗證流程</a></td>
          <td><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">Backend：可靠性驗證</a></td>
      </tr>
  </tbody>
</table>
<h2 id="先備知識">先備知識</h2>
<p>本系列假設你已經完成 <a href="/blog/go/" data-link-title="Go 入門實戰指南" data-link-desc="理解 Go 語言精神與核心開發能力">Go 入門實戰指南</a> 的基礎部分，因為下面這些章節會直接沿用那些概念：</p>
<ul>
<li><a href="/blog/go/03-stdlib/" data-link-title="模組三：標準庫實戰" data-link-desc="使用 fmt、time、encoding/json、net/http、log/slog、context、defer、flag 與 os/env 解決實務問題">模組三：標準庫實戰</a></li>
<li><a href="/blog/go/04-concurrency/" data-link-title="模組四：並發模型" data-link-desc="從 goroutine、channel、select 與 RWMutex 理解 Go 並發模型">模組四：並發模型</a></li>
<li><a href="/blog/go/05-error-testing/" data-link-title="模組五：錯誤處理與測試" data-link-desc="用明確錯誤路徑、testing、table-driven test 與時間注入驗證 Go 程式">模組五：錯誤處理與測試</a></li>
</ul>
<h2 id="每章結構">每章結構</h2>
<p>每章都採用「由淺到深」的結構，先說明問題，再切到設計與實作：</p>
<ol>
<li><strong>原理層</strong>：這個機制解決什麼問題</li>
<li><strong>設計層</strong>：在服務架構中如何切責任</li>
<li><strong>實作層</strong>：用簡化範例程式碼看具體做法</li>
<li><strong>實戰檢查</strong>：維護時要確認哪些風險</li>
</ol>
<hr>
<p><em>文件版本：v0.1.0</em>
<em>最後更新：2026-04-22</em>
<em>系列狀態：核心初稿完成，延伸模組規劃中</em></p>
]]></content:encoded></item><item><title>4.0 Go 並發模型總覽</title><link>https://tarrragon.github.io/blog/go/04-concurrency/concurrency-model/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/04-concurrency/concurrency-model/</guid><description>&lt;p>Go 的並發優勢在於 runtime 讓大量 goroutine 的生命週期、排程與阻塞管理更容易使用。處理高併發時，核心判斷是哪些工作可以並發、哪些資源需要限制，以及 runtime 如何把很多 goroutine 放到有限的 OS thread 上執行。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>分辨 process、thread 與 goroutine 的角色&lt;/li>
&lt;li>理解 Go 並發與平行執行不是同一件事&lt;/li>
&lt;li>看懂為什麼 I/O 型服務特別適合 Go&lt;/li>
&lt;li>判斷什麼時候應該限制並發數&lt;/li>
&lt;li>了解 Redis 與 SQL 在高併發下為什麼要加邊界&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察goroutine-由-go-runtime-管理">【觀察】goroutine 由 Go runtime 管理&lt;/h2>
&lt;p>goroutine 是 Go runtime 管理的輕量工作單位，OS thread 則是作業系統實際排程的執行緒。你通常不會直接手動管理 goroutine 對應到哪一條 thread；Go runtime 會負責把很多 goroutine 排程到較少的 OS thread 上。&lt;/p>
&lt;p>這表示兩件事：&lt;/p>
&lt;ul>
&lt;li>啟動 goroutine 的成本比建立 thread 低得多。&lt;/li>
&lt;li>goroutine 很便宜，不代表下游資源也很便宜。&lt;/li>
&lt;/ul>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>名稱&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>process&lt;/td>
 &lt;td>程式執行的整體容器&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>OS thread&lt;/td>
 &lt;td>作業系統真正排程的執行單位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>goroutine&lt;/td>
 &lt;td>Go runtime 管理的並發工作單位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>runtime&lt;/td>
 &lt;td>負責排程、記憶體管理、阻塞處理與 goroutine 生命週期協調&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀並發和平行是不同層次">【判讀】並發和平行是不同層次&lt;/h2>
&lt;p>並發的核心意義是「很多工作在時間上交疊」，平行的核心意義是「真的同時在多個核心上跑」。Go 可以讓你很容易建立並發工作，但是否能同時跑在多核心上，還要看 runtime 排程、CPU 數量與工作型態。&lt;/p>
&lt;p>對服務開發來說，這個差異很重要：&lt;/p>
&lt;ul>
&lt;li>I/O-bound 工作通常最適合並發化，因為大部分時間都在等網路、磁碟或外部服務。&lt;/li>
&lt;li>CPU-bound 工作不會因為你加很多 goroutine 就自動變快，反而可能因為排程與同步成本變複雜。&lt;/li>
&lt;/ul>
&lt;h2 id="策略高併發的真正重點是限制下游">【策略】高併發的真正重點是限制下游&lt;/h2>
&lt;p>Go 的 goroutine 很容易開，但 Redis、SQL、HTTP API、檔案描述元與記憶體 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a> 都有容量上限。高併發設計的核心是替外部資源設邊界，讓 goroutine 數量、下游連線與排隊時間都保持可預期。&lt;/p>
&lt;p>常見邊界包括：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool&lt;/a> 限制同時處理的工作量&lt;/li>
&lt;li>semaphore 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit&lt;/a> 限制入口速率&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">deadline&lt;/a> 避免單一請求卡太久&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> 或 buffer 對短暫尖峰提供緩衝&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a> 讓上游看到真實壓力&lt;/li>
&lt;/ul>
&lt;h2 id="應用redis-與-sql-都是-io-邊界">【應用】Redis 與 SQL 都是 I/O 邊界&lt;/h2>
&lt;p>Redis 與 SQL 在 Go 裡通常都被當成 I/O 操作來看待：goroutine 負責並發發出請求，但真正的瓶頸通常在網路延遲、連線數、鎖競爭、索引、熱點 key 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction&lt;/a> 範圍。&lt;/p>
&lt;p>這也是為什麼後面的資料存取章節會反覆強調：&lt;/p>
&lt;ul>
&lt;li>client 或 &lt;code>sql.DB&lt;/code> 要共用，不要每個 request 都 new&lt;/li>
&lt;li>每個操作都應該帶 &lt;code>context&lt;/code>&lt;/li>
&lt;li>讀取可以大量並發，但要有連線池和 timeout&lt;/li>
&lt;li>寫入可以並發，但要注意衝突、重試與交易邊界&lt;/li>
&lt;li>當下游開始飽和時，要有明確的拒絕、排隊或降級策略&lt;/li>
&lt;/ul>
&lt;h2 id="延伸runtime-細節不必現在全背">【延伸】runtime 細節不必現在全背&lt;/h2>
&lt;p>本章先建立 runtime 閱讀模型：goroutine 很輕，thread 有成本，下游資源有上限，並發要設邊界。runtime 的完整內部實作可以留到 profiling 與效能診斷階段再深入。&lt;/p>
&lt;p>更進一步的診斷與觀察，會在後面的 runtime profiling 與 goroutine leak 章節再補。&lt;/p>
&lt;h2 id="本章先處理">本章先處理&lt;/h2>
&lt;p>這一章先把 Go 的並發模型講清楚；真正落到資料庫與快取時，可以再看：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">Backend：資料庫與持久化&lt;/a>：看 SQL、transaction、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool&lt;/a> 與 schema 邊界如何承接服務壓力。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">Backend：快取與 Redis&lt;/a>：看 Redis client、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction&lt;/a>、presence store 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key&lt;/a> 如何承接服務壓力。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">bounded worker pool&lt;/a>：把並發數收斂成可控容量。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Go 的並發優勢在於 runtime 讓大量 goroutine 的生命週期、排程與阻塞管理更容易使用。處理高併發時，核心判斷是哪些工作可以並發、哪些資源需要限制，以及 runtime 如何把很多 goroutine 放到有限的 OS thread 上執行。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>分辨 process、thread 與 goroutine 的角色</li>
<li>理解 Go 並發與平行執行不是同一件事</li>
<li>看懂為什麼 I/O 型服務特別適合 Go</li>
<li>判斷什麼時候應該限制並發數</li>
<li>了解 Redis 與 SQL 在高併發下為什麼要加邊界</li>
</ol>
<hr>
<h2 id="觀察goroutine-由-go-runtime-管理">【觀察】goroutine 由 Go runtime 管理</h2>
<p>goroutine 是 Go runtime 管理的輕量工作單位，OS thread 則是作業系統實際排程的執行緒。你通常不會直接手動管理 goroutine 對應到哪一條 thread；Go runtime 會負責把很多 goroutine 排程到較少的 OS thread 上。</p>
<p>這表示兩件事：</p>
<ul>
<li>啟動 goroutine 的成本比建立 thread 低得多。</li>
<li>goroutine 很便宜，不代表下游資源也很便宜。</li>
</ul>
<table>
  <thead>
      <tr>
          <th>名稱</th>
          <th>責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>process</td>
          <td>程式執行的整體容器</td>
      </tr>
      <tr>
          <td>OS thread</td>
          <td>作業系統真正排程的執行單位</td>
      </tr>
      <tr>
          <td>goroutine</td>
          <td>Go runtime 管理的並發工作單位</td>
      </tr>
      <tr>
          <td>runtime</td>
          <td>負責排程、記憶體管理、阻塞處理與 goroutine 生命週期協調</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀並發和平行是不同層次">【判讀】並發和平行是不同層次</h2>
<p>並發的核心意義是「很多工作在時間上交疊」，平行的核心意義是「真的同時在多個核心上跑」。Go 可以讓你很容易建立並發工作，但是否能同時跑在多核心上，還要看 runtime 排程、CPU 數量與工作型態。</p>
<p>對服務開發來說，這個差異很重要：</p>
<ul>
<li>I/O-bound 工作通常最適合並發化，因為大部分時間都在等網路、磁碟或外部服務。</li>
<li>CPU-bound 工作不會因為你加很多 goroutine 就自動變快，反而可能因為排程與同步成本變複雜。</li>
</ul>
<h2 id="策略高併發的真正重點是限制下游">【策略】高併發的真正重點是限制下游</h2>
<p>Go 的 goroutine 很容易開，但 Redis、SQL、HTTP API、檔案描述元與記憶體 <a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 都有容量上限。高併發設計的核心是替外部資源設邊界，讓 goroutine 數量、下游連線與排隊時間都保持可預期。</p>
<p>常見邊界包括：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool</a> 限制同時處理的工作量</li>
<li>semaphore 或 <a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit</a> 限制入口速率</li>
<li><a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> / <a href="/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">deadline</a> 避免單一請求卡太久</li>
<li><a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> 或 buffer 對短暫尖峰提供緩衝</li>
<li><a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> 讓上游看到真實壓力</li>
</ul>
<h2 id="應用redis-與-sql-都是-io-邊界">【應用】Redis 與 SQL 都是 I/O 邊界</h2>
<p>Redis 與 SQL 在 Go 裡通常都被當成 I/O 操作來看待：goroutine 負責並發發出請求，但真正的瓶頸通常在網路延遲、連線數、鎖競爭、索引、熱點 key 或 <a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a> 範圍。</p>
<p>這也是為什麼後面的資料存取章節會反覆強調：</p>
<ul>
<li>client 或 <code>sql.DB</code> 要共用，不要每個 request 都 new</li>
<li>每個操作都應該帶 <code>context</code></li>
<li>讀取可以大量並發，但要有連線池和 timeout</li>
<li>寫入可以並發，但要注意衝突、重試與交易邊界</li>
<li>當下游開始飽和時，要有明確的拒絕、排隊或降級策略</li>
</ul>
<h2 id="延伸runtime-細節不必現在全背">【延伸】runtime 細節不必現在全背</h2>
<p>本章先建立 runtime 閱讀模型：goroutine 很輕，thread 有成本，下游資源有上限，並發要設邊界。runtime 的完整內部實作可以留到 profiling 與效能診斷階段再深入。</p>
<p>更進一步的診斷與觀察，會在後面的 runtime profiling 與 goroutine leak 章節再補。</p>
<h2 id="本章先處理">本章先處理</h2>
<p>這一章先把 Go 的並發模型講清楚；真正落到資料庫與快取時，可以再看：</p>
<ul>
<li><a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">Backend：資料庫與持久化</a>：看 SQL、transaction、<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a> 與 schema 邊界如何承接服務壓力。</li>
<li><a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">Backend：快取與 Redis</a>：看 Redis client、<a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a>、<a href="/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction</a>、presence store 與 <a href="/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key</a> 如何承接服務壓力。</li>
<li><a href="/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">bounded worker pool</a>：把並發數收斂成可控容量。</li>
</ul>
]]></content:encoded></item></channel></rss>