<?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>Best-Practices on Tarragon</title><link>https://tarrragon.github.io/blog/tags/best-practices/</link><description>Recent content in Best-Practices on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 24 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/best-practices/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>9.5 工具決策：regex 到 AST、Python 到 Go 的 tripwire</title><link>https://tarrragon.github.io/blog/go/09-tooling-and-analysis/tool-decision-tripwire/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/09-tooling-and-analysis/tool-decision-tripwire/</guid><description>&lt;p>工具決策的核心責任是&lt;strong>用事前約定的條件決定何時升級，取代事後的模糊直覺&lt;/strong>。&lt;a href="https://tarrragon.github.io/blog/go/glossary/#tripwire-%e6%b1%ba%e7%ad%96" data-link-title="Go 教材核心術語" data-link-desc="整理 Go 入門與進階篇共用的架構、事件、狀態與邊界詞彙">Tripwire 決策&lt;/a>（預設觸發條件）的設計：在某個可量測訊號命中時，主動重新評估現有工具是否夠用、或該升級到下一個層級。這個約定避開兩個常見失敗 —「太早升級」（把 shell 一行解決的事過度工程化）跟「太晚升級」（regex 每週出狀況卻忍著不升，信譽破產）。&lt;/p>
&lt;p>9.1–9.4 講了怎麼寫 Go 工具。這一章退一步，看&lt;strong>什麼時候該寫這個工具、什麼時候該升級既有工具&lt;/strong>。決策錯了，寫得多好都沒用。本章介紹 tripwire 決策法、WRAP 框架在技術決策的套用、以及用 blog 工具鏈選型作為 concrete instance。&lt;/p>
&lt;h2 id="為什麼需要-tripwire">為什麼需要 tripwire&lt;/h2>
&lt;p>「什麼時候升級」本身是決策。如果不做預設，會發生兩件事：&lt;/p>
&lt;p>&lt;strong>太早升級&lt;/strong>：每次問「該不該升」的時候都說「升吧反正不會錯」。結果工具複雜度爆炸，維護成本拖慢產品開發。&lt;/p>
&lt;p>&lt;strong>太晚升級&lt;/strong>：每次都說「regex 再撐一下就好」。結果工具的誤判累積，作者開始手動 override、skip lint、加例外，工具信譽破產。&lt;/p>
&lt;p>Tripwire 是&lt;strong>事前約定&lt;/strong>：「當以下條件之一命中，就重新評估是否升級」。這把「該不該升」從&lt;strong>臨時直覺&lt;/strong>變成&lt;strong>有根據的再評估&lt;/strong>。&lt;/p>
&lt;p>這個概念在 Chip 與 Dan Heath 的《Decisive》裡有詳細討論 — tripwire 的要點是&lt;strong>用事前的明確條件，取代事後的模糊直覺&lt;/strong>。&lt;/p>
&lt;h2 id="wrap-框架套用到工具決策">WRAP 框架套用到工具決策&lt;/h2>
&lt;p>WRAP = Widen options / Reality-test / Attain distance / Prepare to be wrong。對應到技術決策：&lt;/p>
&lt;p>&lt;strong>Widen options&lt;/strong>：不要只在「Go 還是 Python」之間選。多選項至少要有：&lt;/p>
&lt;ul>
&lt;li>現有工具撐著（regex + shell）&lt;/li>
&lt;li>半自動化（Python + regex，50 行腳本）&lt;/li>
&lt;li>自訂工具（Python / Go + 適當 parser）&lt;/li>
&lt;li>買服務（買現成 linter SaaS）&lt;/li>
&lt;li>不解決（接受這個問題）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Reality-test&lt;/strong>：用數字跟樣本驗證假設。「regex 夠用」是假設，數字可能說「每 100 個 match 裡 15 個誤判」。&lt;/p>
&lt;p>&lt;strong>Attain distance&lt;/strong>：退一步看，如果這個工具三年後可能被捨棄，現在投入多少才合理。&lt;/p>
&lt;p>&lt;strong>Prepare to be wrong&lt;/strong>：先設 tripwire，萬一決策錯了也能及時 pivot，不會沉沒到底。&lt;/p>
&lt;p>blog 的工具鏈決策用這個框架跑過一次，結論是 Go + goldmark。過程紀錄在 &lt;a href="https://tarrragon.github.io/blog/posts/mdtoolsgo--goldmark-%E7%9A%84-markdown-%E5%B7%A5%E5%85%B7%E9%8F%88%E8%A8%AD%E8%A8%88/" data-link-title="mdtools：Go &amp;#43; goldmark 的 markdown 工具鏈設計" data-link-desc="mdtools 的架構決策：選 Go &amp;#43; goldmark 的理由（與 Hugo 同源保證 lint↔render 等價）、單 binary 多子命令設計、pre-commit 整合、規則開啟紀律。">mdtools 設計&lt;/a>，這裡只提煉可複用的決策 pattern。&lt;/p>
&lt;h2 id="三個實戰-tripwire">三個實戰 tripwire&lt;/h2>
&lt;p>以下三組 tripwire 對多數內部工具都適用。遇到其中一個命中時，該花一小時重新評估現有工具是否夠用。&lt;/p>
&lt;h3 id="tripwire-1從-shell-one-liner-升級到腳本">Tripwire 1：從 shell one-liner 升級到腳本&lt;/h3>
&lt;p>&lt;strong>訊號&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>同樣的 shell 指令在三個以上地方重複貼過&lt;/li>
&lt;li>指令超過 3 個 pipe 或巢狀 subshell&lt;/li>
&lt;li>指令的行為要根據環境（CI vs local）分支&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>升級方向&lt;/strong>：寫成 20-50 行 Python 或 Bash script，放進 &lt;code>scripts/&lt;/code>。&lt;/p>
&lt;p>&lt;strong>反例&lt;/strong>：每次寫新 shell 命令都起腳本檔。常用的一行 &lt;code>grep&lt;/code> 不需要變 script。&lt;/p>
&lt;h3 id="tripwire-2從-regex-升級到-parser--ast">Tripwire 2：從 regex 升級到 parser / AST&lt;/h3>
&lt;p>&lt;strong>訊號&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Regex 需要「上下文判斷」（這個 match 在 code block 內嗎？在 HTML tag 內嗎？）&lt;/li>
&lt;li>規則要處理嵌套結構（表格內的 link、code block 內的 heading）&lt;/li>
&lt;li>誤報率超過 1% 或每週出現&lt;/li>
&lt;li>新規則要知道「父節點」「子節點」（MD024 siblings_only 就是這類）&lt;/li>
&lt;li>跨檔案的 graph 需求出現（backlink 分析、broken link 偵測）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>升級方向&lt;/strong>：引入該格式的官方 parser（markdown → goldmark；YAML → &lt;code>gopkg.in/yaml.v3&lt;/code>；Go → &lt;code>go/parser&lt;/code>）。&lt;/p></description><content:encoded><![CDATA[<p>工具決策的核心責任是<strong>用事前約定的條件決定何時升級，取代事後的模糊直覺</strong>。<a href="/blog/go/glossary/#tripwire-%e6%b1%ba%e7%ad%96" data-link-title="Go 教材核心術語" data-link-desc="整理 Go 入門與進階篇共用的架構、事件、狀態與邊界詞彙">Tripwire 決策</a>（預設觸發條件）的設計：在某個可量測訊號命中時，主動重新評估現有工具是否夠用、或該升級到下一個層級。這個約定避開兩個常見失敗 —「太早升級」（把 shell 一行解決的事過度工程化）跟「太晚升級」（regex 每週出狀況卻忍著不升，信譽破產）。</p>
<p>9.1–9.4 講了怎麼寫 Go 工具。這一章退一步，看<strong>什麼時候該寫這個工具、什麼時候該升級既有工具</strong>。決策錯了，寫得多好都沒用。本章介紹 tripwire 決策法、WRAP 框架在技術決策的套用、以及用 blog 工具鏈選型作為 concrete instance。</p>
<h2 id="為什麼需要-tripwire">為什麼需要 tripwire</h2>
<p>「什麼時候升級」本身是決策。如果不做預設，會發生兩件事：</p>
<p><strong>太早升級</strong>：每次問「該不該升」的時候都說「升吧反正不會錯」。結果工具複雜度爆炸，維護成本拖慢產品開發。</p>
<p><strong>太晚升級</strong>：每次都說「regex 再撐一下就好」。結果工具的誤判累積，作者開始手動 override、skip lint、加例外，工具信譽破產。</p>
<p>Tripwire 是<strong>事前約定</strong>：「當以下條件之一命中，就重新評估是否升級」。這把「該不該升」從<strong>臨時直覺</strong>變成<strong>有根據的再評估</strong>。</p>
<p>這個概念在 Chip 與 Dan Heath 的《Decisive》裡有詳細討論 — tripwire 的要點是<strong>用事前的明確條件，取代事後的模糊直覺</strong>。</p>
<h2 id="wrap-框架套用到工具決策">WRAP 框架套用到工具決策</h2>
<p>WRAP = Widen options / Reality-test / Attain distance / Prepare to be wrong。對應到技術決策：</p>
<p><strong>Widen options</strong>：不要只在「Go 還是 Python」之間選。多選項至少要有：</p>
<ul>
<li>現有工具撐著（regex + shell）</li>
<li>半自動化（Python + regex，50 行腳本）</li>
<li>自訂工具（Python / Go + 適當 parser）</li>
<li>買服務（買現成 linter SaaS）</li>
<li>不解決（接受這個問題）</li>
</ul>
<p><strong>Reality-test</strong>：用數字跟樣本驗證假設。「regex 夠用」是假設，數字可能說「每 100 個 match 裡 15 個誤判」。</p>
<p><strong>Attain distance</strong>：退一步看，如果這個工具三年後可能被捨棄，現在投入多少才合理。</p>
<p><strong>Prepare to be wrong</strong>：先設 tripwire，萬一決策錯了也能及時 pivot，不會沉沒到底。</p>
<p>blog 的工具鏈決策用這個框架跑過一次，結論是 Go + goldmark。過程紀錄在 <a href="/blog/posts/mdtoolsgo--goldmark-%E7%9A%84-markdown-%E5%B7%A5%E5%85%B7%E9%8F%88%E8%A8%AD%E8%A8%88/" data-link-title="mdtools：Go &#43; goldmark 的 markdown 工具鏈設計" data-link-desc="mdtools 的架構決策：選 Go &#43; goldmark 的理由（與 Hugo 同源保證 lint↔render 等價）、單 binary 多子命令設計、pre-commit 整合、規則開啟紀律。">mdtools 設計</a>，這裡只提煉可複用的決策 pattern。</p>
<h2 id="三個實戰-tripwire">三個實戰 tripwire</h2>
<p>以下三組 tripwire 對多數內部工具都適用。遇到其中一個命中時，該花一小時重新評估現有工具是否夠用。</p>
<h3 id="tripwire-1從-shell-one-liner-升級到腳本">Tripwire 1：從 shell one-liner 升級到腳本</h3>
<p><strong>訊號</strong>：</p>
<ul>
<li>同樣的 shell 指令在三個以上地方重複貼過</li>
<li>指令超過 3 個 pipe 或巢狀 subshell</li>
<li>指令的行為要根據環境（CI vs local）分支</li>
</ul>
<p><strong>升級方向</strong>：寫成 20-50 行 Python 或 Bash script，放進 <code>scripts/</code>。</p>
<p><strong>反例</strong>：每次寫新 shell 命令都起腳本檔。常用的一行 <code>grep</code> 不需要變 script。</p>
<h3 id="tripwire-2從-regex-升級到-parser--ast">Tripwire 2：從 regex 升級到 parser / AST</h3>
<p><strong>訊號</strong>：</p>
<ul>
<li>Regex 需要「上下文判斷」（這個 match 在 code block 內嗎？在 HTML tag 內嗎？）</li>
<li>規則要處理嵌套結構（表格內的 link、code block 內的 heading）</li>
<li>誤報率超過 1% 或每週出現</li>
<li>新規則要知道「父節點」「子節點」（MD024 siblings_only 就是這類）</li>
<li>跨檔案的 graph 需求出現（backlink 分析、broken link 偵測）</li>
</ul>
<p><strong>升級方向</strong>：引入該格式的官方 parser（markdown → goldmark；YAML → <code>gopkg.in/yaml.v3</code>；Go → <code>go/parser</code>）。</p>
<p><strong>反例</strong>：簡單的「每行開頭是 <code>#</code> 就當 heading」這類規則，regex 永遠夠用。不要為了「學 AST」硬上。</p>
<h3 id="tripwire-3從腳本語言升級到-go">Tripwire 3：從腳本語言升級到 Go</h3>
<p><strong>訊號</strong>：</p>
<ul>
<li>需要 parse 有官方 Go 實作的格式（goldmark、go/ast、protobuf 等）</li>
<li>需要跨平台分發單一 binary</li>
<li>Python / Node 的啟動時間在 pre-commit 的 accumulated cost 已感</li>
<li>要整合到 Go 生態系（產生 Go 程式碼、讀 Go 原始碼）</li>
<li>團隊主要語言是 Go，Python 腳本的維護者變成單一瓶頸</li>
</ul>
<p><strong>升級方向</strong>：Go。</p>
<p><strong>反例</strong>：臨時的資料轉換、一次性的 data migration、快速 prototyping — Python 永遠比 Go 快動筆。</p>
<h2 id="實戰紀錄blog-的三層升級">實戰紀錄：blog 的三層升級</h2>
<p>blog 的 markdown 品質工具鏈在一個 session 內走完三層升級。把這個時間線攤開當案例。</p>
<h3 id="layer-0沒工具靠-markdownlint-ide-extension">Layer 0：沒工具，靠 markdownlint IDE extension</h3>
<p><strong>狀況</strong>：IDE 裝 markdownlint extension，作者寫稿時看到 yellow underline 手動改。</p>
<p><strong>出現什麼 tripwire</strong>：內容規模長大後，reviewer 收到 PR 發現 20 個 MD026 違規，手動改成 cognitive burden。更糟的是紅隊教材有平行結構（13 個案例各有 <code>### 弱點環節</code>），被 MD024 誤判為重複，作者開始 ignore 警告。</p>
<p><strong>升級驅動</strong>：Tripwire 2 命中（規則需要父標題上下文，siblings_only 規則 IDE 沒有）。決策：升級到自訂工具。</p>
<h3 id="layer-1-候選python--regex">Layer 1 候選：Python + regex</h3>
<p><strong>狀況</strong>：50 行 Python 腳本，逐行 match。</p>
<p><strong>為什麼沒選</strong>：兩個跨檔需求已經浮現 — 卡片雙向完整性、L1 link 驗證。這是 graph 需求，regex 做不到 (Tripwire 2 的後半段命中)。加上 blog 本身用 Hugo（Go 寫的，markdown 由 goldmark parse），用 Python 的 markdown parser 會有 render 跟 lint 判讀不一致的長尾風險。</p>
<p>這個評估花了約 15 分鐘，記錄在決策文件裡 — 重點是<strong>評估本身有 artefact 可追溯</strong>。</p>
<h3 id="layer-2go--goldmark">Layer 2：Go + goldmark</h3>
<p><strong>狀況</strong>：選 Go，因為 (a) goldmark 是 Hugo 的 parser，lint 結果跟 render 必然一致；(b) 跨檔 graph 分析用 Go struct 乾淨；(c) 單一 binary 方便接 pre-commit hook 跟 CI，不用擔心 Python 環境。</p>
<p><strong>如何驗證決策正確</strong>：看三個月後的狀態 — 工具有沒有被 bypass？新規則加起來順不順？CI 有沒有反覆失敗？作者有沒有開始覺得工具阻礙產出？這些訊號都沒出現，表示決策有效。若有出現，就是 Tripwire 3 的反向觸發（「該降級回 Python」或「該拆成多個專門工具」），又要重新評估。</p>
<h2 id="延遲決策的具體成本">延遲決策的具體成本</h2>
<p>常見反論：「不急，等真的需要再升」。問題是<strong>延遲本身有成本</strong>：</p>
<ul>
<li><strong>Technical debt 複利</strong>：regex 工具越長越大，每條新 rule 都變難，最後要重寫時要一次 migration 所有 rule。</li>
<li><strong>誤報侵蝕信譽</strong>：使用者每週看到工具報錯、檢查後發現是誤判，開始忽略工具。信譽一旦壞，再好的工具也沒用。</li>
<li><strong>Option value 流失</strong>：跨檔分析、graph 視覺化、CI 整合這些 downstream feature 都要在 AST 基礎上才能做；延遲升級等於延遲 feature 路徑。</li>
<li><strong>機會成本複利</strong>：每週花 30 分鐘手動改 lint 誤判，一年累積 26 小時 — 比升級工具的 8 小時多 3 倍。</li>
</ul>
<p>時間視角變長，升級的 NPV 幾乎永遠正。<strong>延遲不是零成本的預設，是要主動合理化的選擇</strong>。</p>
<h2 id="為什麼-blog-不走先-python-再-go的雙階段">為什麼 blog 不走「先 Python 再 Go」的雙階段</h2>
<p>有個常見建議：「先用 Python 快速做出雛形，驗證概念後再用 Go 重寫」。這個建議對<strong>不確定需求</strong>的情境有效（「我們不知道要什麼工具」），對<strong>已知需求</strong>的情境是浪費。</p>
<p>blog 的狀況是已知：</p>
<ul>
<li>要 markdown lint + 跨檔 graph + pre-commit + CI — 四個需求都很明確</li>
<li>目標語言（Go）已經確定（Hugo 生態、單一 binary 需求）</li>
<li>第三方 parser 選擇（goldmark）已經是最優解</li>
</ul>
<p>在這些條件下，寫 Python 原型的唯一價值是「學 AST 概念」。但同樣學習也能直接用 Go + goldmark 完成。花兩倍時間寫兩遍只為了「先驗證」，邏輯不成立。</p>
<p>判準：<strong>需求越明確、目標語言越確定，雙階段越浪費；需求越模糊、選型還在評估，雙階段越值得</strong>。</p>
<h2 id="決策的副作用artefact">決策的副作用：artefact</h2>
<p>不管最終選什麼，決策過程本身要留下 artefact。blog 案例裡的 artefact：</p>
<ul>
<li><a href="/blog/posts/mdtoolsgo--goldmark-%E7%9A%84-markdown-%E5%B7%A5%E5%85%B7%E9%8F%88%E8%A8%AD%E8%A8%88/" data-link-title="mdtools：Go &#43; goldmark 的 markdown 工具鏈設計" data-link-desc="mdtools 的架構決策：選 Go &#43; goldmark 的理由（與 Hugo 同源保證 lint↔render 等價）、單 binary 多子命令設計、pre-commit 整合、規則開啟紀律。">mdtools 設計紀錄</a>：為什麼是 Go、為什麼是 goldmark、tripwire 怎麼設的</li>
<li><a href="/blog/posts/%E4%BB%80%E9%BA%BC%E6%98%AF-ast-%E5%BE%9E%E5%AD%97%E4%B8%B2%E5%88%B0%E8%AA%9E%E6%B3%95%E6%A8%B9%E7%9A%84%E8%A6%96%E8%A7%92%E8%BD%89%E6%8F%9B/" data-link-title="什麼是 AST — 從字串到語法樹的視角轉換" data-link-desc="AST 與 regex 的差異判準：規則需要知道文字處在什麼結構中時 regex 就不夠。附 regex 誤判的具體 case。">什麼是 AST</a>：AST vs regex 的概念說明</li>
<li><a href="/blog/posts/blog-markdown-%E5%AF%AB%E4%BD%9C%E8%A6%8F%E7%AF%84%E8%88%87-mdtools-%E6%AA%A2%E6%9F%A5/" data-link-title="Blog Markdown 寫作規範與 mdtools 檢查" data-link-desc="本 blog 的 Markdown 排版規範權威契約。涵蓋 H1 禁用、MD024 siblings_only、反釣魚 TLD 校驗、卡片雙向完整性、front matter schema；改規則時要與 scripts/mdtools 實作同步。">markdown 寫作規範</a>：工具要滿足的契約</li>
</ul>
<p>三個文件加起來約 900 行，寫作時間不到半天。</p>
<p><strong>為什麼留 artefact 比決策本身更重要</strong>：</p>
<ul>
<li>半年後同樣問題再浮現時，不會重跑一遍評估</li>
<li>新加入的協作者能快速跟上決策脈絡</li>
<li>Tripwire 條件寫下來才能被驗證（「三個月後有沒有命中？」）</li>
<li>反面證據出現時（例如發現 goldmark 有 bug），有清楚的位置記錄 revised decision</li>
</ul>
<p>沒 artefact 的決策基本上等於沒做過。<strong>決策是動作，artefact 才是沉澱</strong>。</p>
<h2 id="常見陷阱">常見陷阱</h2>
<h3 id="把-tripwire-設太低">把 tripwire 設太低</h3>
<p>「每週誤判一次就升級」實務上等於「每週升級」。Tripwire 要設在<strong>真的造成信譽或產出瓶頸</strong>的位置。</p>
<h3 id="把-tripwire-設太高">把 tripwire 設太高</h3>
<p>「等到 50% 誤判才升級」就太晚了 — 信譽早就垮了。合理範圍是 1-5% 誤判，或每週一次以上。</p>
<h3 id="用-tripwire-取代日常-review">用 tripwire 取代日常 review</h3>
<p>Tripwire 是「提醒重新評估」，不是「自動升級」。命中時要花時間評估，可能發現「還不該升，因為還有 X 原因」。Tripwire 是重新思考的觸發，不是自動化決策。</p>
<h3 id="忽視已命中的-tripwire">忽視已命中的 tripwire</h3>
<p>「這個誤判已經出現第四週了，但我還是覺得先不要升」— 這是在告訴自己原本的 tripwire 設錯了，不是在等更好的時機。重新評估 tripwire 本身，不是 ignore。</p>
<h2 id="擴充路徑">擴充路徑</h2>
<ul>
<li><strong>Decision log 範本</strong>：把團隊的決策過程寫成 template，讓下次不用從零開始</li>
<li><strong>Post-mortem of decisions</strong>：決策後三個月回頭看，把「當時怎麼想」跟「現在怎麼看」對照</li>
<li><strong>Pre-mortem 技巧</strong>：決策前假設「三個月後這決定被推翻，最可能的原因是什麼」，當成補充 tripwire</li>
</ul>
<h2 id="下一步">下一步</h2>
<p><a href="/blog/go/09-tooling-and-analysis/pre-commit-and-ci/" data-link-title="9.6 Pre-commit hook 與 CI 整合" data-link-desc="工具寫完只是起點；接到 pre-commit hook 跟 CI 才真正守住品質。Re-staging、dry-run vs apply、不能繞過的邊界">9.6 pre-commit hook 與 CI 整合</a> 回到工程落地，看工具怎麼從 binary 變成 commit 與 CI 流程裡的執行體。</p>
]]></content:encoded></item><item><title>模組六：實戰指南</title><link>https://tarrragon.github.io/blog/go/06-practical/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/06-practical/</guid><description>&lt;p>本模組把 Go 的核心概念轉成常見服務開發任務。核心順序是：先定義資料與行為語意，再處理輸入邊界，接著更新 usecase、repository、event/&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a> 邊界，最後補測試。這裡的範例會使用中立的即時通知服務，不要求讀者知道任何特定專案。&lt;/p>
&lt;p>實戰章節的重點是練習 Go 的判斷方式：何時只需要 struct，何時需要 method，何時需要 interface，何時需要 goroutine，何時應該把狀態集中管理。大型架構模板留到壓力出現後再評估；服務設計只是這些語言概念的應用場景。&lt;/p>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>關鍵收穫&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go/06-practical/new-websocket-action/" data-link-title="6.1 如何新增一個即時訊息 action" data-link-desc="修改 client message、路由與 handler">6.1&lt;/a>&lt;/td>
 &lt;td>如何新增一個即時訊息 action&lt;/td>
 &lt;td>用 struct、JSON、error 與 usecase 切開輸入邊界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go/06-practical/new-event-type/" data-link-title="6.2 如何新增一種 domain event" data-link-desc="擴展事件常數、輸入驗證與處理流程">6.2&lt;/a>&lt;/td>
 &lt;td>如何新增一種 domain event&lt;/td>
 &lt;td>用明確型別定義事件語意、envelope 與驗證規則&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go/06-practical/state-fields/" data-link-title="6.3 如何擴展狀態投影欄位" data-link-desc="更新狀態模型、repository 與 API 輸出">6.3&lt;/a>&lt;/td>
 &lt;td>如何擴展狀態投影欄位&lt;/td>
 &lt;td>判斷欄位屬於 domain state、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a> 或 response view&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go/06-practical/new-background-worker/" data-link-title="6.4 如何新增背景工作流程" data-link-desc="接入 context、channel 與 shutdown">6.4&lt;/a>&lt;/td>
 &lt;td>如何新增背景工作流程&lt;/td>
 &lt;td>用 context、channel、ticker 與 shutdown 管理 goroutine 生命週期&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go/06-practical/structured-recording/" data-link-title="6.5 如何新增結構化記錄欄位" data-link-desc="區分 operational log、domain event log 與狀態資料">6.5&lt;/a>&lt;/td>
 &lt;td>如何新增結構化記錄欄位&lt;/td>
 &lt;td>區分 structured log、domain &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log&lt;/a> 與 state repository&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go/06-practical/repository-port/" data-link-title="6.6 如何新增 repository port" data-link-desc="先建立儲存邊界，再決定 memory、SQLite 或外部資料庫實作">6.6&lt;/a>&lt;/td>
 &lt;td>如何新增 repository port&lt;/td>
 &lt;td>用小介面建立儲存邊界，再決定 memory 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database&lt;/a> 實作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go/06-practical/service-scenarios/" data-link-title="6.7 Go 常見服務場景總覽" data-link-desc="整理 Go 最常落地的服務情境：即時、背景、事件、通知與 API 聚合">6.7&lt;/a>&lt;/td>
 &lt;td>Go 常見服務場景總覽&lt;/td>
 &lt;td>看懂 Go 最常落地的即時、背景與事件處理場景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go/06-practical/data-access-boundaries/" data-link-title="6.8 高併發下的 Redis 與 SQL 使用原則" data-link-desc="從 Go 服務角度整理 Redis 與 SQL 的高併發存取邊界">6.8&lt;/a>&lt;/td>
 &lt;td>高併發下的 Redis 與 SQL 使用原則&lt;/td>
 &lt;td>用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a>、pool 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a> 控制下游壓力&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="本模組的教學主軸">本模組的教學主軸&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>資料先有語意&lt;/strong>：struct 欄位、JSON tag、zero value 與 &lt;code>omitempty&lt;/code> 都要表達資料意義。&lt;/li>
