<?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>模組零：Go 選型與設計哲學 on Tarragon</title><link>https://tarrragon.github.io/blog/go/00-philosophy/</link><description>Recent content in 模組零：Go 選型與設計哲學 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 22 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/go/00-philosophy/index.xml" rel="self" type="application/rss+xml"/><item><title>0.1 Go 的簡單哲學與認知負擔</title><link>https://tarrragon.github.io/blog/go/00-philosophy/simplicity/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/00-philosophy/simplicity/</guid><description>&lt;p>Go 的核心取捨是降低讀程式的人需要同時記住的事情。它不追求語法表現力最大化，而是追求團隊在多年後仍能快速判讀服務如何啟動、資料如何流動、錯誤如何處理。這些特性之所以重要，是因為它們直接支撐了 Go 在高併發服務、worker、長連線與事件處理場景中的可維護性。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>說明 Go 為什麼偏好顯式控制流程&lt;/li>
&lt;li>看懂入口程式中依賴組裝的意圖&lt;/li>
&lt;li>區分「程式碼短」和「認知負擔低」&lt;/li>
&lt;li>用 Go 的風格閱讀現有服務&lt;/li>
&lt;/ol>
&lt;h2 id="為什麼這章在第零章">為什麼這章在第零章&lt;/h2>
&lt;p>如果工作負載、架構與 runtime 條件已經顯示 Go 是合適選項，接下來就要理解 Go 為什麼會長成現在這種樣子。簡單、顯式、少魔法是讓高併發服務在多人維護時仍能被快速理解的工程策略。&lt;/p>
&lt;hr>
&lt;h2 id="觀察go-程式的入口通常很直">【觀察】Go 程式的入口通常很直&lt;/h2>
&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="nx">events&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 class="mi">128&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">notifications&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">Notification&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">128&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="nx">repo&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">NewNotificationRepository&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">worker&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">NewWorker&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">repo&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">notifications&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">server&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">NewHTTPServer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">repo&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">notifications&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段程式沒有依賴注入框架，也沒有隱式容器。所有元件的建立順序、依賴關係、資料流向都直接寫在入口。&lt;/p>
&lt;p>這種寫法特別適合服務型應用：當系統需要處理很多並發請求、背景工作或外部事件時，入口越清楚，就越容易確認誰負責什麼、失敗時會停在哪裡。&lt;/p>
&lt;h2 id="判讀簡單是少猜幾件事">【判讀】簡單是少猜幾件事&lt;/h2>
&lt;p>對維護者來說，入口程式的主要工作是回答三個問題：&lt;/p>
&lt;ol>
&lt;li>哪些元件存在？&lt;/li>
&lt;li>元件彼此怎麼連接？&lt;/li>
&lt;li>程式關閉時誰負責停止？&lt;/li>
&lt;/ol>
&lt;p>Go 偏好把這些答案留在表面。當你看到 &lt;code>NewWorker(repo, events, notifications)&lt;/code>，你不需要先理解框架規則，就能知道 worker 依賴 repository，從事件 channel 讀資料，輸出通知到另一個 channel。抽象的責任是讓資料流更容易判讀；抽象若讓讀者需要猜測背後規則，就削弱了 Go 在長期維護上的主要價值。&lt;/p>
&lt;h2 id="策略閱讀-go-程式先找資料流">【策略】閱讀 Go 程式先找資料流&lt;/h2>
&lt;p>讀 Go 應用時，可以用以下順序降低認知負擔：&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>1&lt;/td>
 &lt;td>process 從哪裡開始？&lt;/td>
 &lt;td>入口程式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2&lt;/td>
 &lt;td>HTTP endpoint 在哪裡註冊？&lt;/td>
 &lt;td>route 註冊處&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>背景工作有哪些？&lt;/td>
 &lt;td>&lt;code>go ...&lt;/code> 呼叫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4&lt;/td>
 &lt;td>資料用什麼型別傳遞？&lt;/td>
 &lt;td>model 定義&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5&lt;/td>
 &lt;td>共享狀態由誰保護？&lt;/td>
 &lt;td>repository 或狀態元件&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這個順序先建立系統地圖，再深入單一函式，避免一開始就陷入細節。&lt;/p>