&lt;li>&lt;strong>邊界先小後大&lt;/strong>：先用函式與 struct 整理行為，只有在替換、測試或隔離需求出現時才引入 interface。&lt;/li>
&lt;li>&lt;strong>goroutine 要有生命週期&lt;/strong>：背景工作必須能取消、停止與測試；只把工作丟進 &lt;code>go func()&lt;/code> 會讓 shutdown、錯誤回報與測試邊界變模糊。&lt;/li>
&lt;li>&lt;strong>記錄要按用途分流&lt;/strong>：log 用於操作診斷，event log 用於事實記錄，repository 用於目前狀態。&lt;/li>
&lt;li>&lt;strong>架構來自壓力&lt;/strong>：domain package、repository port、event envelope 是服務變大後的自然拆分，不是入門程式的預設起點。&lt;/li>
&lt;/ul>
&lt;h2 id="章節粒度說明">章節粒度說明&lt;/h2>
&lt;p>本模組每一章都是「完成一個常見開發任務」的完整流程，所以篇幅會比語法章長。章節會同時包含資料定義、邊界判斷、簡化實作、測試與設計檢查；這是為了讓讀者看到一次修改如何穿過 Go 服務的多個層次。&lt;/p>
&lt;p>細節主題會在後續模組拆開深入：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>本模組任務&lt;/th>
 &lt;th>深入章節&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>即時訊息 action&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/02-networking-websocket/" data-link-title="模組二：WebSocket 服務架構" data-link-desc="WebSocket client lifecycle、heartbeat、訂閱路由與慢客戶端管理">WebSocket 服務架構&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>domain event 與去重&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/04-architecture-boundaries/" data-link-title="模組四：架構邊界與事件系統" data-link-desc="用事件驅動架構拆解事件來源、處理流程、狀態邊界與即時推送">架構邊界與事件系統&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>狀態投影與 repository&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/04-architecture-boundaries/source-of-truth/" data-link-title="4.3 Source of Truth：狀態邊界" data-link-desc="集中狀態更新、保護可變資料、設計查詢 projection">Source of Truth：狀態邊界&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>背景 worker 與 shutdown&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/06-production-operations/graceful-shutdown/" data-link-title="6.1 graceful shutdown 與 signal handling" data-link-desc="用 signal 與 context 傳遞停止訊號">graceful shutdown 與 signal handling&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>structured log 與 event log&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/06-production-operations/log-fields/" data-link-title="6.3 結構化日誌欄位設計" data-link-desc="讓 log 可 grep、可聚合、可追蹤">結構化日誌欄位設計&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/go-advanced/07-distributed-operations/observability-pipeline/" data-link-title="7.4 Observability pipeline、metrics 與 tracing" data-link-desc="把 structured log、metric、trace 與 profile 組成可操作的診斷系統">Observability pipeline&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>repository 到資料庫&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/go-advanced/07-distributed-operations/database-transactions/" data-link-title="7.1 資料庫 transaction 與 schema migration" data-link-desc="把 repository 邊界延伸到資料庫交易、migration 與一致性語意">資料庫 transaction 與 schema migration&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="本模組使用的範例主題">本模組使用的範例主題&lt;/h2>
&lt;ul>
&lt;li>即時通知服務的 action route&lt;/li>
&lt;li>domain event envelope&lt;/li>
&lt;li>任務狀態 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection&lt;/a> 更新&lt;/li>
&lt;li>背景 worker 啟動與停止&lt;/li>
&lt;li>structured log 欄位&lt;/li>
&lt;li>repository port 與 memory implementation&lt;/li>
&lt;/ul>
&lt;h2 id="學習時間">學習時間&lt;/h2>
&lt;p>預計 2-3 小時&lt;/p></description><content:encoded><![CDATA[<p>本模組把 Go 的核心概念轉成常見服務開發任務。核心順序是：先定義資料與行為語意，再處理輸入邊界，接著更新 usecase、repository、event/<a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 邊界，最後補測試。這裡的範例會使用中立的即時通知服務，不要求讀者知道任何特定專案。</p>
<p>實戰章節的重點是練習 Go 的判斷方式：何時只需要 struct，何時需要 method，何時需要 interface，何時需要 goroutine，何時應該把狀態集中管理。大型架構模板留到壓力出現後再評估；服務設計只是這些語言概念的應用場景。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/go/06-practical/new-websocket-action/" data-link-title="6.1 如何新增一個即時訊息 action" data-link-desc="修改 client message、路由與 handler">6.1</a></td>
          <td>如何新增一個即時訊息 action</td>
          <td>用 struct、JSON、error 與 usecase 切開輸入邊界</td>
      </tr>
      <tr>
          <td><a href="/blog/go/06-practical/new-event-type/" data-link-title="6.2 如何新增一種 domain event" data-link-desc="擴展事件常數、輸入驗證與處理流程">6.2</a></td>
          <td>如何新增一種 domain event</td>
          <td>用明確型別定義事件語意、envelope 與驗證規則</td>
      </tr>
      <tr>
          <td><a href="/blog/go/06-practical/state-fields/" data-link-title="6.3 如何擴展狀態投影欄位" data-link-desc="更新狀態模型、repository 與 API 輸出">6.3</a></td>
          <td>如何擴展狀態投影欄位</td>
          <td>判斷欄位屬於 domain state、<a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a> 或 response view</td>
      </tr>
      <tr>
          <td><a href="/blog/go/06-practical/new-background-worker/" data-link-title="6.4 如何新增背景工作流程" data-link-desc="接入 context、channel 與 shutdown">6.4</a></td>
          <td>如何新增背景工作流程</td>
          <td>用 context、channel、ticker 與 shutdown 管理 goroutine 生命週期</td>
      </tr>
      <tr>
          <td><a href="/blog/go/06-practical/structured-recording/" data-link-title="6.5 如何新增結構化記錄欄位" data-link-desc="區分 operational log、domain event log 與狀態資料">6.5</a></td>
          <td>如何新增結構化記錄欄位</td>
          <td>區分 structured log、domain <a href="/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log</a> 與 state repository</td>
      </tr>
      <tr>
          <td><a href="/blog/go/06-practical/repository-port/" data-link-title="6.6 如何新增 repository port" data-link-desc="先建立儲存邊界，再決定 memory、SQLite 或外部資料庫實作">6.6</a></td>
          <td>如何新增 repository port</td>
          <td>用小介面建立儲存邊界，再決定 memory 或 <a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database</a> 實作</td>
      </tr>
      <tr>
          <td><a href="/blog/go/06-practical/service-scenarios/" data-link-title="6.7 Go 常見服務場景總覽" data-link-desc="整理 Go 最常落地的服務情境：即時、背景、事件、通知與 API 聚合">6.7</a></td>
          <td>Go 常見服務場景總覽</td>
          <td>看懂 Go 最常落地的即時、背景與事件處理場景</td>
      </tr>
      <tr>
          <td><a href="/blog/go/06-practical/data-access-boundaries/" data-link-title="6.8 高併發下的 Redis 與 SQL 使用原則" data-link-desc="從 Go 服務角度整理 Redis 與 SQL 的高併發存取邊界">6.8</a></td>
          <td>高併發下的 Redis 與 SQL 使用原則</td>
          <td>用 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a>、pool 與 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> 控制下游壓力</td>
      </tr>
  </tbody>
</table>
<h2 id="本模組的教學主軸">本模組的教學主軸</h2>
<ul>
<li><strong>資料先有語意</strong>：struct 欄位、JSON tag、zero value 與 <code>omitempty</code> 都要表達資料意義。</li>
<li><strong>邊界先小後大</strong>：先用函式與 struct 整理行為，只有在替換、測試或隔離需求出現時才引入 interface。</li>
<li><strong>goroutine 要有生命週期</strong>：背景工作必須能取消、停止與測試；只把工作丟進 <code>go func()</code> 會讓 shutdown、錯誤回報與測試邊界變模糊。</li>
<li><strong>記錄要按用途分流</strong>：log 用於操作診斷，event log 用於事實記錄，repository 用於目前狀態。</li>
<li><strong>架構來自壓力</strong>：domain package、repository port、event envelope 是服務變大後的自然拆分，不是入門程式的預設起點。</li>
</ul>
<h2 id="章節粒度說明">章節粒度說明</h2>
<p>本模組每一章都是「完成一個常見開發任務」的完整流程，所以篇幅會比語法章長。章節會同時包含資料定義、邊界判斷、簡化實作、測試與設計檢查；這是為了讓讀者看到一次修改如何穿過 Go 服務的多個層次。</p>
<p>細節主題會在後續模組拆開深入：</p>
<table>
  <thead>
      <tr>
          <th>本模組任務</th>
          <th>深入章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>即時訊息 action</td>
          <td><a href="/blog/go-advanced/02-networking-websocket/" data-link-title="模組二：WebSocket 服務架構" data-link-desc="WebSocket client lifecycle、heartbeat、訂閱路由與慢客戶端管理">WebSocket 服務架構</a></td>
      </tr>
      <tr>
          <td>domain event 與去重</td>
          <td><a href="/blog/go-advanced/04-architecture-boundaries/" data-link-title="模組四：架構邊界與事件系統" data-link-desc="用事件驅動架構拆解事件來源、處理流程、狀態邊界與即時推送">架構邊界與事件系統</a></td>
      </tr>
      <tr>
          <td>狀態投影與 repository</td>
          <td><a href="/blog/go-advanced/04-architecture-boundaries/source-of-truth/" data-link-title="4.3 Source of Truth：狀態邊界" data-link-desc="集中狀態更新、保護可變資料、設計查詢 projection">Source of Truth：狀態邊界</a></td>
      </tr>
      <tr>
          <td>背景 worker 與 shutdown</td>
          <td><a href="/blog/go-advanced/06-production-operations/graceful-shutdown/" data-link-title="6.1 graceful shutdown 與 signal handling" data-link-desc="用 signal 與 context 傳遞停止訊號">graceful shutdown 與 signal handling</a></td>
      </tr>
      <tr>
          <td>structured log 與 event log</td>
          <td><a href="/blog/go-advanced/06-production-operations/log-fields/" data-link-title="6.3 結構化日誌欄位設計" data-link-desc="讓 log 可 grep、可聚合、可追蹤">結構化日誌欄位設計</a> 與 <a href="/blog/go-advanced/07-distributed-operations/observability-pipeline/" data-link-title="7.4 Observability pipeline、metrics 與 tracing" data-link-desc="把 structured log、metric、trace 與 profile 組成可操作的診斷系統">Observability pipeline</a></td>
      </tr>
      <tr>
          <td>repository 到資料庫</td>
          <td><a href="/blog/go-advanced/07-distributed-operations/database-transactions/" data-link-title="7.1 資料庫 transaction 與 schema migration" data-link-desc="把 repository 邊界延伸到資料庫交易、migration 與一致性語意">資料庫 transaction 與 schema migration</a></td>
      </tr>
  </tbody>
</table>
<h2 id="本模組使用的範例主題">本模組使用的範例主題</h2>
<ul>
<li>即時通知服務的 action route</li>
<li>domain event envelope</li>
<li>任務狀態 <a href="/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection</a> 更新</li>
<li>背景 worker 啟動與停止</li>
<li>structured log 欄位</li>
<li>repository port 與 memory implementation</li>
</ul>
<h2 id="學習時間">學習時間</h2>
<p>預計 2-3 小時</p>
]]></content:encoded></item><item><title>6.7 Go 常見服務場景總覽</title><link>https://tarrragon.github.io/blog/go/06-practical/service-scenarios/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/06-practical/service-scenarios/</guid><description>&lt;p>這一章先整理 Go 常見會被用到的服務場景。對剛從 PHP 或 Python 轉來的讀者來說，先知道 Go 最常做哪些事，會更容易理解前面那些語言特性為什麼重要。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>看出 Go 常見的服務落點&lt;/li>
&lt;li>理解每種場景通常需要哪些核心能力&lt;/li>
&lt;li>判斷哪些場景值得進一步用 Go 實作&lt;/li>
&lt;li>把後續的實戰章節放回正確脈絡&lt;/li>
&lt;li>分辨即時服務、背景 worker 與 API service 的不同責任&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察go-很常出現在服務邊界">【觀察】Go 很常出現在服務邊界&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>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> service&lt;/td>
 &lt;td>長連線、事件傳遞、訂閱與廣播&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>background worker&lt;/td>
 &lt;td>背景處理、批次同步、事件消費&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>API gateway&lt;/td>
 &lt;td>路由、聚合、驗證與轉送&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>notification system&lt;/td>
 &lt;td>推播、排程與重試&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>event processor&lt;/td>
 &lt;td>事件解碼、去重、派送與狀態更新&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些場景共通點都是：工作量大多是 I/O 與協調，CPU 單點重運算通常只占一小部分。&lt;/p>