&lt;h2 id="執行用入口程式畫出資料流">【執行】用入口程式畫出資料流&lt;/h2>
&lt;p>即時通知服務可以先整理成這張資料流：&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">file / HTTP / timer ──&amp;gt; events ──&amp;gt; Worker ──&amp;gt; notifications
&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>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> Repository
&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>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> HTTP API&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這張圖比逐行閱讀更重要。它先回答「系統如何運作」，再讓你回到個別函式確認細節。&lt;/p></description><content:encoded><![CDATA[<p>Go 的核心取捨是降低讀程式的人需要同時記住的事情。它不追求語法表現力最大化，而是追求團隊在多年後仍能快速判讀服務如何啟動、資料如何流動、錯誤如何處理。這些特性之所以重要，是因為它們直接支撐了 Go 在高併發服務、worker、長連線與事件處理場景中的可維護性。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>說明 Go 為什麼偏好顯式控制流程</li>
<li>看懂入口程式中依賴組裝的意圖</li>
<li>區分「程式碼短」和「認知負擔低」</li>
<li>用 Go 的風格閱讀現有服務</li>
</ol>
<h2 id="為什麼這章在第零章">為什麼這章在第零章</h2>
<p>如果工作負載、架構與 runtime 條件已經顯示 Go 是合適選項，接下來就要理解 Go 為什麼會長成現在這種樣子。簡單、顯式、少魔法是讓高併發服務在多人維護時仍能被快速理解的工程策略。</p>
<hr>
<h2 id="觀察go-程式的入口通常很直">【觀察】Go 程式的入口通常很直</h2>
<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">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">128</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">notifications</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="nx">Notification</span><span class="p">,</span> <span class="mi">128</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">repo</span> <span class="o">:=</span> <span class="nf">NewNotificationRepository</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nx">worker</span> <span class="o">:=</span> <span class="nf">NewWorker</span><span class="p">(</span><span class="nx">repo</span><span class="p">,</span> <span class="nx">events</span><span class="p">,</span> <span class="nx">notifications</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">server</span> <span class="o">:=</span> <span class="nf">NewHTTPServer</span><span class="p">(</span><span class="nx">repo</span><span class="p">,</span> <span class="nx">notifications</span><span class="p">)</span></span></span></code></pre></div><p>這段程式沒有依賴注入框架，也沒有隱式容器。所有元件的建立順序、依賴關係、資料流向都直接寫在入口。</p>
<p>這種寫法特別適合服務型應用：當系統需要處理很多並發請求、背景工作或外部事件時，入口越清楚，就越容易確認誰負責什麼、失敗時會停在哪裡。</p>
<h2 id="判讀簡單是少猜幾件事">【判讀】簡單是少猜幾件事</h2>
<p>對維護者來說，入口程式的主要工作是回答三個問題：</p>
<ol>
<li>哪些元件存在？</li>
<li>元件彼此怎麼連接？</li>
<li>程式關閉時誰負責停止？</li>
</ol>
<p>Go 偏好把這些答案留在表面。當你看到 <code>NewWorker(repo, events, notifications)</code>，你不需要先理解框架規則，就能知道 worker 依賴 repository，從事件 channel 讀資料，輸出通知到另一個 channel。抽象的責任是讓資料流更容易判讀；抽象若讓讀者需要猜測背後規則，就削弱了 Go 在長期維護上的主要價值。</p>
<h2 id="策略閱讀-go-程式先找資料流">【策略】閱讀 Go 程式先找資料流</h2>
<p>讀 Go 應用時，可以用以下順序降低認知負擔：</p>
<table>
  <thead>
      <tr>
          <th>順序</th>
          <th>問題</th>
          <th>對應檔案</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>process 從哪裡開始？</td>
          <td>入口程式</td>
      </tr>
      <tr>
          <td>2</td>
          <td>HTTP endpoint 在哪裡註冊？</td>
          <td>route 註冊處</td>
      </tr>
      <tr>
          <td>3</td>
          <td>背景工作有哪些？</td>
          <td><code>go ...</code> 呼叫</td>
      </tr>
      <tr>
          <td>4</td>
          <td>資料用什麼型別傳遞？</td>
          <td>model 定義</td>
      </tr>
      <tr>
          <td>5</td>
          <td>共享狀態由誰保護？</td>
          <td>repository 或狀態元件</td>
      </tr>
  </tbody>
</table>
<p>這個順序先建立系統地圖，再深入單一函式，避免一開始就陷入細節。</p>
<h2 id="執行用入口程式畫出資料流">【執行】用入口程式畫出資料流</h2>
<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">file / HTTP / timer ──&gt; events ──&gt; Worker ──&gt; notifications
</span></span><span class="line"><span class="ln">2</span><span class="cl">                                      │
</span></span><span class="line"><span class="ln">3</span><span class="cl">                                      ▼
</span></span><span class="line"><span class="ln">4</span><span class="cl">                                  Repository
</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></span><span class="line"><span class="ln">7</span><span class="cl">                                   HTTP API</span></span></code></pre></div><p>這張圖比逐行閱讀更重要。它先回答「系統如何運作」，再讓你回到個別函式確認細節。</p>
]]></content:encoded></item><item><title>0.2 組合優先：小介面與明確依賴</title><link>https://tarrragon.github.io/blog/go/00-philosophy/composition/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/00-philosophy/composition/</guid><description>&lt;p>Go 組合的核心原則是用小型型別與小介面拼出行為。程式需要什麼能力，就依賴那個能力；型別擁有哪些資料，就把資料明確放在 struct 裡。這種設計讓高併發服務更容易拆解責任，也更容易在選型成立時維持邊界清楚。&lt;/p>
&lt;h2 id="為什麼這章在第零章">為什麼這章在第零章&lt;/h2>
&lt;p>當你的工作負載本來就適合 Go 時，真正需要確認的就不只是語法，而是 Go 如何幫你把服務邊界維持清楚。組合優先讓 &lt;code>main()&lt;/code>、constructor、handler、worker 與 repository 的責任能被直接看見，這是 Go 在服務型專案中很重要的可維護性來源。&lt;/p>
&lt;h2 id="組合先描述擁有什麼">組合先描述擁有什麼&lt;/h2>
&lt;p>struct 的核心責任是把一組資料與依賴放在同一個明確邊界內。Go 不用 class inheritance 表達「某個型別繼承另一個型別」，而是用欄位組合出需要的結構。&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">Logger&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">Info&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>&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">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">6&lt;/span>&lt;span class="cl"> &lt;span class="nx">addr&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">logger&lt;/span> &lt;span class="nx">Logger&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>Server&lt;/code> 擁有一個地址，也依賴一個 logger。這些資訊都在欄位上直接呈現，讀者不需要追蹤隱式容器或父類別初始化順序。&lt;/p>
&lt;p>當依賴變多時，struct 仍然應該只保留這個型別真正需要的依賴。把整個 application container 塞進 struct，通常會讓依賴邊界變模糊。&lt;/p>
&lt;h2 id="小介面先描述需要什麼">小介面先描述需要什麼&lt;/h2>
&lt;p>interface 的核心責任是描述呼叫端需要的行為。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">type&lt;/span> &lt;span class="nx">UserFinder&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">FindUser&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">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">UserHandler&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">finder&lt;/span> &lt;span class="nx">UserFinder&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>UserHandler&lt;/code> 不需要知道資料來自資料庫、快取或遠端 API。它只需要「可以用 id 找使用者」這個能力，因此介面只放 &lt;code>FindUser&lt;/code>。&lt;/p>
&lt;p>介面應由替換、測試或隔離需求推動。只有一個具體型別，而且沒有測試替身或替換需求時，先直接依賴具體型別通常更簡單。過度抽象的代價是把原本簡單的依賴藏成難追蹤的間接關係，反而比直接依賴更難閱讀。&lt;/p>
&lt;h2 id="依賴由外層組裝">依賴由外層組裝&lt;/h2>
&lt;p>Go 應用組裝依賴的核心策略是讓外層建立具體型別，內層只接收自己需要的依賴。常見位置是 &lt;code>main()&lt;/code> 或專門的 constructor。&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">NewUserHandler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">finder&lt;/span> &lt;span class="nx">UserFinder&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">UserHandler&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="nx">UserHandler&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">finder&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">finder&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="nx">db&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">NewDatabase&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;postgres://localhost/app&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">repository&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">NewUserRepository&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">db&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">handler&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">NewUserHandler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">repository&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">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">HandleFunc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/users/&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">handler&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ServeHTTP&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>這段程式讓資料流與依賴關係保持可見：repository 依賴 db，handler 依賴 repository 的查詢能力。Go 的組合方式偏好把這些關係寫出來，而不是藏在 framework magic 裡。&lt;/p>
&lt;h2 id="行為可以用-embedding-重用">行為可以用 embedding 重用&lt;/h2>
&lt;p>embedding 的核心用途是把一個型別的欄位或方法提升到外層型別。它是組合工具；若需要表達明確擁有關係，named field 會比繼承式心智模型更清楚。&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">AuditFields&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">CreatedAt&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"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">UpdatedAt&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"> 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">User&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">ID&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">Email&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">AuditFields&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>User&lt;/code> 透過 embedding 擁有 &lt;code>CreatedAt&lt;/code> 與 &lt;code>UpdatedAt&lt;/code>。這適合重用資料欄位，但不代表 &lt;code>User&lt;/code> 在概念上「繼承」了 &lt;code>AuditFields&lt;/code> 的完整行為。&lt;/p>
&lt;p>embedding 應該用在語意自然的地方。若提升方法會讓外層型別出現不該公開的能力，明確寫欄位名稱通常更安全。&lt;/p>
&lt;h2 id="組合讓測試替換更自然">組合讓測試替換更自然&lt;/h2>
&lt;p>組合的測試價值是可以替換依賴，而不需要啟動整個系統。只要 production code 依賴小介面，測試就能提供 fake。&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">fakeUserFinder&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">user&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">err&lt;/span> &lt;span class="kt">error&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">f&lt;/span> &lt;span class="nx">fakeUserFinder&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">FindUser&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">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"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">f&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">User&lt;/span>&lt;span class="p">{},&lt;/span> &lt;span class="nx">f&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">err&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="k">return&lt;/span> &lt;span class="nx">f&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">user&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">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>測試 handler 時，可以把 &lt;code>fakeUserFinder&lt;/code> 傳進去，專注檢查 HTTP response。這種替換的目標是讓測試只覆蓋當前邊界的行為。&lt;/p></description><content:encoded><![CDATA[<p>Go 組合的核心原則是用小型型別與小介面拼出行為。程式需要什麼能力，就依賴那個能力；型別擁有哪些資料，就把資料明確放在 struct 裡。這種設計讓高併發服務更容易拆解責任，也更容易在選型成立時維持邊界清楚。</p>
<h2 id="為什麼這章在第零章">為什麼這章在第零章</h2>
<p>當你的工作負載本來就適合 Go 時，真正需要確認的就不只是語法，而是 Go 如何幫你把服務邊界維持清楚。組合優先讓 <code>main()</code>、constructor、handler、worker 與 repository 的責任能被直接看見，這是 Go 在服務型專案中很重要的可維護性來源。</p>
<h2 id="組合先描述擁有什麼">組合先描述擁有什麼</h2>
<p>struct 的核心責任是把一組資料與依賴放在同一個明確邊界內。Go 不用 class inheritance 表達「某個型別繼承另一個型別」，而是用欄位組合出需要的結構。</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">Logger</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">Info</span><span class="p">(</span><span class="nx">message</span> <span class="kt">string</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">Server</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">addr</span>   <span class="kt">string</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"> <span class="nx">logger</span> <span class="nx">Logger</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>Server</code> 擁有一個地址，也依賴一個 logger。這些資訊都在欄位上直接呈現，讀者不需要追蹤隱式容器或父類別初始化順序。</p>
<p>當依賴變多時，struct 仍然應該只保留這個型別真正需要的依賴。把整個 application container 塞進 struct，通常會讓依賴邊界變模糊。</p>
<h2 id="小介面先描述需要什麼">小介面先描述需要什麼</h2>
<p>interface 的核心責任是描述呼叫端需要的行為。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">type</span> <span class="nx">UserFinder</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">FindUser</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">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">UserHandler</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">finder</span> <span class="nx">UserFinder</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>UserHandler</code> 不需要知道資料來自資料庫、快取或遠端 API。它只需要「可以用 id 找使用者」這個能力，因此介面只放 <code>FindUser</code>。</p>
<p>介面應由替換、測試或隔離需求推動。只有一個具體型別，而且沒有測試替身或替換需求時，先直接依賴具體型別通常更簡單。過度抽象的代價是把原本簡單的依賴藏成難追蹤的間接關係，反而比直接依賴更難閱讀。</p>
<h2 id="依賴由外層組裝">依賴由外層組裝</h2>
<p>Go 應用組裝依賴的核心策略是讓外層建立具體型別，內層只接收自己需要的依賴。常見位置是 <code>main()</code> 或專門的 constructor。</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">NewUserHandler</span><span class="p">(</span><span class="nx">finder</span> <span class="nx">UserFinder</span><span class="p">)</span> <span class="nx">UserHandler</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">UserHandler</span><span class="p">{</span><span class="nx">finder</span><span class="p">:</span> <span class="nx">finder</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="nx">db</span> <span class="o">:=</span> <span class="nf">NewDatabase</span><span class="p">(</span><span class="s">&#34;postgres://localhost/app&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"> <span class="nx">repository</span> <span class="o">:=</span> <span class="nf">NewUserRepository</span><span class="p">(</span><span class="nx">db</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"> <span class="nx">handler</span> <span class="o">:=</span> <span class="nf">NewUserHandler</span><span class="p">(</span><span class="nx">repository</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">http</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/users/&#34;</span><span class="p">,</span> <span class="nx">handler</span><span class="p">.</span><span class="nx">ServeHTTP</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>這段程式讓資料流與依賴關係保持可見：repository 依賴 db，handler 依賴 repository 的查詢能力。Go 的組合方式偏好把這些關係寫出來，而不是藏在 framework magic 裡。</p>
<h2 id="行為可以用-embedding-重用">行為可以用 embedding 重用</h2>
<p>embedding 的核心用途是把一個型別的欄位或方法提升到外層型別。它是組合工具；若需要表達明確擁有關係，named field 會比繼承式心智模型更清楚。</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">AuditFields</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">CreatedAt</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span>
</span></span><span class="line"><span class="ln"> 3</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"> 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">User</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">ID</span>    <span class="kt">string</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"> <span class="nx">Email</span> <span class="kt">string</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"> <span class="nx">AuditFields</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> 透過 embedding 擁有 <code>CreatedAt</code> 與 <code>UpdatedAt</code>。這適合重用資料欄位，但不代表 <code>User</code> 在概念上「繼承」了 <code>AuditFields</code> 的完整行為。</p>
<p>embedding 應該用在語意自然的地方。若提升方法會讓外層型別出現不該公開的能力，明確寫欄位名稱通常更安全。</p>
<h2 id="組合讓測試替換更自然">組合讓測試替換更自然</h2>
<p>組合的測試價值是可以替換依賴，而不需要啟動整個系統。只要 production code 依賴小介面，測試就能提供 fake。</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">fakeUserFinder</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">user</span> <span class="nx">User</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">fakeUserFinder</span><span class="p">)</span> <span class="nf">FindUser</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">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="nx">User</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">user</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></code></pre></div><p>測試 handler 時，可以把 <code>fakeUserFinder</code> 傳進去，專注檢查 HTTP response。這種替換的目標是讓測試只覆蓋當前邊界的行為。</p>
]]></content:encoded></item><item><title>0.3 錯誤處理：把失敗路徑寫出來</title><link>https://tarrragon.github.io/blog/go/00-philosophy/error-thinking/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/00-philosophy/error-thinking/</guid><description>&lt;p>Go 錯誤處理的核心原則是把失敗路徑明確寫在程式流程中。&lt;code>if err != nil&lt;/code> 看起來重複，但它讓每一步可能失敗的地方都可見，也讓讀者能直接知道失敗時程式會怎麼結束。對需要長時間運行、並發處理、背景工作或即時請求回應的服務來說，這種顯式失敗路徑比隱式例外更容易維護。&lt;/p>
&lt;h2 id="為什麼這章在第零章">為什麼這章在第零章&lt;/h2>
&lt;p>如果你的場景已經把 Go 推向服務型系統，錯誤處理就是維持服務穩定的核心能力。這一章要先建立的是：Go 會要求你把失敗說清楚，因為在高併發或長時間運行的情境下，模糊的失敗行為會比清楚的錯誤訊息更難排查。&lt;/p>
&lt;h2 id="error-是普通回傳值">error 是普通回傳值&lt;/h2>
&lt;p>Go 的 &lt;code>error&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">LoadConfig&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">path&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">Config&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">data&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">os&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">ReadFile&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">path&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">Config&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">Errorf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;read config %q: %w&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">path&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="kd">var&lt;/span> &lt;span class="nx">config&lt;/span> &lt;span class="nx">Config&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">json&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Unmarshal&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">config&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">Config&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">Errorf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;parse config %q: %w&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">path&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">config&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">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>這段程式有兩個失敗點：讀檔失敗與 JSON 解析失敗。每個失敗點都立刻處理並回傳，正常流程則留在函式底部。&lt;/p>
&lt;h2 id="早期返回讓正常流程清楚">早期返回讓正常流程清楚&lt;/h2>
&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">CreateUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">email&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">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">email&lt;/span> &lt;span class="p">=&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">email&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">email&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s">&amp;#34;&amp;#34;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">User&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">Errorf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;email is required&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="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="p">!&lt;/span>&lt;span class="nx">strings&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">email&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;@&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"> 8&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">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;invalid email&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="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">User&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">Email&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">email&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="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;code>fmt.Errorf&lt;/code> 搭配 &lt;code>%w&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">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">user&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">2&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 user %q: %w&amp;#34;&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">Email&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">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>&lt;code>save user &amp;quot;alice@example.com&amp;quot;&lt;/code> 是當前操作脈絡，原始錯誤則被 &lt;code>%w&lt;/code> 保留下來。呼叫端可以印出完整錯誤，也可以用 &lt;code>errors.Is&lt;/code> 或 &lt;code>errors.As&lt;/code> 檢查特定錯誤。&lt;/p>
&lt;p>錯誤包裝應提供新的操作脈絡。&lt;code>fmt.Errorf(&amp;quot;failed: %w&amp;quot;, err)&lt;/code> 這類包裝沒有資訊量；好的錯誤訊息應該回答「做什麼失敗」與「關鍵資料是什麼」。&lt;/p>
&lt;h2 id="http-handler-要把錯誤轉成協定語意">HTTP handler 要把錯誤轉成協定語意&lt;/h2>
&lt;p>HTTP handler 的錯誤處理核心是把內部錯誤轉成明確 status code。輸入格式錯誤通常是 &lt;code>400&lt;/code>，找不到資料是 &lt;code>404&lt;/code>，不支援的方法是 &lt;code>405&lt;/code>，未預期內部錯誤才是 &lt;code>500&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">handleCreateUser&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"> 2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">req&lt;/span> &lt;span class="nx">createUserRequest&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="nx">json&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewDecoder&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">Body&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nf">Decode&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&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="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="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;invalid json&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">StatusBadRequest&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">user&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">CreateUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Email&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">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="nx">err&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">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">StatusBadRequest&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>&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="nf">writeJSON&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">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">StatusCreated&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">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>handler 的錯誤對應應反映協定語意。錯誤來自呼叫端輸入時，回 &lt;code>400&lt;/code> 才能讓 client 知道應該修正 request；未預期內部錯誤才應進入 &lt;code>500&lt;/code>。&lt;/p>
&lt;h2 id="log-應該放在有處理責任的位置">log 應該放在有處理責任的位置&lt;/h2>
&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>、重試、轉成 HTTP response 或讓程式結束。&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="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">config&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">LoadConfig&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;config.json&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">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="k">return&lt;/span> &lt;span class="nf">StartServer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">config&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>&lt;/span>&lt;span class="line">&lt;span class="ln">10&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">11&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">run&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">12&lt;/span>&lt;span class="cl"> &lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Fatal&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">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>LoadConfig&lt;/code> 回傳錯誤比直接 &lt;code>log.Fatal&lt;/code> 更符合責任邊界，因為它不知道呼叫端是否想重試、使用預設值或結束程式。&lt;code>main&lt;/code> 是 process 邊界，才適合決定失敗時結束。&lt;/p>
&lt;h2 id="小結">小結&lt;/h2>
&lt;p>Go 錯誤處理的價值是讓失敗路徑可讀、可測、可追蹤。每個 &lt;code>if err != nil&lt;/code> 都是一個明確的決策點：是否補脈絡、是否轉成協定狀態、是否記錄、是否終止流程。這種顯式設計是 Go 長期維護性的核心之一。&lt;/p></description><content:encoded><![CDATA[<p>Go 錯誤處理的核心原則是把失敗路徑明確寫在程式流程中。<code>if err != nil</code> 看起來重複，但它讓每一步可能失敗的地方都可見，也讓讀者能直接知道失敗時程式會怎麼結束。對需要長時間運行、並發處理、背景工作或即時請求回應的服務來說，這種顯式失敗路徑比隱式例外更容易維護。</p>
<h2 id="為什麼這章在第零章">為什麼這章在第零章</h2>
<p>如果你的場景已經把 Go 推向服務型系統，錯誤處理就是維持服務穩定的核心能力。這一章要先建立的是：Go 會要求你把失敗說清楚，因為在高併發或長時間運行的情境下，模糊的失敗行為會比清楚的錯誤訊息更難排查。</p>
<h2 id="error-是普通回傳值">error 是普通回傳值</h2>
<p>Go 的 <code>error</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">LoadConfig</span><span class="p">(</span><span class="nx">path</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="nx">Config</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">data</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">os</span><span class="p">.</span><span class="nf">ReadFile</span><span class="p">(</span><span class="nx">path</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">Config</span><span class="p">{},</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;read config %q: %w&#34;</span><span class="p">,</span> <span class="nx">path</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="kd">var</span> <span class="nx">config</span> <span class="nx">Config</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">json</span><span class="p">.</span><span class="nf">Unmarshal</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">config</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">     <span class="k">return</span> <span class="nx">Config</span><span class="p">{},</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;parse config %q: %w&#34;</span><span class="p">,</span> <span class="nx">path</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"> <span class="k">return</span> <span class="nx">config</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>這段程式有兩個失敗點：讀檔失敗與 JSON 解析失敗。每個失敗點都立刻處理並回傳，正常流程則留在函式底部。</p>
<h2 id="早期返回讓正常流程清楚">早期返回讓正常流程清楚</h2>
<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">CreateUser</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="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">email</span> <span class="p">=</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">TrimSpace</span><span class="p">(</span><span class="nx">email</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">email</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="k">return</span> <span class="nx">User</span><span class="p">{},</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;email is required&#34;</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="p">!</span><span class="nx">strings</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="nx">email</span><span class="p">,</span> <span class="s">&#34;@&#34;</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">return</span> <span class="nx">User</span><span class="p">{},</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;invalid email&#34;</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="nx">User</span><span class="p">{</span><span class="nx">Email</span><span class="p">:</span> <span class="nx">email</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="p">}</span></span></span></code></pre></div><p>這個函式先處理空字串與格式錯誤，最後才建立使用者。讀者不需要進入深層巢狀條件，就能知道哪些資料不能通過。</p>
<h2 id="包裝錯誤要補上操作脈絡">包裝錯誤要補上操作脈絡</h2>
<p>錯誤包裝的核心責任是保留原始錯誤，同時補上當前操作脈絡。<code>fmt.Errorf</code> 搭配 <code>%w</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">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">user</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">2</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 user %q: %w&#34;</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">Email</span><span class="p">,</span> <span class="nx">err</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>save user &quot;alice@example.com&quot;</code> 是當前操作脈絡，原始錯誤則被 <code>%w</code> 保留下來。呼叫端可以印出完整錯誤，也可以用 <code>errors.Is</code> 或 <code>errors.As</code> 檢查特定錯誤。</p>
<p>錯誤包裝應提供新的操作脈絡。<code>fmt.Errorf(&quot;failed: %w&quot;, err)</code> 這類包裝沒有資訊量；好的錯誤訊息應該回答「做什麼失敗」與「關鍵資料是什麼」。</p>
<h2 id="http-handler-要把錯誤轉成協定語意">HTTP handler 要把錯誤轉成協定語意</h2>
<p>HTTP handler 的錯誤處理核心是把內部錯誤轉成明確 status code。輸入格式錯誤通常是 <code>400</code>，找不到資料是 <code>404</code>，不支援的方法是 <code>405</code>，未預期內部錯誤才是 <code>500</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">handleCreateUser</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"> 2</span><span class="cl"> <span class="kd">var</span> <span class="nx">req</span> <span class="nx">createUserRequest</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="nx">json</span><span class="p">.</span><span class="nf">NewDecoder</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">Body</span><span class="p">).</span><span class="nf">Decode</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">req</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"> 4</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;invalid json&#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"> 5</span><span class="cl">  <span class="k">return</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"> <span class="nx">user</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">CreateUser</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">Email</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">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="nx">err</span><span class="p">.</span><span class="nf">Error</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">11</span><span class="cl">  <span class="k">return</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="nf">writeJSON</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusCreated</span><span class="p">,</span> <span class="nx">user</span><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>handler 的錯誤對應應反映協定語意。錯誤來自呼叫端輸入時，回 <code>400</code> 才能讓 client 知道應該修正 request；未預期內部錯誤才應進入 <code>500</code>。</p>
<h2 id="log-應該放在有處理責任的位置">log 應該放在有處理責任的位置</h2>
<p>錯誤記錄的核心規則是誰負責處理錯誤，誰才記錄錯誤。底層函式通常回傳錯誤，上層邊界再決定要 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>、重試、轉成 HTTP response 或讓程式結束。</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="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"> <span class="nx">config</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">LoadConfig</span><span class="p">(</span><span class="s">&#34;config.json&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="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="k">return</span> <span class="nf">StartServer</span><span class="p">(</span><span class="nx">config</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></span><span class="line"><span class="ln">10</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">11</span><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">run</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">12</span><span class="cl">  <span class="nx">log</span><span class="p">.</span><span class="nf">Fatal</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><code>LoadConfig</code> 回傳錯誤比直接 <code>log.Fatal</code> 更符合責任邊界，因為它不知道呼叫端是否想重試、使用預設值或結束程式。<code>main</code> 是 process 邊界，才適合決定失敗時結束。</p>
<h2 id="小結">小結</h2>
<p>Go 錯誤處理的價值是讓失敗路徑可讀、可測、可追蹤。每個 <code>if err != nil</code> 都是一個明確的決策點：是否補脈絡、是否轉成協定狀態、是否記錄、是否終止流程。這種顯式設計是 Go 長期維護性的核心之一。</p>
]]></content:encoded></item><item><title>0.4 什麼時候選 Go</title><link>https://tarrragon.github.io/blog/go/00-philosophy/selecting-go/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/00-philosophy/selecting-go/</guid><description>&lt;p>選擇 Go 的核心判斷是工作場景是否需要長時間運行、明確邊界、穩定併發與簡單部署。這一章用工程條件判斷 Go 是否適合目前問題；若工作更依賴框架模板、快速表單 CRUD、動態行為或大量 runtime magic，其他語言或框架可能更符合需求。&lt;/p>
&lt;p>選型文章的目標是建立判斷路徑。讀者未來面對的可能是 API service、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> server、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> worker、內部工具或資料處理流程；同一個語言在不同工作負載下會有不同價值。先理解判斷條件，再進入語法細節，才不會把「我會寫 Go」誤解成「所有問題都該用 Go」。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>判斷哪些工作負載適合 Go&lt;/li>
&lt;li>看出哪些系統型態特別適合 Go&lt;/li>
&lt;li>區分 Go 的強項與不適合硬上的場景&lt;/li>
&lt;li>用工程條件取代語言偏好來做選型&lt;/li>
&lt;li>為後續語法與實作章節建立正確的閱讀順序&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察先看工作負載再看語言">【觀察】先看工作負載，再看語言&lt;/h2>
&lt;p>Go 最常被選中的場景，是需要穩定處理大量服務型工作的系統。這些工作通常包含等待外部 I/O、管理長生命週期、持續處理背景任務或維持清楚服務邊界：&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>高併發 I/O&lt;/td>
 &lt;td>goroutine 成本低，適合大量等待型工作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>長連線服務&lt;/td>
 &lt;td>容易管理生命週期、取消與資源清理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>背景 worker&lt;/td>
 &lt;td>可以把工作拆成小單位並持續處理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事件處理&lt;/td>
 &lt;td>channel、select 與明確邊界很適合事件流&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>API service&lt;/td>
 &lt;td>標準庫直接支撐 HTTP、context、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>如果工作本質就是一堆等待外部 I/O 的操作，Go 往往比把整個問題放進單線程迴圈更自然。&lt;/p>
&lt;h3 id="高併發-io先看服務是否長時間等待外部回應">高併發 I/O：先看服務是否長時間等待外部回應&lt;/h3>
&lt;p>高併發 I/O 的核心特徵是「同時有很多工作在等待網路、檔案、資料庫或外部 API」。判斷時可以先看 request 的時間花在哪裡：如果大部分時間都在等下游回應，CPU 計算只占一小段，這就是等待型工作。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>API gateway 同時轉送請求到多個下游服務&lt;/li>
&lt;li>價格比較網站同時查詢多個供應商 API&lt;/li>
&lt;li>檔案上傳服務同時處理大量 client 的上傳進度&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook&lt;/a> receiver 同時接收付款、物流、通知平台的 callback&lt;/li>
&lt;/ul>
&lt;p>這類服務的主要工程問題是「同時有很多等待中的工作，而且每個工作都需要容量邊界」。Go 的 goroutine 可以讓每個等待中的 request 有清楚的執行單位，&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> 與取消，channel 或 semaphore 可以限制同時打到下游的數量。Go 的價值在於讓等待、取消與容量控制都變成明確程式結構。&lt;/p>
&lt;h3 id="長連線服務先看-client-是否會持續留在線上">長連線服務：先看 client 是否會持續留在線上&lt;/h3>
&lt;p>長連線服務的核心特徵是「client 連上來之後不會立刻結束」。判斷時可以看連線是否需要維持數分鐘、數小時，甚至整個工作階段；只要 server 要持續追蹤 client 狀態，就會遇到生命週期管理問題。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>即時聊天室與客服對話&lt;/li>
&lt;li>線上協作文件的多人編輯狀態&lt;/li>
&lt;li>股票、運動比分或遊戲狀態的即時推送&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> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sse/" data-link-title="Server-Sent Events (SSE)" data-link-desc="說明 SSE 如何透過 HTTP 長連線向 client 單向推送事件">SSE&lt;/a> 更新&lt;/li>
&lt;/ul>
&lt;p>這類服務的主要工程問題是「連線會斷、client 會變慢、server 要清理資源」。Go 很適合把 read loop、write loop、heartbeat、subscription、shutdown 拆成不同 goroutine 與 channel 邊界。當連線失效時，&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> 與 unregister 流程可以把清理責任收斂到同一個地方。&lt;/p>
&lt;h3 id="背景-worker先看工作是否不適合卡住-request">背景 worker：先看工作是否不適合卡住 request&lt;/h3>
&lt;p>背景 worker 的核心特徵是「工作需要持續處理，並且適合從使用者 request 的等待時間中拆出來」。判斷時可以看某個操作是否需要重試、排程、批次處理，或等待外部系統完成。&lt;/p>
&lt;p>接近真實網路服務的例子包括：&lt;/p>
&lt;ul>
&lt;li>寄送 email、簡訊或推播通知&lt;/li>
&lt;li>影片轉檔、圖片壓縮與報表產生&lt;/li>
&lt;li>每晚同步 CRM、金流或庫存資料&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> message 並更新內部狀態&lt;/li>
&lt;/ul>
&lt;p>這類服務的主要工程問題是「工作要能開始、停止、重試、記錄錯誤並控制速率」。Go 的 &lt;code>Run(ctx)&lt;/code>、ticker、&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>、channel &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> 與 structured &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a> 可以把 worker 生命週期寫清楚。Go 的好處是讓背景流程仍然可取消、可觀測、可測試，而不只是把工作丟到背景。&lt;/p>
&lt;h3 id="事件處理先看系統是否圍繞已發生的事流動">事件處理：先看系統是否圍繞已發生的事流動&lt;/h3>
&lt;p>事件處理的核心特徵是「系統收到某件已發生的事，再依規則更新狀態或觸發後續行為」。判斷時可以看資料是否常以 &lt;code>created&lt;/code>、&lt;code>updated&lt;/code>、&lt;code>failed&lt;/code>、&lt;code>completed&lt;/code> 這類事實形式流動。&lt;/p></description><content:encoded><![CDATA[<p>選擇 Go 的核心判斷是工作場景是否需要長時間運行、明確邊界、穩定併發與簡單部署。這一章用工程條件判斷 Go 是否適合目前問題；若工作更依賴框架模板、快速表單 CRUD、動態行為或大量 runtime magic，其他語言或框架可能更符合需求。</p>
<p>選型文章的目標是建立判斷路徑。讀者未來面對的可能是 API service、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> server、<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> worker、內部工具或資料處理流程；同一個語言在不同工作負載下會有不同價值。先理解判斷條件，再進入語法細節，才不會把「我會寫 Go」誤解成「所有問題都該用 Go」。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>判斷哪些工作負載適合 Go</li>
<li>看出哪些系統型態特別適合 Go</li>
<li>區分 Go 的強項與不適合硬上的場景</li>
<li>用工程條件取代語言偏好來做選型</li>
<li>為後續語法與實作章節建立正確的閱讀順序</li>
</ol>
<hr>
<h2 id="觀察先看工作負載再看語言">【觀察】先看工作負載，再看語言</h2>
<p>Go 最常被選中的場景，是需要穩定處理大量服務型工作的系統。這些工作通常包含等待外部 I/O、管理長生命週期、持續處理背景任務或維持清楚服務邊界：</p>
<table>
  <thead>
      <tr>
          <th>工作型態</th>
          <th>為什麼適合 Go</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>高併發 I/O</td>
          <td>goroutine 成本低，適合大量等待型工作</td>
      </tr>
      <tr>
          <td>長連線服務</td>
          <td>容易管理生命週期、取消與資源清理</td>
      </tr>
      <tr>
          <td>背景 worker</td>
          <td>可以把工作拆成小單位並持續處理</td>
      </tr>
      <tr>
          <td>事件處理</td>
          <td>channel、select 與明確邊界很適合事件流</td>
      </tr>
      <tr>
          <td>API service</td>
          <td>標準庫直接支撐 HTTP、context、<a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 與 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a></td>
      </tr>
  </tbody>
</table>
<p>如果工作本質就是一堆等待外部 I/O 的操作，Go 往往比把整個問題放進單線程迴圈更自然。</p>
<h3 id="高併發-io先看服務是否長時間等待外部回應">高併發 I/O：先看服務是否長時間等待外部回應</h3>
<p>高併發 I/O 的核心特徵是「同時有很多工作在等待網路、檔案、資料庫或外部 API」。判斷時可以先看 request 的時間花在哪裡：如果大部分時間都在等下游回應，CPU 計算只占一小段，這就是等待型工作。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>API gateway 同時轉送請求到多個下游服務</li>
<li>價格比較網站同時查詢多個供應商 API</li>
<li>檔案上傳服務同時處理大量 client 的上傳進度</li>
<li><a href="/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook</a> receiver 同時接收付款、物流、通知平台的 callback</li>
</ul>
<p>這類服務的主要工程問題是「同時有很多等待中的工作，而且每個工作都需要容量邊界」。Go 的 goroutine 可以讓每個等待中的 request 有清楚的執行單位，<code>context</code> 可以控制 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 與取消，channel 或 semaphore 可以限制同時打到下游的數量。Go 的價值在於讓等待、取消與容量控制都變成明確程式結構。</p>
<h3 id="長連線服務先看-client-是否會持續留在線上">長連線服務：先看 client 是否會持續留在線上</h3>
<p>長連線服務的核心特徵是「client 連上來之後不會立刻結束」。判斷時可以看連線是否需要維持數分鐘、數小時，甚至整個工作階段；只要 server 要持續追蹤 client 狀態，就會遇到生命週期管理問題。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>即時聊天室與客服對話</li>
<li>線上協作文件的多人編輯狀態</li>
<li>股票、運動比分或遊戲狀態的即時推送</li>
<li>後台任務進度頁面的 <a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> / <a href="/blog/backend/knowledge-cards/sse/" data-link-title="Server-Sent Events (SSE)" data-link-desc="說明 SSE 如何透過 HTTP 長連線向 client 單向推送事件">SSE</a> 更新</li>
</ul>
<p>這類服務的主要工程問題是「連線會斷、client 會變慢、server 要清理資源」。Go 很適合把 read loop、write loop、heartbeat、subscription、shutdown 拆成不同 goroutine 與 channel 邊界。當連線失效時，<code>context</code>、<a href="/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">deadline</a> 與 unregister 流程可以把清理責任收斂到同一個地方。</p>
<h3 id="背景-worker先看工作是否不適合卡住-request">背景 worker：先看工作是否不適合卡住 request</h3>
<p>背景 worker 的核心特徵是「工作需要持續處理，並且適合從使用者 request 的等待時間中拆出來」。判斷時可以看某個操作是否需要重試、排程、批次處理，或等待外部系統完成。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>寄送 email、簡訊或推播通知</li>
<li>影片轉檔、圖片壓縮與報表產生</li>
<li>每晚同步 CRM、金流或庫存資料</li>
<li>消費 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> message 並更新內部狀態</li>
</ul>
<p>這類服務的主要工程問題是「工作要能開始、停止、重試、記錄錯誤並控制速率」。Go 的 <code>Run(ctx)</code>、ticker、<a href="/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool</a>、channel <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> 與 structured <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 可以把 worker 生命週期寫清楚。Go 的好處是讓背景流程仍然可取消、可觀測、可測試，而不只是把工作丟到背景。</p>
<h3 id="事件處理先看系統是否圍繞已發生的事流動">事件處理：先看系統是否圍繞已發生的事流動</h3>
<p>事件處理的核心特徵是「系統收到某件已發生的事，再依規則更新狀態或觸發後續行為」。判斷時可以看資料是否常以 <code>created</code>、<code>updated</code>、<code>failed</code>、<code>completed</code> 這類事實形式流動。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>訂單付款成功後更新訂單狀態並發送通知</li>
<li>使用者註冊完成後建立歡迎流程與分析事件</li>
<li>CI job 狀態改變後推送到 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a></li>
<li>IoT 裝置上報 sensor reading 後觸發告警</li>
</ul>
<p>這類服務的主要工程問題是「事件來源多、順序可能不同、重複事件需要處理」。Go 的型別可以定義穩定 event envelope，channel 或 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> adapter 可以把來源收斂到 processor，processor 再集中處理 validation、dedup、state transition 與 publish。Go 的價值在於讓事件流的每一段責任清楚可測。</p>
<h3 id="api-service先看服務是否需要清楚的-request-邊界">API service：先看服務是否需要清楚的 request 邊界</h3>
<p>API service 的核心特徵是「外部 client 用明確 request 取得資料或要求系統執行動作」。判斷時可以看服務是否需要穩定路由、輸入驗證、timeout、error response、<a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 與 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a>。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>手機 App 的會員、訂單、通知 API</li>
<li>SaaS 產品提供給客戶整合的 public API</li>
<li>內部微服務之間的 HTTP/gRPC API</li>
<li><a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a> 查詢目前狀態與操作後端任務的 API</li>
</ul>
<p>這類服務的主要工程問題是「request 進來後要有清楚邊界」。Go 標準庫的 <code>net/http</code>、<code>context</code>、<code>encoding/json</code>、<code>log/slog</code> 與 testing package 已經提供服務骨架需要的基本能力。當 API 邊界清楚時，handler 可以專注在傳輸格式，usecase 處理行為規則，repository 或 external client 處理資料依賴。</p>
<p>例如一個通知服務需要同時處理三件事：接收 HTTP callback、把事件放進背景處理流程、再把結果推送給已訂閱的 client。這個服務的主要成本通常在於同時等待網路、管理 client 連線、控制 queue 滿載與清理失效資源；單次計算反而只是其中一小部分。Go 的 goroutine、channel、context 與標準庫 HTTP 可以把這些生命週期寫成明確的程式結構。</p>
<p>相反地，一個只需要三個表單頁面、幾個後台列表和現成權限模板的內部管理系統，主要成本可能在 UI、表單驗證、ORM convention 與後台 scaffolding。這種工作也能用 Go 完成，但選型時應先問：「主要成本是在服務生命週期，還是在框架已經提供的業務頁面組裝？」這個問題比語言效能更接近真正瓶頸。</p>
<h2 id="判讀架構邊界是否清楚">【判讀】架構邊界是否清楚</h2>
<p>Go 特別適合邊界清楚的後端服務。當一個系統可以自然拆成輸入、協調、狀態、輸出幾層時，Go 的 struct、interface、package 與明確依賴會讓責任更容易看見。</p>
<p>例如以下這類系統通常是 Go 的好候選：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> 即時服務</li>
<li>notification service</li>
<li>queue <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a></li>
<li>log / event pipeline</li>
<li>需要清楚 ports/adapters 的 backend service</li>
</ul>
<p>產品若高度依賴框架提供的大量現成功能，或核心價值在於快速拼接大量業務頁面與模板，選型時應把框架生態列為主要條件。這類情境可以先評估 Python、Ruby、JavaScript/TypeScript 或其他更貼近既有生態的方案。</p>
<p>判斷架構邊界時，可以先畫出資料如何通過系統：</p>
<ol>
<li>外部請求或事件從哪裡進來</li>
<li>哪一層負責驗證與轉換</li>
<li>哪一層負責狀態轉移或業務規則</li>
<li>哪一層負責回應、推送或記錄</li>
</ol>
<p>如果這四個問題能自然拆成幾個責任清楚的元件，Go 會讓這些邊界很容易被程式碼表達。handler 可以處理傳輸格式，usecase 可以處理行為規則，repository 或 state owner 可以處理狀態，publisher 或 response layer 可以處理輸出。這種設計不需要大型框架先定義所有路徑，Go 的簡單型別與小介面就能支撐。</p>
<p>如果團隊還在探索商業流程，連資料模型、頁面流程與權限規則都會每天改，框架的 convention 可能更重要。這時候語言選型的重點是「顯式設計的成本是否值得現在承擔」。Go 會鼓勵你把邊界寫清楚；當邊界本身仍在頻繁變動，這種清楚有時會變成前置設計成本。</p>
<h2 id="策略runtime-與部署條件也是選型的一部分">【策略】runtime 與部署條件也是選型的一部分</h2>
<p>Go 的優勢不只是語法，還包括 runtime 與部署形態：</p>
<ul>
<li>單一 binary，部署流程簡單</li>
<li>啟動速度快，適合 container 與短週期交付</li>
<li>標準庫完整，很多服務不需要先找一堆框架</li>
<li>可讀性高，長期維護成本較容易控制</li>
</ul>
<p>如果你的團隊很在意：</p>
<ul>
<li>記憶體用量</li>
<li>啟動時間</li>
<li>觀測與除錯</li>
<li>服務在高流量下的穩定性</li>
</ul>
<p>那 Go 的工程價值就會很明顯。</p>
<p>部署條件會影響語言價值，因為服務最終要在開發機之外的環境長期運行。假設一個團隊要把多個小服務放進 container，每個服務都需要 <a href="/blog/backend/knowledge-cards/health-check-liveness/" data-link-title="Liveness" data-link-desc="說明平台如何判斷 process 是否仍然存活，以及何時應重啟">health check</a>、timeout、structured log、<a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a> 與固定資源限制。Go 的單一 binary 和標準庫讓這些能力可以用相對少的外部依賴完成；服務啟動、部署與回滾也比較容易被平台工程師理解。</p>
<p>另一個常見例子是 CLI 工具或 sidecar service。這類程式常被放進 CI、Kubernetes job、systemd service 或部署腳本中。Go 編譯後的 binary 可以降低 runtime 安裝與版本衝突問題。這是交付形態優勢：當程式要在很多環境中穩定啟動，少一層 runtime 依賴就是一個可觀的工程收益。</p>
<h2 id="執行哪些情況要先評估其他方案">【執行】哪些情況要先評估其他方案</h2>
<p>選型的執行規則是先確認主要瓶頸，再決定語言。以下情境通常應先評估其他語言、框架或平台能力：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>極度偏 CRUD 模板系統</td>
          <td>框架生態可能比語言特性更重要</td>
      </tr>
      <tr>
          <td>大量動態行為與 runtime 配置</td>
          <td>Go 會要求更多顯式設計</td>
      </tr>
      <tr>
          <td>團隊主要目標是快速試錯</td>
          <td>Go 的工程紀律可能比腳本型語言更有前置成本</td>
      </tr>
      <tr>
          <td>主要瓶頸在前端整合流程</td>
          <td>主要解法在前端工具鏈、元件生態與產品流程</td>
      </tr>
  </tbody>
</table>
<p>Go 可以處理其中部分情境，但它的工程價值未必對準主要瓶頸。當主要成本在框架生態、動態流程或前端整合時，下一步應先比較那些領域更成熟的工具。</p>
<h3 id="極度偏-crud-模板系統先看頁面是否圍繞資料表轉">極度偏 CRUD 模板系統：先看頁面是否圍繞資料表轉</h3>
<p>CRUD 模板系統的核心特徵是「大部分功能都在新增、查詢、修改、刪除資料」。判斷時可以先看產品畫面：如果主要頁面都是列表、篩選、表單、詳情頁、權限設定與匯出報表，系統很可能偏 CRUD。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>電商平台的商品、訂單、會員、優惠券後台</li>
<li>餐廳訂位系統的店家、桌位、時段、訂單管理</li>
<li>客服工單系統的 ticket 列表、狀態修改、負責人指派</li>
<li>活動報名系統的活動、票種、參加者、付款狀態管理</li>
</ul>
<p>這類系統的主要工作通常在「快速產生穩定後台功能」。框架如果已經提供 <a href="/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">authentication</a>、<a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">authorization</a>、ORM、form validation、admin scaffolding、pagination 與 search，團隊會先從框架生態獲得產能。Go 仍然可以負責其中的高流量 API、背景同步、付款 callback 或報表生成，但整個後台產品的主體未必需要先用 Go 開始。</p>
<p>判斷問題可以這樣問：如果拿掉後台表單、列表與權限頁，系統還剩下什麼核心工程問題？如果答案很少，框架模板可能就是主要能力。</p>
<h3 id="大量動態行為與-runtime-配置先看規則是否由使用者定義">大量動態行為與 runtime 配置：先看規則是否由使用者定義</h3>
<p>動態行為系統的核心特徵是「行為在執行期間由設定、腳本、規則或使用者操作改變」。判斷時可以先觀察：工程師是否常常需要讓非工程使用者自己新增欄位、調整流程、改驗證規則或配置通知條件。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>表單建置器：使用者可以自己新增欄位、驗證規則與送出後動作</li>
<li>工作流系統：管理者可以設定「訂單超過金額就送審」、「狀態改變就寄信」</li>
<li>CMS：編輯可以建立不同內容模型、欄位與發布流程</li>
<li>行銷自動化工具：使用者可以用條件組合出不同受眾與觸發規則</li>
</ul>
<p>這類系統的主要工程問題在於「如何安全表達動態規則」。Go 的靜態型別會鼓勵你把資料結構與行為先定義清楚；這對穩定服務很有價值，但對高度動態的產品，可能需要額外設計 rule engine、schema registry、plugin boundary 或 DSL。若產品核心就是讓使用者自由配置流程，選型時應把動態模型能力列為主要評估項目。</p>
<p>Go 的合理位置通常在規則執行引擎、事件處理器或高併發 delivery service。管理介面、規則編輯器與 schema 設計器則可能更依賴前端與動態框架生態。</p>
<h3 id="團隊主要目標是快速試錯先看需求是否每天改方向">團隊主要目標是快速試錯：先看需求是否每天改方向</h3>
<p>快速試錯情境的核心特徵是「產品問題尚未被驗證」。判斷時可以觀察需求文件與會議紀錄：如果資料模型、頁面流程、定價方式、權限規則與使用者角色都還在頻繁改，團隊此時最需要的是降低改方向的成本。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>新創產品的第一版 MVP</li>
<li>內部工具的概念驗證</li>
<li>尚未確定商業模式的 marketplace</li>
<li>正在測試轉換率的報名、購買或 onboarding 流程</li>
</ul>
<p>這類階段的主要問題是「學到使用者真正需要什麼」。腳本型語言、full-stack framework、低程式碼工具或現成 SaaS 可能更適合先驗證流程。Go 的型別、錯誤處理與顯式邊界會提高長期可維護性，但在問題尚未穩定時，過早建立完整邊界可能會讓每次方向調整都需要較多工程變更。</p>
<p>Go 仍然可以在試錯產品中出現，但通常適合放在已經確定會留下的部分，例如 <a href="/blog/backend/knowledge-cards/webhook/" data-link-title="Webhook" data-link-desc="說明外部系統回呼事件的接收、驗證與處理邊界">webhook</a> receiver、背景任務、匯入匯出服務或需要穩定運行的小型 API。產品流程本身可以先用更容易改動的工具探索。</p>
<h3 id="主要瓶頸在前端整合流程先看使用者價值是否發生在介面">主要瓶頸在前端整合流程：先看使用者價值是否發生在介面</h3>
<p>前端整合型產品的核心特徵是「使用者價值主要發生在互動介面」。判斷時可以先看產品成功條件：如果最重要的是頁面轉換率、互動動畫、表單體驗、SEO、設計系統、第三方前端 SDK 或多裝置呈現，後端語言通常只是整體解法的一部分。</p>
<p>接近真實網路服務的例子包括：</p>
<ul>
<li>行銷 landing page 與 A/B testing 流程</li>
<li>電商結帳頁、購物車、折扣碼與付款 UI</li>
<li>內容網站、文件站、會員訂閱頁</li>
<li>需要大量拖拉、預覽、即時編輯的設計工具</li>
</ul>
<p>這類系統的主要工程問題在於前端狀態、元件設計、瀏覽器限制、SEO、分析追蹤與使用者流程。Go 可以提供穩定 API、授權、訂單狀態或事件接收，但選型時要先確認後端 runtime 是否真的在解主要瓶頸。若瓶頸集中在 UI 與產品流程，Next.js、Remix、Nuxt 或其他前端框架生態可能是更直接的評估起點。</p>
<p>判斷問題可以這樣問：使用者感受到的主要差異來自 server 的併發能力，還是來自畫面反應、表單流程、SEO 與第三方整合？如果主要差異在後者，Go 可以是後端配角，不一定是產品主體的第一個選型決策。</p>
<p>選型可以用三個問題收斂：</p>
<ol>
<li><strong>主要瓶頸是什麼？</strong> 如果瓶頸是大量 I/O、長連線、背景處理、部署穩定性，Go 值得優先評估；如果瓶頸是 UI scaffolding、資料後台或快速試錯，框架生態可能更關鍵。</li>
<li><strong>邊界是否已經清楚？</strong> 如果輸入、規則、狀態與輸出能被穩定拆開，Go 的顯式設計會帶來可讀性；如果流程每天改，先用 convention 強的工具驗證產品可能更合理。</li>
<li><strong>團隊要優化短期探索還是長期維護？</strong> Go 會把錯誤處理、型別、依賴與生命週期攤開寫清楚；這對長期服務是優點，對一次性探索則可能顯得偏重。</li>
</ol>
<p>因此，選 Go 的結論應該長得像工程判斷。例如：「這個服務會維持上千條長連線，需要明確 timeout、<a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> 與 <a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">shutdown</a> 所以 Go 是好候選。」或是：「這個系統主要是後台 CRUD 和權限頁面，框架產能比 runtime 特性更重要，所以先評估 Django、Rails 或 Next.js 生態。」這樣的句子能讓團隊看見選型依據，也能在條件改變時重新評估。</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></channel></rss>