&lt;h2 id="判讀即時服務需要穩定的生命週期">【判讀】即時服務需要穩定的生命週期&lt;/h2>
&lt;p>WebSocket、&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;/p>
&lt;ul>
&lt;li>連線建立與關閉&lt;/li>
&lt;li>heartbeat&lt;/li>
&lt;li>事件路由&lt;/li>
&lt;li>慢 client&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>這些都和 goroutine、channel、context、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 直接相關，所以 Go 很自然會出現在這裡。&lt;/p>
&lt;h2 id="判讀背景-worker-需要明確的停止條件">【判讀】背景 worker 需要明確的停止條件&lt;/h2>
&lt;p>&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>、cron worker、event relay 或同步流程，通常都需要：&lt;/p>
&lt;ul>
&lt;li>可取消&lt;/li>
&lt;li>可重試&lt;/li>
&lt;li>可觀測&lt;/li>
&lt;li>可限流&lt;/li>
&lt;/ul>
&lt;p>這類工作很適合 Go，因為 runtime、context 與標準庫已經把這些邊界鋪好了。&lt;/p>
&lt;h2 id="策略api-聚合與服務編排">【策略】API 聚合與服務編排&lt;/h2>
&lt;p>有些 Go 服務的主要角色是協調與整合：&lt;/p>
&lt;ul>
&lt;li>把多個下游資料聚合成一個 response&lt;/li>
&lt;li>在多個服務之間轉送 command&lt;/li>
&lt;li>在進入 domain 前先做 request normalization&lt;/li>
&lt;/ul>
&lt;p>這類工作通常會出現在微服務環境裡，Go 的清楚邊界與簡單部署會很有價值。&lt;/p>
&lt;h2 id="小結">小結&lt;/h2>
&lt;p>Go 的常見場景很少是「單純做一個頁面」，更多是即時、背景、事件、聚合與服務邊界。先知道 Go 常被放在哪裡，後面的實戰章節就會更容易理解。&lt;/p></description><content:encoded><![CDATA[<p>這一章先整理 Go 常見會被用到的服務場景。對剛從 PHP 或 Python 轉來的讀者來說，先知道 Go 最常做哪些事，會更容易理解前面那些語言特性為什麼重要。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>看出 Go 常見的服務落點</li>
<li>理解每種場景通常需要哪些核心能力</li>
<li>判斷哪些場景值得進一步用 Go 實作</li>
<li>把後續的實戰章節放回正確脈絡</li>
<li>分辨即時服務、背景 worker 與 API service 的不同責任</li>
</ol>
<hr>
<h2 id="觀察go-很常出現在服務邊界">【觀察】Go 很常出現在服務邊界</h2>
<p>Go 最常出現在需要長時間運行、協調 I/O、維持清楚邊界的服務層：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>Go 常扮演的角色</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> service</td>
          <td>長連線、事件傳遞、訂閱與廣播</td>
      </tr>
      <tr>
          <td>background worker</td>
          <td>背景處理、批次同步、事件消費</td>
      </tr>
      <tr>
          <td>API gateway</td>
          <td>路由、聚合、驗證與轉送</td>
      </tr>
      <tr>
          <td>notification system</td>
          <td>推播、排程與重試</td>
      </tr>
      <tr>
          <td>event processor</td>
          <td>事件解碼、去重、派送與狀態更新</td>
      </tr>
  </tbody>
</table>
<p>這些場景共通點都是：工作量大多是 I/O 與協調，CPU 單點重運算通常只占一小部分。</p>
<h2 id="判讀即時服務需要穩定的生命週期">【判讀】即時服務需要穩定的生命週期</h2>
<p>WebSocket、<a href="/blog/backend/knowledge-cards/sse/" data-link-title="Server-Sent Events (SSE)" data-link-desc="說明 SSE 如何透過 HTTP 長連線向 client 單向推送事件">SSE</a> 或其他長連線服務，通常需要處理：</p>
<ul>
<li>連線建立與關閉</li>
<li>heartbeat</li>
<li>事件路由</li>
<li>慢 client</li>
<li><a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a></li>
</ul>
<p>這些都和 goroutine、channel、context、<a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 直接相關，所以 Go 很自然會出現在這裡。</p>
<h2 id="判讀背景-worker-需要明確的停止條件">【判讀】背景 worker 需要明確的停止條件</h2>
<p><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>、cron worker、event relay 或同步流程，通常都需要：</p>
<ul>
<li>可取消</li>
<li>可重試</li>
<li>可觀測</li>
<li>可限流</li>
</ul>
<p>這類工作很適合 Go，因為 runtime、context 與標準庫已經把這些邊界鋪好了。</p>
<h2 id="策略api-聚合與服務編排">【策略】API 聚合與服務編排</h2>
<p>有些 Go 服務的主要角色是協調與整合：</p>
<ul>
<li>把多個下游資料聚合成一個 response</li>
<li>在多個服務之間轉送 command</li>
<li>在進入 domain 前先做 request normalization</li>
</ul>
<p>這類工作通常會出現在微服務環境裡，Go 的清楚邊界與簡單部署會很有價值。</p>
<h2 id="小結">小結</h2>
<p>Go 的常見場景很少是「單純做一個頁面」，更多是即時、背景、事件、聚合與服務邊界。先知道 Go 常被放在哪裡，後面的實戰章節就會更容易理解。</p>
]]></content:encoded></item><item><title>7.8 壓力出現後的重構路線</title><link>https://tarrragon.github.io/blog/go/07-refactoring/pressure-driven-refactor/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/07-refactoring/pressure-driven-refactor/</guid><description>&lt;p>這一章補的是一個很實際的問題：服務還能跑，但已經不好讀、不好測、不好改時，應該怎麼重構。Go 的重構是先辨認壓力來源，再逐步把邊界拉開。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>辨認 handler、state、interface 與 adapter 的壓力訊號&lt;/li>
&lt;li>了解什麼時候該先拆函式，什麼時候該拆 package&lt;/li>
&lt;li>用小步遷移方式保持行為穩定&lt;/li>
&lt;li>將重構與測試保護綁在一起&lt;/li>
&lt;li>讓服務變大時仍能維持可讀性&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察壓力通常先出現在局部">【觀察】壓力通常先出現在局部&lt;/h2>
&lt;p>Go 服務變大後，最先冒出來的問題通常是局部壓力：&lt;/p>
&lt;ul>
&lt;li>handler 開始太厚&lt;/li>
&lt;li>state 太分散&lt;/li>
&lt;li>event 語意不清&lt;/li>
&lt;li>依賴越來越多&lt;/li>
&lt;li>測試開始很脆弱&lt;/li>
&lt;/ul>
&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>再把責任拆到 package 或 interface&lt;/li>
&lt;li>最後才引入更清楚的 adapter 邊界&lt;/li>
&lt;/ol>
&lt;p>這樣可以降低重構風險，也比較符合 Go 的漸進式習慣。&lt;/p>
&lt;h2 id="策略先拆最有壓力的邊界">【策略】先拆最有壓力的邊界&lt;/h2>
&lt;p>最值得先處理的通常是這幾種：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>壓力訊號&lt;/th>
 &lt;th>優先動作&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>handler 過重&lt;/td>
 &lt;td>拆成協定處理與業務函式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>外部依賴難測&lt;/td>
 &lt;td>抽出小介面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>state 外洩&lt;/td>
 &lt;td>集中擁有者並控制 copy boundary&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事件混亂&lt;/td>
 &lt;td>先定義語意，再拆 package&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>依賴耦合太高&lt;/td>
 &lt;td>用 ports/adapters 穩定方向&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些動作不一定同時做，而是按壓力大小逐步處理。&lt;/p>
&lt;h2 id="執行composition-root-是最後的收斂點">【執行】composition root 是最後的收斂點&lt;/h2>
&lt;p>當系統開始出現明確的 application、domain 與 adapter 時，composition root 會變成依賴組裝的收斂點。重構的目標是讓邏輯邊界與依賴方向更穩定。&lt;/p></description><content:encoded><![CDATA[<p>這一章補的是一個很實際的問題：服務還能跑，但已經不好讀、不好測、不好改時，應該怎麼重構。Go 的重構是先辨認壓力來源，再逐步把邊界拉開。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>辨認 handler、state、interface 與 adapter 的壓力訊號</li>
<li>了解什麼時候該先拆函式，什麼時候該拆 package</li>
<li>用小步遷移方式保持行為穩定</li>
<li>將重構與測試保護綁在一起</li>
<li>讓服務變大時仍能維持可讀性</li>
</ol>
<hr>
<h2 id="觀察壓力通常先出現在局部">【觀察】壓力通常先出現在局部</h2>
<p>Go 服務變大後，最先冒出來的問題通常是局部壓力：</p>
<ul>
<li>handler 開始太厚</li>
<li>state 太分散</li>
<li>event 語意不清</li>
<li>依賴越來越多</li>
<li>測試開始很脆弱</li>
</ul>
<p>這些訊號表示該重構，但不代表要一次重寫。</p>
<h2 id="判讀先保行為再搬結構">【判讀】先保行為，再搬結構</h2>
<p>重構的基本順序應該是：</p>
<ol>
<li>先讓行為有測試或觀察點</li>
<li>再把大函式拆成小函式</li>
<li>再把責任拆到 package 或 interface</li>
<li>最後才引入更清楚的 adapter 邊界</li>
</ol>
<p>這樣可以降低重構風險，也比較符合 Go 的漸進式習慣。</p>
<h2 id="策略先拆最有壓力的邊界">【策略】先拆最有壓力的邊界</h2>
<p>最值得先處理的通常是這幾種：</p>
<table>
  <thead>
      <tr>
          <th>壓力訊號</th>
          <th>優先動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>handler 過重</td>
          <td>拆成協定處理與業務函式</td>
      </tr>
      <tr>
          <td>外部依賴難測</td>
          <td>抽出小介面</td>
      </tr>
      <tr>
          <td>state 外洩</td>
          <td>集中擁有者並控制 copy boundary</td>
      </tr>
      <tr>
          <td>事件混亂</td>
          <td>先定義語意，再拆 package</td>
      </tr>
      <tr>
          <td>依賴耦合太高</td>
          <td>用 ports/adapters 穩定方向</td>
      </tr>
  </tbody>
</table>
<p>這些動作不一定同時做，而是按壓力大小逐步處理。</p>
<h2 id="執行composition-root-是最後的收斂點">【執行】composition root 是最後的收斂點</h2>
<p>當系統開始出現明確的 application、domain 與 adapter 時，composition root 會變成依賴組裝的收斂點。重構的目標是讓邏輯邊界與依賴方向更穩定。</p>
]]></content:encoded></item><item><title>1.8 Go tooling 與日常開發流程</title><link>https://tarrragon.github.io/blog/go/01-basics/go-tooling-workflow/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/01-basics/go-tooling-workflow/</guid><description>&lt;p>Go tooling 的核心價值是讓日常開發流程標準化。&lt;code>go run&lt;/code>、&lt;code>go test&lt;/code>、&lt;code>go fmt&lt;/code>、&lt;code>go mod tidy&lt;/code>、&lt;code>go build&lt;/code> 是 Go 專案最基本的協作語言。&lt;/p>
&lt;h2 id="預計補充內容">預計補充內容&lt;/h2>
&lt;p>這些工具使用邊界會在下列章節展開：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/01-basics/main-flow/" data-link-title="1.7 從入口程式看應用啟動流程" data-link-desc="用入口程式建立 Go 程式的啟動與資料流模型">Go 入門：從入口程式看應用啟動流程&lt;/a>：先看 &lt;code>go run&lt;/code> 與 &lt;code>go build&lt;/code> 如何對應入口 package，才能理解 Go 專案真正的執行起點。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/05-error-testing/testing-basics/" data-link-title="5.2 testing 基礎" data-link-desc="用 testing package 驗證函式行為">Go 入門：testing 基礎&lt;/a>：先建立最小回歸檢查的習慣，再談 &lt;code>go test ./...&lt;/code> 在流程中的角色。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">Backend：可靠性驗證流程&lt;/a>：CI 與自動化驗證的責任在這裡展開，不應塞進語言章節。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口&lt;/a>：容器建置、發布門檻與平台合約屬於部署層，不是 toolchain 本身。&lt;/li>
&lt;/ul>
&lt;h2 id="與-backend-教材的分工">與 Backend 教材的分工&lt;/h2>
&lt;p>本章只處理 Go toolchain。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合流程如何在合併前驗證品質與相容性">CI pipeline&lt;/a>、container build、部署前 gate 與 release artifact 會放在 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">Backend：可靠性驗證流程&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口&lt;/a>。&lt;/p>
&lt;h2 id="和-go-教材的關係">和 Go 教材的關係&lt;/h2>
&lt;p>這一章承接的是入口流程、測試與設定讀取；如果你要先回看語言教材，可以讀：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/01-basics/main-flow/" data-link-title="1.7 從入口程式看應用啟動流程" data-link-desc="用入口程式建立 Go 程式的啟動與資料流模型">Go：從入口程式看應用啟動流程&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/05-error-testing/testing-basics/" data-link-title="5.2 testing 基礎" data-link-desc="用 testing package 驗證函式行為">Go：testing 基礎&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/03-stdlib/config-flags-env/" data-link-title="3.9 flag、os/env 與設定邊界" data-link-desc="用標準庫讀取設定，並把外部輸入轉成 config struct">Go：flag、os/env 與設定邊界&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/07-refactoring/composition-root/" data-link-title="7.7 composition root 與依賴組裝" data-link-desc="把具體 adapter、config 與 usecase wiring 留在應用入口層">Go：composition root 與依賴組裝&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Go tooling 的核心價值是讓日常開發流程標準化。<code>go run</code>、<code>go test</code>、<code>go fmt</code>、<code>go mod tidy</code>、<code>go build</code> 是 Go 專案最基本的協作語言。</p>
<h2 id="預計補充內容">預計補充內容</h2>
<p>這些工具使用邊界會在下列章節展開：</p>
<ul>
<li><a href="/blog/go/01-basics/main-flow/" data-link-title="1.7 從入口程式看應用啟動流程" data-link-desc="用入口程式建立 Go 程式的啟動與資料流模型">Go 入門：從入口程式看應用啟動流程</a>：先看 <code>go run</code> 與 <code>go build</code> 如何對應入口 package，才能理解 Go 專案真正的執行起點。</li>
<li><a href="/blog/go/05-error-testing/testing-basics/" data-link-title="5.2 testing 基礎" data-link-desc="用 testing package 驗證函式行為">Go 入門：testing 基礎</a>：先建立最小回歸檢查的習慣，再談 <code>go test ./...</code> 在流程中的角色。</li>
<li><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">Backend：可靠性驗證流程</a>：CI 與自動化驗證的責任在這裡展開，不應塞進語言章節。</li>
<li><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口</a>：容器建置、發布門檻與平台合約屬於部署層，不是 toolchain 本身。</li>
</ul>
<h2 id="與-backend-教材的分工">與 Backend 教材的分工</h2>
<p>本章只處理 Go toolchain。<a href="/blog/backend/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合流程如何在合併前驗證品質與相容性">CI pipeline</a>、container build、部署前 gate 與 release artifact 會放在 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">Backend：可靠性驗證流程</a> 與 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口</a>。</p>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是入口流程、測試與設定讀取；如果你要先回看語言教材，可以讀：</p>
<ul>
<li><a href="/blog/go/01-basics/main-flow/" data-link-title="1.7 從入口程式看應用啟動流程" data-link-desc="用入口程式建立 Go 程式的啟動與資料流模型">Go：從入口程式看應用啟動流程</a></li>
<li><a href="/blog/go/05-error-testing/testing-basics/" data-link-title="5.2 testing 基礎" data-link-desc="用 testing package 驗證函式行為">Go：testing 基礎</a></li>
<li><a href="/blog/go/03-stdlib/config-flags-env/" data-link-title="3.9 flag、os/env 與設定邊界" data-link-desc="用標準庫讀取設定，並把外部輸入轉成 config struct">Go：flag、os/env 與設定邊界</a></li>
<li><a href="/blog/go/07-refactoring/composition-root/" data-link-title="7.7 composition root 與依賴組裝" data-link-desc="把具體 adapter、config 與 usecase wiring 留在應用入口層">Go：composition root 與依賴組裝</a></li>
</ul>
]]></content:encoded></item></channel></rss>