<?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>模組三：標準庫實戰 on Tarragon</title><link>https://tarrragon.github.io/blog/go/03-stdlib/</link><description>Recent content in 模組三：標準庫實戰 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/03-stdlib/index.xml" rel="self" type="application/rss+xml"/><item><title>3.1 fmt、strings 與基本文字處理</title><link>https://tarrragon.github.io/blog/go/03-stdlib/fmt-strings/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/03-stdlib/fmt-strings/</guid><description>&lt;p>文字處理的核心規則是：格式化輸出交給 &lt;code>fmt&lt;/code>，字串查找、裁切、替換與組合交給 &lt;code>strings&lt;/code>。本章將用 CLI 輸出、設定值清理與簡單 parser 建立標準庫文字處理基礎。&lt;/p>
&lt;h2 id="fmt-負責格式化">&lt;code>fmt&lt;/code> 負責格式化&lt;/h2>
&lt;p>&lt;code>fmt&lt;/code> 的核心責任是把資料轉成可閱讀的文字。它可以輸出到標準輸出，也可以把格式化結果組成字串，常用於 CLI 訊息、錯誤訊息與簡單除錯。&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">name&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="s">&amp;#34;worker&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nx">count&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="mi">3&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">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%s handled %d jobs\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">count&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">message&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Sprintf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%s handled %d jobs&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">count&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">message&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Printf&lt;/code> 會直接輸出，&lt;code>Sprintf&lt;/code> 會回傳字串。這個差異很重要：函式內部如果只是要建立訊息，通常應該用 &lt;code>Sprintf&lt;/code> 回傳字串，而不是直接印出。&lt;/p>
&lt;h2 id="格式動詞描述輸出形狀">格式動詞描述輸出形狀&lt;/h2>
&lt;p>格式動詞的核心作用是告訴 &lt;code>fmt&lt;/code> 如何呈現資料。常見動詞包括 &lt;code>%s&lt;/code> 表示字串，&lt;code>%d&lt;/code> 表示十進位整數，&lt;code>%v&lt;/code> 表示一般值，&lt;code>%+v&lt;/code> 顯示 struct 欄位名稱，&lt;code>%#v&lt;/code> 顯示更接近 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">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"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">ID&lt;/span> &lt;span class="kt">int&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">Name&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="nx">user&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">User&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">ID&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">7&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">Name&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s">&amp;#34;alice&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%v\n&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="c1">// {7 alice}&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">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%+v\n&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="c1">// {ID:7 Name:alice}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%#v\n&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="c1">// main.User{ID:7, Name:&amp;#34;alice&amp;#34;}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>%v&lt;/code> 適合一般輸出，&lt;code>%+v&lt;/code> 適合快速檢查 struct 欄位，&lt;code>%#v&lt;/code> 適合除錯或理解實際型別。正式 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a> 通常應該使用結構化 log，而不是把所有資料塞進格式化字串。&lt;/p>
&lt;h2 id="錯誤訊息要包含可行動資訊">錯誤訊息要包含可行動資訊&lt;/h2>
&lt;p>錯誤訊息的核心原則是描述失敗的操作與關鍵資料。&lt;code>fmt.Errorf&lt;/code> 可以建立帶格式的 error，也可以用 &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="kd">func&lt;/span> &lt;span class="nf">loadUser&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="kt">error&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">id&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s">&amp;#34;&amp;#34;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Errorf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;load user: id is required&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>錯誤訊息是給工程師定位問題的線索。像 &lt;code>&amp;quot;failed&amp;quot;&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="nf">saveConfig&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">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">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 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">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 config&lt;/code>、目標 &lt;code>path&lt;/code> 與原始錯誤。呼叫端可以顯示完整錯誤，也可以用 &lt;code>errors.Is&lt;/code> 或 &lt;code>errors.As&lt;/code> 檢查被包裝的錯誤。&lt;/p>
&lt;h2 id="strings-負責字串操作">&lt;code>strings&lt;/code> 負責字串操作&lt;/h2>
&lt;p>&lt;code>strings&lt;/code> 的核心責任是提供不需要正規表示式的常見字串操作。裁切空白、檢查前後綴、切割、替換、大小寫轉換，都應該先考慮 &lt;code>strings&lt;/code>。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">raw&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="s">&amp;#34; api,worker,admin &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nx">raw&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">raw&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">parts&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">strings&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">raw&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>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">part&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">parts&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">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">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">part&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段程式先移除整體前後空白，再用逗號切割，最後清理每個片段。這種處理很常見於環境變數、設定檔與簡單文字輸入。&lt;/p>
&lt;h2 id="查找與判斷應該直接表達意圖">查找與判斷應該直接表達意圖&lt;/h2>
&lt;p>字串判斷的核心原則是使用最貼近意圖的函式。檢查包含關係用 &lt;code>Contains&lt;/code>，檢查開頭用 &lt;code>HasPrefix&lt;/code>，檢查結尾用 &lt;code>HasSuffix&lt;/code>。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">path&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="s">&amp;#34;/api/users&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="nx">strings&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">HasPrefix&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">path&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;/api/&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">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;api route&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="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">path&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="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;user resource&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;/code>&lt;/pre>&lt;/div>&lt;p>用 &lt;code>Index&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">strings&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Index&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">path&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;/api/&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;api route&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段程式需要讀者理解 &lt;code>Index&lt;/code> 回傳 &lt;code>0&lt;/code> 表示出現在開頭；&lt;code>HasPrefix&lt;/code> 則直接說出規則。入門階段應優先選擇語意清楚的 API。&lt;/p>
&lt;h2 id="組合字串要看資料量">組合字串要看資料量&lt;/h2>
&lt;p>組合少量字串時，&lt;code>+&lt;/code> 與 &lt;code>fmt.Sprintf&lt;/code> 通常足夠；大量或迴圈內組合字串時，&lt;code>strings.Builder&lt;/code> 更適合。核心判斷是：資料量小時重視可讀性，資料量大時避免反覆建立中間字串。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">name&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="s">&amp;#34;alice&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nx">message&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="s">&amp;#34;hello, &amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">name&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">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">message&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>少量字串串接很直覺，不需要過度設計。當你在迴圈中累積文字，&lt;code>strings.Builder&lt;/code> 能更明確地表達「正在逐步建構一段文字」。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">builder&lt;/span> &lt;span class="nx">strings&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Builder&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">name&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s">&amp;#34;alice&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;bob&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;carol&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">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">builder&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteString&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>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nx">builder&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteString&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nx">builder&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteString&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;\n&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="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">builder&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">String&lt;/span>&lt;span class="p">())&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>strings.Builder&lt;/code> 不是每次組字串都必須使用。若資料量小、流程簡單，普通串接往往更好讀。&lt;/p>
&lt;h2 id="簡單-parser-可以先用標準庫">簡單 parser 可以先用標準庫&lt;/h2>
&lt;p>簡單文字解析的核心策略是先用清楚的步驟切割資料，再逐步驗證格式。只有當格式本身複雜到難以維護時，才需要引入 parser 或正規表示式。&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">parsePair&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">input&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">string&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">parts&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">strings&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">SplitN&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">input&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="mi">2&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="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">parts&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="s">&amp;#34;&amp;#34;&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="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 pair %q: missing =&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">input&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">key&lt;/span> &lt;span class="o">:=&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">parts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">value&lt;/span> &lt;span class="o">:=&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">parts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">key&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">10&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="s">&amp;#34;&amp;#34;&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="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 pair %q: empty key&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">input&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&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">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>SplitN&lt;/code> 限制最多切成兩段，避免 value 裡再次出現 &lt;code>=&lt;/code> 時被過度切割。這個例子也先處理格式錯誤，再回傳正常結果，讓流程保持清楚。&lt;/p></description><content:encoded><![CDATA[<p>文字處理的核心規則是：格式化輸出交給 <code>fmt</code>，字串查找、裁切、替換與組合交給 <code>strings</code>。本章將用 CLI 輸出、設定值清理與簡單 parser 建立標準庫文字處理基礎。</p>
<h2 id="fmt-負責格式化"><code>fmt</code> 負責格式化</h2>
<p><code>fmt</code> 的核心責任是把資料轉成可閱讀的文字。它可以輸出到標準輸出，也可以把格式化結果組成字串，常用於 CLI 訊息、錯誤訊息與簡單除錯。</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">name</span> <span class="o">:=</span> <span class="s">&#34;worker&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">count</span> <span class="o">:=</span> <span class="mi">3</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">fmt</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;%s handled %d jobs\n&#34;</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">count</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nx">message</span> <span class="o">:=</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;%s handled %d jobs&#34;</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">count</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">message</span><span class="p">)</span></span></span></code></pre></div><p><code>Printf</code> 會直接輸出，<code>Sprintf</code> 會回傳字串。這個差異很重要：函式內部如果只是要建立訊息，通常應該用 <code>Sprintf</code> 回傳字串，而不是直接印出。</p>
<h2 id="格式動詞描述輸出形狀">格式動詞描述輸出形狀</h2>
<p>格式動詞的核心作用是告訴 <code>fmt</code> 如何呈現資料。常見動詞包括 <code>%s</code> 表示字串，<code>%d</code> 表示十進位整數，<code>%v</code> 表示一般值，<code>%+v</code> 顯示 struct 欄位名稱，<code>%#v</code> 顯示更接近 Go 語法的表示。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">type</span> <span class="nx">User</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">ID</span>   <span class="kt">int</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">Name</span> <span class="kt">string</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nx">user</span> <span class="o">:=</span> <span class="nx">User</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="mi">7</span><span class="p">,</span> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;alice&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;%v\n&#34;</span><span class="p">,</span> <span class="nx">user</span><span class="p">)</span>  <span class="c1">// {7 alice}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;%+v\n&#34;</span><span class="p">,</span> <span class="nx">user</span><span class="p">)</span> <span class="c1">// {ID:7 Name:alice}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;%#v\n&#34;</span><span class="p">,</span> <span class="nx">user</span><span class="p">)</span> <span class="c1">// main.User{ID:7, Name:&#34;alice&#34;}</span></span></span></code></pre></div><p><code>%v</code> 適合一般輸出，<code>%+v</code> 適合快速檢查 struct 欄位，<code>%#v</code> 適合除錯或理解實際型別。正式 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 通常應該使用結構化 log，而不是把所有資料塞進格式化字串。</p>
<h2 id="錯誤訊息要包含可行動資訊">錯誤訊息要包含可行動資訊</h2>
<p>錯誤訊息的核心原則是描述失敗的操作與關鍵資料。<code>fmt.Errorf</code> 可以建立帶格式的 error，也可以用 <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="kd">func</span> <span class="nf">loadUser</span><span class="p">(</span><span class="nx">id</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">if</span> <span class="nx">id</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;load user: id is required&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>錯誤訊息是給工程師定位問題的線索。像 <code>&quot;failed&quot;</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="nf">saveConfig</span><span class="p">(</span><span class="nx">path</span><span class="p">,</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">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 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">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這裡的訊息包含操作 <code>save config</code>、目標 <code>path</code> 與原始錯誤。呼叫端可以顯示完整錯誤，也可以用 <code>errors.Is</code> 或 <code>errors.As</code> 檢查被包裝的錯誤。</p>
<h2 id="strings-負責字串操作"><code>strings</code> 負責字串操作</h2>
<p><code>strings</code> 的核心責任是提供不需要正規表示式的常見字串操作。裁切空白、檢查前後綴、切割、替換、大小寫轉換，都應該先考慮 <code>strings</code>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">raw</span> <span class="o">:=</span> <span class="s">&#34;  api,worker,admin  &#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">raw</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">raw</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">parts</span> <span class="o">:=</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">Split</span><span class="p">(</span><span class="nx">raw</span><span class="p">,</span> <span class="s">&#34;,&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">part</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">parts</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">strings</span><span class="p">.</span><span class="nf">TrimSpace</span><span class="p">(</span><span class="nx">part</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段程式先移除整體前後空白，再用逗號切割，最後清理每個片段。這種處理很常見於環境變數、設定檔與簡單文字輸入。</p>
<h2 id="查找與判斷應該直接表達意圖">查找與判斷應該直接表達意圖</h2>
<p>字串判斷的核心原則是使用最貼近意圖的函式。檢查包含關係用 <code>Contains</code>，檢查開頭用 <code>HasPrefix</code>，檢查結尾用 <code>HasSuffix</code>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">path</span> <span class="o">:=</span> <span class="s">&#34;/api/users&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">if</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">HasPrefix</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="s">&#34;/api/&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;api route&#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="nx">strings</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="s">&#34;users&#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="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;user resource&#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></code></pre></div><p>用 <code>Index</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">strings</span><span class="p">.</span><span class="nf">Index</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="s">&#34;/api/&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;api route&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段程式需要讀者理解 <code>Index</code> 回傳 <code>0</code> 表示出現在開頭；<code>HasPrefix</code> 則直接說出規則。入門階段應優先選擇語意清楚的 API。</p>
<h2 id="組合字串要看資料量">組合字串要看資料量</h2>
<p>組合少量字串時，<code>+</code> 與 <code>fmt.Sprintf</code> 通常足夠；大量或迴圈內組合字串時，<code>strings.Builder</code> 更適合。核心判斷是：資料量小時重視可讀性，資料量大時避免反覆建立中間字串。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">name</span> <span class="o">:=</span> <span class="s">&#34;alice&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">message</span> <span class="o">:=</span> <span class="s">&#34;hello, &#34;</span> <span class="o">+</span> <span class="nx">name</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">message</span><span class="p">)</span></span></span></code></pre></div><p>少量字串串接很直覺，不需要過度設計。當你在迴圈中累積文字，<code>strings.Builder</code> 能更明確地表達「正在逐步建構一段文字」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">builder</span> <span class="nx">strings</span><span class="p">.</span><span class="nx">Builder</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">name</span> <span class="o">:=</span> <span class="k">range</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;alice&#34;</span><span class="p">,</span> <span class="s">&#34;bob&#34;</span><span class="p">,</span> <span class="s">&#34;carol&#34;</span><span class="p">}</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">builder</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">&#34;- &#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nx">builder</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">builder</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">&#34;\n&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Print</span><span class="p">(</span><span class="nx">builder</span><span class="p">.</span><span class="nf">String</span><span class="p">())</span></span></span></code></pre></div><p><code>strings.Builder</code> 不是每次組字串都必須使用。若資料量小、流程簡單，普通串接往往更好讀。</p>
<h2 id="簡單-parser-可以先用標準庫">簡單 parser 可以先用標準庫</h2>
<p>簡單文字解析的核心策略是先用清楚的步驟切割資料，再逐步驗證格式。只有當格式本身複雜到難以維護時，才需要引入 parser 或正規表示式。</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">parsePair</span><span class="p">(</span><span class="nx">input</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">,</span> <span class="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">parts</span> <span class="o">:=</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">SplitN</span><span class="p">(</span><span class="nx">input</span><span class="p">,</span> <span class="s">&#34;=&#34;</span><span class="p">,</span> <span class="mi">2</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="nb">len</span><span class="p">(</span><span class="nx">parts</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">2</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="s">&#34;&#34;</span><span class="p">,</span> <span class="s">&#34;&#34;</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 pair %q: missing =&#34;</span><span class="p">,</span> <span class="nx">input</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">key</span> <span class="o">:=</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">TrimSpace</span><span class="p">(</span><span class="nx">parts</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">value</span> <span class="o">:=</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">TrimSpace</span><span class="p">(</span><span class="nx">parts</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">if</span> <span class="nx">key</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">return</span> <span class="s">&#34;&#34;</span><span class="p">,</span> <span class="s">&#34;&#34;</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 pair %q: empty key&#34;</span><span class="p">,</span> <span class="nx">input</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">return</span> <span class="nx">key</span><span class="p">,</span> <span class="nx">value</span><span class="p">,</span> <span class="kc">nil</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>SplitN</code> 限制最多切成兩段，避免 value 裡再次出現 <code>=</code> 時被過度切割。這個例子也先處理格式錯誤，再回傳正常結果，讓流程保持清楚。</p>
<h2 id="小結">小結</h2>
<p>下一章會進入 <code>time</code>，說明時間點、時間長度與 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 的標準表示方式。</p>
]]></content:encoded></item><item><title>3.2 time：時間與 duration</title><link>https://tarrragon.github.io/blog/go/03-stdlib/time/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/03-stdlib/time/</guid><description>&lt;p>時間處理的核心規則是：時間點使用 &lt;code>time.Time&lt;/code>，時間長度使用 &lt;code>time.Duration&lt;/code>。本章將說明 now、parse、format、duration、timer 與 ticker 的基本用法。&lt;/p>
&lt;h2 id="timetime-表示時間點">&lt;code>time.Time&lt;/code> 表示時間點&lt;/h2>
&lt;p>&lt;code>time.Time&lt;/code> 的核心意義是一個具體時間點。它可以代表現在、某個解析出來的時間、資料庫中的時間戳，或 API 回傳的建立時間。&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">now&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Now&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">now&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>time.Now()&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">isExpired&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">now&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Time&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">deadline&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Time&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">bool&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">now&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">After&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">deadline&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>time.Now()&lt;/code>，而是由呼叫端傳入現在時間。測試時就能提供固定時間點，避免測試結果受執行時間影響。&lt;/p>
&lt;h2 id="timeduration-表示時間長度">&lt;code>time.Duration&lt;/code> 表示時間長度&lt;/h2>
&lt;p>&lt;code>time.Duration&lt;/code> 的核心意義是一段時間長度，不是某個時間點。它常用於 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a>、interval、重試等待與效能測量。&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">timeout&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="mi">5&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nx">interval&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="mi">200&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Millisecond&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">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">timeout&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">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">interval&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>time.Second&lt;/code>、&lt;code>time.Millisecond&lt;/code> 這些常數本身是 &lt;code>Duration&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="c1">// 可讀性差：&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">timeout&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Duration&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">5000000000&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="c1">// 可讀性好：&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nx">timeout&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="mi">5&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>直接寫數字會讓讀者無法立即看出單位。Go 的時間 API 以奈秒為底層單位，但程式碼應該使用明確單位表達意圖。&lt;/p>
&lt;h2 id="時間加減要區分時間點與長度">時間加減要區分時間點與長度&lt;/h2>
&lt;p>時間運算的核心規則是：時間點加上 duration 會得到另一個時間點，兩個時間點相減會得到 duration。&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">start&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Now&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">deadline&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">start&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">30&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">elapsed&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Since&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">start&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">deadline&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">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">elapsed&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Add&lt;/code> 適合計算截止時間，&lt;code>Since&lt;/code> 適合計算從某個時間點到現在經過多久。&lt;code>time.Until(deadline)&lt;/code> 則可以計算距離某個未來時間還有多久。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">remaining&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Until&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">deadline&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="nx">remaining&lt;/span> &lt;span class="o">&amp;lt;=&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;expired&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這些 API 讓程式直接表達時間語意，而不是把時間轉成數字後自行相減。&lt;/p>
&lt;h2 id="parse-與-format-使用-layout">parse 與 format 使用 layout&lt;/h2>
&lt;p>Go 時間格式化的核心規則是使用固定參考時間 &lt;code>2006-01-02 15:04:05&lt;/code> 作為 layout。layout 用這個參考時間的各個部分代表輸出形狀。&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">now&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Date&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2026&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">22&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">9&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">UTC&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">now&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Format&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;2006-01-02&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="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">now&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Format&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;2006-01-02 15:04:05&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果你想輸出年月日，就在 layout 裡寫 &lt;code>2006-01-02&lt;/code>；如果想輸出 24 小時制時間，就寫 &lt;code>15:04:05&lt;/code>。這和許多語言使用 &lt;code>YYYY-MM-DD&lt;/code> 的方式不同，是 Go 時間 API 最容易混淆的地方。&lt;/p>
&lt;p>解析時間也使用同一套 layout。&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">input&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="s">&amp;#34;2026-04-22 09:30:00&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">createdAt&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">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Parse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;2006-01-02 15:04:05&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">input&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">err&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;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">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">createdAt&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>time.Parse&lt;/code> 預設會以 UTC 解讀不含時區的時間。若資料代表某個本地時區，應使用 &lt;code>time.ParseInLocation&lt;/code>。&lt;/p>
&lt;h2 id="timer-表示一次性的未來事件">timer 表示一次性的未來事件&lt;/h2>
&lt;p>&lt;code>time.Timer&lt;/code> 的核心用途是在一段時間後發出一次訊號。它常用於 timeout、延遲執行與 select 控制。&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">timer&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewTimer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">defer&lt;/span> &lt;span class="nx">timer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Stop&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="k">select&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="k">case&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">timer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">C&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;timeout&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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>timer.C&lt;/code> 是一個 channel，到時間後會收到一個時間值。若 timer 可能提前不再使用，應呼叫 &lt;code>Stop&lt;/code> 釋放資源。&lt;/p>
&lt;p>在很多簡單情境中，&lt;code>time.After&lt;/code> 更短。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">select&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">case&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">After&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;timeout&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>time.After&lt;/code> 適合一次性的簡單 timeout；在高頻迴圈或需要取消、重設的情境中，&lt;code>time.NewTimer&lt;/code> 通常比較適合。&lt;/p>
&lt;h2 id="ticker-表示週期性事件">ticker 表示週期性事件&lt;/h2>
&lt;p>&lt;code>time.Ticker&lt;/code> 的核心用途是固定間隔發出訊號。它常用於定期清理、輪詢、健康檢查與背景工作。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">ticker&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewTicker&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">defer&lt;/span> &lt;span class="nx">ticker&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Stop&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="p">&amp;lt;&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="o">++&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">ticker&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">C&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;tick&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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>ticker.C&lt;/code> 每隔指定 duration 會收到一次訊號。只要不再使用 ticker，就應呼叫 &lt;code>Stop&lt;/code>，避免背景資源持續運作。&lt;/p>
&lt;p>週期性工作要有明確的退出條件。實務上常搭配 &lt;code>context.Context&lt;/code>。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">ticker&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewTicker&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">ticker&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Stop&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">select&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Done&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">ticker&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">C&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;work&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段程式每秒執行一次工作，直到 context 被取消。時間控制與生命週期控制分開後，程式會比較容易測試與關閉。&lt;/p>
&lt;h2 id="小結">小結&lt;/h2>
&lt;p>下一章會進入 &lt;code>os&lt;/code> 與 &lt;code>io&lt;/code>，說明檔案、輸入輸出與 streaming API 的共同抽象。&lt;/p></description><content:encoded><![CDATA[<p>時間處理的核心規則是：時間點使用 <code>time.Time</code>，時間長度使用 <code>time.Duration</code>。本章將說明 now、parse、format、duration、timer 與 ticker 的基本用法。</p>
<h2 id="timetime-表示時間點"><code>time.Time</code> 表示時間點</h2>
<p><code>time.Time</code> 的核心意義是一個具體時間點。它可以代表現在、某個解析出來的時間、資料庫中的時間戳，或 API 回傳的建立時間。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">now</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Now</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">now</span><span class="p">)</span></span></span></code></pre></div><p><code>time.Now()</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">isExpired</span><span class="p">(</span><span class="nx">now</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">,</span> <span class="nx">deadline</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="kt">bool</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">now</span><span class="p">.</span><span class="nf">After</span><span class="p">(</span><span class="nx">deadline</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>time.Now()</code>，而是由呼叫端傳入現在時間。測試時就能提供固定時間點，避免測試結果受執行時間影響。</p>
<h2 id="timeduration-表示時間長度"><code>time.Duration</code> 表示時間長度</h2>
<p><code>time.Duration</code> 的核心意義是一段時間長度，不是某個時間點。它常用於 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a>、interval、重試等待與效能測量。</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">timeout</span> <span class="o">:=</span> <span class="mi">5</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">interval</span> <span class="o">:=</span> <span class="mi">200</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Millisecond</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">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">timeout</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">interval</span><span class="p">)</span></span></span></code></pre></div><p><code>time.Second</code>、<code>time.Millisecond</code> 這些常數本身是 <code>Duration</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="c1">// 可讀性差：</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">timeout</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Duration</span><span class="p">(</span><span class="mi">5000000000</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="c1">// 可讀性好：</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nx">timeout</span> <span class="o">:=</span> <span class="mi">5</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span></span></span></code></pre></div><p>直接寫數字會讓讀者無法立即看出單位。Go 的時間 API 以奈秒為底層單位，但程式碼應該使用明確單位表達意圖。</p>
<h2 id="時間加減要區分時間點與長度">時間加減要區分時間點與長度</h2>
<p>時間運算的核心規則是：時間點加上 duration 會得到另一個時間點，兩個時間點相減會得到 duration。</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">start</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Now</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">deadline</span> <span class="o">:=</span> <span class="nx">start</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="mi">30</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">elapsed</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Since</span><span class="p">(</span><span class="nx">start</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">deadline</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">elapsed</span><span class="p">)</span></span></span></code></pre></div><p><code>Add</code> 適合計算截止時間，<code>Since</code> 適合計算從某個時間點到現在經過多久。<code>time.Until(deadline)</code> 則可以計算距離某個未來時間還有多久。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">remaining</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Until</span><span class="p">(</span><span class="nx">deadline</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="nx">remaining</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;expired&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這些 API 讓程式直接表達時間語意，而不是把時間轉成數字後自行相減。</p>
<h2 id="parse-與-format-使用-layout">parse 與 format 使用 layout</h2>
<p>Go 時間格式化的核心規則是使用固定參考時間 <code>2006-01-02 15:04:05</code> 作為 layout。layout 用這個參考時間的各個部分代表輸出形狀。</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">now</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Date</span><span class="p">(</span><span class="mi">2026</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">22</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">30</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">time</span><span class="p">.</span><span class="nx">UTC</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">now</span><span class="p">.</span><span class="nf">Format</span><span class="p">(</span><span class="s">&#34;2006-01-02&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">now</span><span class="p">.</span><span class="nf">Format</span><span class="p">(</span><span class="s">&#34;2006-01-02 15:04:05&#34;</span><span class="p">))</span></span></span></code></pre></div><p>如果你想輸出年月日，就在 layout 裡寫 <code>2006-01-02</code>；如果想輸出 24 小時制時間，就寫 <code>15:04:05</code>。這和許多語言使用 <code>YYYY-MM-DD</code> 的方式不同，是 Go 時間 API 最容易混淆的地方。</p>
<p>解析時間也使用同一套 layout。</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">input</span> <span class="o">:=</span> <span class="s">&#34;2026-04-22 09:30:00&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">createdAt</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span><span class="s">&#34;2006-01-02 15:04:05&#34;</span><span class="p">,</span> <span class="nx">input</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">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">5</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span>
</span></span><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">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">createdAt</span><span class="p">)</span></span></span></code></pre></div><p><code>time.Parse</code> 預設會以 UTC 解讀不含時區的時間。若資料代表某個本地時區，應使用 <code>time.ParseInLocation</code>。</p>
<h2 id="timer-表示一次性的未來事件">timer 表示一次性的未來事件</h2>
<p><code>time.Timer</code> 的核心用途是在一段時間後發出一次訊號。它常用於 timeout、延遲執行與 select 控制。</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">timer</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">NewTimer</span><span class="p">(</span><span class="mi">2</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">defer</span> <span class="nx">timer</span><span class="p">.</span><span class="nf">Stop</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">case</span> <span class="o">&lt;-</span><span class="nx">timer</span><span class="p">.</span><span class="nx">C</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;timeout&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>timer.C</code> 是一個 channel，到時間後會收到一個時間值。若 timer 可能提前不再使用，應呼叫 <code>Stop</code> 釋放資源。</p>
<p>在很多簡單情境中，<code>time.After</code> 更短。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">case</span> <span class="o">&lt;-</span><span class="nx">time</span><span class="p">.</span><span class="nf">After</span><span class="p">(</span><span class="mi">2</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;timeout&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>time.After</code> 適合一次性的簡單 timeout；在高頻迴圈或需要取消、重設的情境中，<code>time.NewTimer</code> 通常比較適合。</p>
<h2 id="ticker-表示週期性事件">ticker 表示週期性事件</h2>
<p><code>time.Ticker</code> 的核心用途是固定間隔發出訊號。它常用於定期清理、輪詢、健康檢查與背景工作。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">ticker</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">NewTicker</span><span class="p">(</span><span class="mi">1</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">defer</span> <span class="nx">ticker</span><span class="p">.</span><span class="nf">Stop</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">for</span> <span class="nx">i</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="p">&lt;</span> <span class="mi">3</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="o">&lt;-</span><span class="nx">ticker</span><span class="p">.</span><span class="nx">C</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;tick&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>ticker.C</code> 每隔指定 duration 會收到一次訊號。只要不再使用 ticker，就應呼叫 <code>Stop</code>，避免背景資源持續運作。</p>
<p>週期性工作要有明確的退出條件。實務上常搭配 <code>context.Context</code>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">run</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">ticker</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">NewTicker</span><span class="p">(</span><span class="mi">1</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">defer</span> <span class="nx">ticker</span><span class="p">.</span><span class="nf">Stop</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Done</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">ticker</span><span class="p">.</span><span class="nx">C</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;work&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段程式每秒執行一次工作，直到 context 被取消。時間控制與生命週期控制分開後，程式會比較容易測試與關閉。</p>
<h2 id="小結">小結</h2>
<p>下一章會進入 <code>os</code> 與 <code>io</code>，說明檔案、輸入輸出與 streaming API 的共同抽象。</p>
]]></content:encoded></item><item><title>3.3 os/io：檔案與輸入輸出</title><link>https://tarrragon.github.io/blog/go/03-stdlib/files-io/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/03-stdlib/files-io/</guid><description>&lt;p>Go I/O 的核心規則是：資料來源抽象成 &lt;code>io.Reader&lt;/code>，資料目的地抽象成 &lt;code>io.Writer&lt;/code>。本章將從檔案讀寫開始，建立 &lt;code>os&lt;/code>、&lt;code>io&lt;/code> 與 streaming API 的基本模型。&lt;/p>
&lt;h2 id="檔案操作從-os-開始">檔案操作從 &lt;code>os&lt;/code> 開始&lt;/h2>
&lt;p>&lt;code>os&lt;/code> package 的核心責任是處理作業系統層級的資源，例如檔案、目錄、環境變數與 process 相關資訊。入門階段最常用的是讀檔、寫檔與建立檔案。&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">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="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">2&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">3&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">4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">))&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>os.ReadFile&lt;/code> 會一次把整個檔案讀進記憶體，適合設定檔、小型文字檔與測試資料。若檔案可能很大，就應改用 streaming 方式逐步讀取。&lt;/p>
&lt;p>寫入小檔案也可以使用 &lt;code>os.WriteFile&lt;/code>。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">data&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="nb">byte&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;name=demo\n&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="k">if&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">WriteFile&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;app.env&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mo">0644&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="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;/code>&lt;/pre>&lt;/div>&lt;p>最後的 &lt;code>0644&lt;/code> 是檔案權限。它表示檔案擁有者可讀寫，其他人可讀。權限是 Unix 檔案權限慣例。&lt;/p>
&lt;h2 id="開啟檔案後要關閉">開啟檔案後要關閉&lt;/h2>
&lt;p>檔案是作業系統資源，開啟後應在不使用時關閉。Go 常用 &lt;code>defer file.Close()&lt;/code> 放在成功開啟檔案後，確保函式結束時釋放資源。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nx">file&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">Open&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;data.txt&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span 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"> 3&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"> 4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="k">defer&lt;/span> &lt;span class="nx">file&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Close&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="nx">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">io&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">ReadAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">file&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">err&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">))&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>defer&lt;/code> 應該放在確認 &lt;code>err == nil&lt;/code> 之後，因為開啟失敗時 &lt;code>file&lt;/code> 可能是 &lt;code>nil&lt;/code>。這是 Go I/O 程式很重要的基本順序：先檢查錯誤，再使用資源。&lt;/p>
&lt;h2 id="ioreader-表示可讀來源">&lt;code>io.Reader&lt;/code> 表示可讀來源&lt;/h2>
&lt;p>&lt;code>io.Reader&lt;/code> 的核心意義是「可以讀出 bytes 的來源」。檔案、網路連線、HTTP request body、字串 reader 都可以是 reader。&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">countBytes&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="nx">io&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Reader&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">int&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">io&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">ReadAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&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="mi">0&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">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="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&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">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>io.Reader&lt;/code>。這就是 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="nx">count&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">countBytes&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">NewReader&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;hello&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span 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">3&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">4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">count&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>strings.NewReader&lt;/code> 可以把字串包成 reader，常用於測試與範例。因為函式依賴 &lt;code>io.Reader&lt;/code>，測試時不需要真的建立檔案。&lt;/p>
&lt;h2 id="iowriter-表示可寫目的地">&lt;code>io.Writer&lt;/code> 表示可寫目的地&lt;/h2>
&lt;p>&lt;code>io.Writer&lt;/code> 的核心意義是「可以接收 bytes 的目的地」。檔案、網路連線、HTTP response、記憶體 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a> 都可以是 writer。&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">writeGreeting&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span> &lt;span class="nx">io&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Writer&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">name&lt;/span> &lt;span class="kt">string&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">_&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">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Fprintf&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;hello, %s\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&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">4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個函式不決定輸出位置，只決定輸出內容。呼叫端可以把內容寫到標準輸出、檔案或 buffer。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">buffer&lt;/span> &lt;span class="nx">bytes&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Buffer&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">writeGreeting&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">buffer&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;alice&amp;#34;&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="k">return&lt;/span> &lt;span class="nx">err&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">buffer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">String&lt;/span>&lt;span class="p">())&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>bytes.Buffer&lt;/code> 同時實作 reader 與 writer，適合用來累積輸出或測試寫入結果。&lt;/p>
&lt;h2 id="streaming-適合大資料或長連線">streaming 適合大資料或長連線&lt;/h2>
&lt;p>streaming 的核心策略是分段處理資料，而不是一次把全部資料載入記憶體。當檔案很大、資料來自網路，或你只需要逐步轉送資料時，streaming 會比 &lt;code>ReadAll&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">copyFile&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">dstPath&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">srcPath&lt;/span> &lt;span class="kt">string&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">src&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">Open&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">srcPath&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 class="k">defer&lt;/span> &lt;span class="nx">src&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Close&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 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">dst&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">Create&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">dstPath&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="k">return&lt;/span> &lt;span class="nx">err&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">dst&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Close&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">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="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">io&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Copy&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">dst&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">src&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">err&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>io.Copy&lt;/code> 從 reader 讀資料並寫到 writer。這段程式沒有手動配置完整檔案大小的 byte slice，因此可以處理比記憶體更大的檔案。&lt;/p>
&lt;h2 id="bufioscanner-適合逐行讀取">&lt;code>bufio.Scanner&lt;/code> 適合逐行讀取&lt;/h2>
&lt;p>逐行處理文字的核心工具是 &lt;code>bufio.Scanner&lt;/code>。它會把 reader 切成一個個 token，預設 token 是一行文字。&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">printLines&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="nx">io&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Reader&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">scanner&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">bufio&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewScanner&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">scanner&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Scan&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scanner&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Text&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">return&lt;/span> &lt;span class="nx">scanner&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Err&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">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>scanner.Scan()&lt;/code> 每次成功讀到一行就回傳 &lt;code>true&lt;/code>，讀完或遇到錯誤時回傳 &lt;code>false&lt;/code>。迴圈結束後要檢查 &lt;code>scanner.Err()&lt;/code>，因為讀取錯誤不會在迴圈內直接回傳。&lt;/p>
&lt;p>&lt;code>Scanner&lt;/code> 適合一般文字行，但它有預設 token 大小限制。若要處理非常長的行或大型二進位資料，應改用 &lt;code>bufio.Reader&lt;/code> 或其他 streaming API。&lt;/p></description><content:encoded><![CDATA[<p>Go I/O 的核心規則是：資料來源抽象成 <code>io.Reader</code>，資料目的地抽象成 <code>io.Writer</code>。本章將從檔案讀寫開始，建立 <code>os</code>、<code>io</code> 與 streaming API 的基本模型。</p>
<h2 id="檔案操作從-os-開始">檔案操作從 <code>os</code> 開始</h2>
<p><code>os</code> package 的核心責任是處理作業系統層級的資源，例如檔案、目錄、環境變數與 process 相關資訊。入門階段最常用的是讀檔、寫檔與建立檔案。</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">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="s">&#34;config.json&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nb">string</span><span class="p">(</span><span class="nx">data</span><span class="p">))</span></span></span></code></pre></div><p><code>os.ReadFile</code> 會一次把整個檔案讀進記憶體，適合設定檔、小型文字檔與測試資料。若檔案可能很大，就應改用 streaming 方式逐步讀取。</p>
<p>寫入小檔案也可以使用 <code>os.WriteFile</code>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">data</span> <span class="o">:=</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;name=demo\n&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">os</span><span class="p">.</span><span class="nf">WriteFile</span><span class="p">(</span><span class="s">&#34;app.env&#34;</span><span class="p">,</span> <span class="nx">data</span><span class="p">,</span> <span class="mo">0644</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="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></code></pre></div><p>最後的 <code>0644</code> 是檔案權限。它表示檔案擁有者可讀寫，其他人可讀。權限是 Unix 檔案權限慣例。</p>
<h2 id="開啟檔案後要關閉">開啟檔案後要關閉</h2>
<p>檔案是作業系統資源，開啟後應在不使用時關閉。Go 常用 <code>defer file.Close()</code> 放在成功開啟檔案後，確保函式結束時釋放資源。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nx">file</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">Open</span><span class="p">(</span><span class="s">&#34;data.txt&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">defer</span> <span class="nx">file</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nx">data</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">io</span><span class="p">.</span><span class="nf">ReadAll</span><span class="p">(</span><span class="nx">file</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nb">string</span><span class="p">(</span><span class="nx">data</span><span class="p">))</span></span></span></code></pre></div><p><code>defer</code> 應該放在確認 <code>err == nil</code> 之後，因為開啟失敗時 <code>file</code> 可能是 <code>nil</code>。這是 Go I/O 程式很重要的基本順序：先檢查錯誤，再使用資源。</p>
<h2 id="ioreader-表示可讀來源"><code>io.Reader</code> 表示可讀來源</h2>
<p><code>io.Reader</code> 的核心意義是「可以讀出 bytes 的來源」。檔案、網路連線、HTTP request body、字串 reader 都可以是 reader。</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">countBytes</span><span class="p">(</span><span class="nx">r</span> <span class="nx">io</span><span class="p">.</span><span class="nx">Reader</span><span class="p">)</span> <span class="p">(</span><span class="kt">int</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">io</span><span class="p">.</span><span class="nf">ReadAll</span><span class="p">(</span><span class="nx">r</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="mi">0</span><span class="p">,</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="nb">len</span><span class="p">(</span><span class="nx">data</span><span class="p">),</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個函式不關心資料來自檔案、記憶體或網路，只要求呼叫端提供一個 <code>io.Reader</code>。這就是 Go 介面設計的典型風格：用小介面描述能力，而不是描述具體來源。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">count</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">countBytes</span><span class="p">(</span><span class="nx">strings</span><span class="p">.</span><span class="nf">NewReader</span><span class="p">(</span><span class="s">&#34;hello&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">count</span><span class="p">)</span></span></span></code></pre></div><p><code>strings.NewReader</code> 可以把字串包成 reader，常用於測試與範例。因為函式依賴 <code>io.Reader</code>，測試時不需要真的建立檔案。</p>
<h2 id="iowriter-表示可寫目的地"><code>io.Writer</code> 表示可寫目的地</h2>
<p><code>io.Writer</code> 的核心意義是「可以接收 bytes 的目的地」。檔案、網路連線、HTTP response、記憶體 <a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 都可以是 writer。</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">writeGreeting</span><span class="p">(</span><span class="nx">w</span> <span class="nx">io</span><span class="p">.</span><span class="nx">Writer</span><span class="p">,</span> <span class="nx">name</span> <span class="kt">string</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">_</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Fprintf</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">&#34;hello, %s\n&#34;</span><span class="p">,</span> <span class="nx">name</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個函式不決定輸出位置，只決定輸出內容。呼叫端可以把內容寫到標準輸出、檔案或 buffer。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">buffer</span> <span class="nx">bytes</span><span class="p">.</span><span class="nx">Buffer</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">writeGreeting</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">buffer</span><span class="p">,</span> <span class="s">&#34;alice&#34;</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">buffer</span><span class="p">.</span><span class="nf">String</span><span class="p">())</span></span></span></code></pre></div><p><code>bytes.Buffer</code> 同時實作 reader 與 writer，適合用來累積輸出或測試寫入結果。</p>
<h2 id="streaming-適合大資料或長連線">streaming 適合大資料或長連線</h2>
<p>streaming 的核心策略是分段處理資料，而不是一次把全部資料載入記憶體。當檔案很大、資料來自網路，或你只需要逐步轉送資料時，streaming 會比 <code>ReadAll</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">copyFile</span><span class="p">(</span><span class="nx">dstPath</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">srcPath</span> <span class="kt">string</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">src</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">Open</span><span class="p">(</span><span class="nx">srcPath</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">defer</span> <span class="nx">src</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">dst</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">Create</span><span class="p">(</span><span class="nx">dstPath</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="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">defer</span> <span class="nx">dst</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">io</span><span class="p">.</span><span class="nf">Copy</span><span class="p">(</span><span class="nx">dst</span><span class="p">,</span> <span class="nx">src</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>io.Copy</code> 從 reader 讀資料並寫到 writer。這段程式沒有手動配置完整檔案大小的 byte slice，因此可以處理比記憶體更大的檔案。</p>
<h2 id="bufioscanner-適合逐行讀取"><code>bufio.Scanner</code> 適合逐行讀取</h2>
<p>逐行處理文字的核心工具是 <code>bufio.Scanner</code>。它會把 reader 切成一個個 token，預設 token 是一行文字。</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">printLines</span><span class="p">(</span><span class="nx">r</span> <span class="nx">io</span><span class="p">.</span><span class="nx">Reader</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">scanner</span> <span class="o">:=</span> <span class="nx">bufio</span><span class="p">.</span><span class="nf">NewScanner</span><span class="p">(</span><span class="nx">r</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">for</span> <span class="nx">scanner</span><span class="p">.</span><span class="nf">Scan</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">scanner</span><span class="p">.</span><span class="nf">Text</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">return</span> <span class="nx">scanner</span><span class="p">.</span><span class="nf">Err</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>scanner.Scan()</code> 每次成功讀到一行就回傳 <code>true</code>，讀完或遇到錯誤時回傳 <code>false</code>。迴圈結束後要檢查 <code>scanner.Err()</code>，因為讀取錯誤不會在迴圈內直接回傳。</p>
<p><code>Scanner</code> 適合一般文字行，但它有預設 token 大小限制。若要處理非常長的行或大型二進位資料，應改用 <code>bufio.Reader</code> 或其他 streaming API。</p>
<h2 id="小結">小結</h2>
<p>下一章會進入 JSON，說明 Go 如何把 struct 與外部資料格式互相轉換。</p>
]]></content:encoded></item><item><title>3.4 encoding/json：資料交換</title><link>https://tarrragon.github.io/blog/go/03-stdlib/json/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/03-stdlib/json/</guid><description>&lt;p>&lt;code>encoding/json&lt;/code> 是 Go 標準庫中負責 JSON 編碼與解碼的 package。它的核心用途是把 Go struct 轉成 JSON，或把 JSON 轉回 Go struct，讓程式能和設定檔、HTTP API、message &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> 等外部格式交換資料。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>用 &lt;code>json.Unmarshal&lt;/code> 解析 JSON bytes&lt;/li>
&lt;li>用 &lt;code>json.Marshal&lt;/code> 輸出 JSON bytes&lt;/li>
&lt;li>用 &lt;code>json.NewDecoder&lt;/code> 解析 stream&lt;/li>
&lt;li>用 &lt;code>json.NewEncoder&lt;/code> 寫出 response&lt;/li>
&lt;li>正確處理 JSON 解析錯誤&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察json-解碼是外部資料進入-go-型別的邊界">【觀察】JSON 解碼是外部資料進入 Go 型別的邊界&lt;/h2>
&lt;p>JSON 解碼的核心規則是：外部資料必須先進入明確的 Go struct，後續程式才應依賴型別欄位。以下範例把設定檔 JSON 解析成 &lt;code>Config&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">Config&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">AppName&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="s">`json:&amp;#34;appName&amp;#34;`&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">Port&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="s">`json:&amp;#34;port&amp;#34;`&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">Debug&lt;/span> &lt;span class="kt">bool&lt;/span> &lt;span class="s">`json:&amp;#34;debug&amp;#34;`&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">LoadConfig&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">byte&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"> 8&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">cfg&lt;/span> &lt;span class="nx">Config&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="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">cfg&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">10&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 JSON: %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">11&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">cfg&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>&lt;code>json.Unmarshal&lt;/code> 需要接收 pointer，因為它要把解析結果寫入 &lt;code>cfg&lt;/code>。若傳入 &lt;code>cfg&lt;/code> 而不是 &lt;code>&amp;amp;cfg&lt;/code>，解碼結果無法寫回呼叫端變數。&lt;/p>
&lt;h2 id="判讀json-tag-是解碼與編碼的欄位對照表">【判讀】JSON tag 是解碼與編碼的欄位對照表&lt;/h2>
&lt;p>JSON tag 的核心規則是：Go 欄位名稱和 JSON 欄位名稱可以不同，但必須在 struct tag 中明確對應。&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">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">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">ID&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="s">`json:&amp;#34;id&amp;#34;`&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">Name&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="s">`json:&amp;#34;name&amp;#34;`&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">CreatedAt&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="s">`json:&amp;#34;createdAt&amp;#34;`&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 struct 對應的 JSON 是：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&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="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;u_1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Alice&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="nt">&amp;#34;createdAt&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2026-04-22T10:00:00Z&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Go 欄位必須 exported，&lt;code>encoding/json&lt;/code> 才能讀寫。小寫開頭欄位是 unexported，JSON package 不會填入。&lt;/p>
&lt;h2 id="策略bytes-用-marshalunmarshalstream-用-encoderdecoder">【策略】bytes 用 Marshal/Unmarshal，stream 用 Encoder/Decoder&lt;/h2>
&lt;p>JSON API 選擇的核心規則是：資料已經在記憶體中用 &lt;code>Marshal&lt;/code> / &lt;code>Unmarshal&lt;/code>，資料來自 stream 用 &lt;code>Encoder&lt;/code> / &lt;code>Decoder&lt;/code>。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>適合 API&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>[]byte&lt;/code> 解析成 struct&lt;/td>
 &lt;td>&lt;code>json.Unmarshal&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>struct 轉成 &lt;code>[]byte&lt;/code>&lt;/td>
 &lt;td>&lt;code>json.Marshal&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>從 &lt;code>io.Reader&lt;/code> 讀 JSON&lt;/td>
 &lt;td>&lt;code>json.NewDecoder&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫 JSON 到 &lt;code>io.Writer&lt;/code>&lt;/td>
 &lt;td>&lt;code>json.NewEncoder&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>HTTP request body 是 stream，適合用 decoder：&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">decodeCreateUser&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 class="nx">CreateUserRequest&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="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="k">return&lt;/span> &lt;span class="nx">CreateUserRequest&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;decode request JSON: %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 class="k">return&lt;/span> &lt;span class="nx">req&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">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>HTTP response writer 也是 stream，適合用 encoder：&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">writeJSON&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">status&lt;/span> &lt;span class="kt">int&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">data&lt;/span> &lt;span class="kt">any&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">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Header&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nf">Set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Content-Type&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;application/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="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteHeader&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">status&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">_&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">json&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewEncoder&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nf">Encode&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&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;/code>&lt;/pre>&lt;/div>&lt;h2 id="執行在-http-handler-中處理-json">【執行】在 HTTP handler 中處理 JSON&lt;/h2>
&lt;p>HTTP JSON handler 的核心規則是：解析錯誤屬於 client input 問題，通常回 400；內部處理錯誤屬於 server 問題，通常回 500。&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">CreateUserRequest&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">Name&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="s">`json:&amp;#34;name&amp;#34;`&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">CreateUserResponse&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">ID&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="s">`json:&amp;#34;id&amp;#34;`&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">Name&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="s">`json:&amp;#34;name&amp;#34;`&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">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">11&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">12&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">13&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">StatusBadRequest&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="s">&amp;#34;error&amp;#34;&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>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Name&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">20&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">StatusBadRequest&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="s">&amp;#34;error&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s">&amp;#34;name is required&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl"> &lt;span class="nx">resp&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">CreateUserResponse&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl"> &lt;span class="nx">ID&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s">&amp;#34;u_1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl"> &lt;span class="nx">Name&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">Name&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl"> &lt;span class="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">resp&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&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 把 JSON 邊界處理清楚：先解碼，再驗證，再執行核心邏輯，最後輸出 JSON。&lt;/p></description><content:encoded><![CDATA[<p><code>encoding/json</code> 是 Go 標準庫中負責 JSON 編碼與解碼的 package。它的核心用途是把 Go struct 轉成 JSON，或把 JSON 轉回 Go struct，讓程式能和設定檔、HTTP API、message <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> 等外部格式交換資料。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>用 <code>json.Unmarshal</code> 解析 JSON bytes</li>
<li>用 <code>json.Marshal</code> 輸出 JSON bytes</li>
<li>用 <code>json.NewDecoder</code> 解析 stream</li>
<li>用 <code>json.NewEncoder</code> 寫出 response</li>
<li>正確處理 JSON 解析錯誤</li>
</ol>
<hr>
<h2 id="觀察json-解碼是外部資料進入-go-型別的邊界">【觀察】JSON 解碼是外部資料進入 Go 型別的邊界</h2>
<p>JSON 解碼的核心規則是：外部資料必須先進入明確的 Go struct，後續程式才應依賴型別欄位。以下範例把設定檔 JSON 解析成 <code>Config</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">type</span> <span class="nx">Config</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">AppName</span> <span class="kt">string</span> <span class="s">`json:&#34;appName&#34;`</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">Port</span>    <span class="kt">int</span>    <span class="s">`json:&#34;port&#34;`</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">Debug</span>   <span class="kt">bool</span>   <span class="s">`json:&#34;debug&#34;`</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kd">func</span> <span class="nf">LoadConfig</span><span class="p">(</span><span class="nx">data</span> <span class="p">[]</span><span class="kt">byte</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"> 8</span><span class="cl">    <span class="kd">var</span> <span class="nx">cfg</span> <span class="nx">Config</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="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">cfg</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">10</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 JSON: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">return</span> <span class="nx">cfg</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><code>json.Unmarshal</code> 需要接收 pointer，因為它要把解析結果寫入 <code>cfg</code>。若傳入 <code>cfg</code> 而不是 <code>&amp;cfg</code>，解碼結果無法寫回呼叫端變數。</p>
<h2 id="判讀json-tag-是解碼與編碼的欄位對照表">【判讀】JSON tag 是解碼與編碼的欄位對照表</h2>
<p>JSON tag 的核心規則是：Go 欄位名稱和 JSON 欄位名稱可以不同，但必須在 struct tag 中明確對應。</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">User</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">ID</span>        <span class="kt">string</span> <span class="s">`json:&#34;id&#34;`</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">Name</span>      <span class="kt">string</span> <span class="s">`json:&#34;name&#34;`</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">CreatedAt</span> <span class="kt">string</span> <span class="s">`json:&#34;createdAt&#34;`</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個 struct 對應的 JSON 是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;u_1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Alice&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nt">&#34;createdAt&#34;</span><span class="p">:</span> <span class="s2">&#34;2026-04-22T10:00:00Z&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Go 欄位必須 exported，<code>encoding/json</code> 才能讀寫。小寫開頭欄位是 unexported，JSON package 不會填入。</p>
<h2 id="策略bytes-用-marshalunmarshalstream-用-encoderdecoder">【策略】bytes 用 Marshal/Unmarshal，stream 用 Encoder/Decoder</h2>
<p>JSON API 選擇的核心規則是：資料已經在記憶體中用 <code>Marshal</code> / <code>Unmarshal</code>，資料來自 stream 用 <code>Encoder</code> / <code>Decoder</code>。</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>適合 API</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>[]byte</code> 解析成 struct</td>
          <td><code>json.Unmarshal</code></td>
      </tr>
      <tr>
          <td>struct 轉成 <code>[]byte</code></td>
          <td><code>json.Marshal</code></td>
      </tr>
      <tr>
          <td>從 <code>io.Reader</code> 讀 JSON</td>
          <td><code>json.NewDecoder</code></td>
      </tr>
      <tr>
          <td>寫 JSON 到 <code>io.Writer</code></td>
          <td><code>json.NewEncoder</code></td>
      </tr>
  </tbody>
</table>
<p>HTTP request body 是 stream，適合用 decoder：</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">decodeCreateUser</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 class="nx">CreateUserRequest</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="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="k">return</span> <span class="nx">CreateUserRequest</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;decode request JSON: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">return</span> <span class="nx">req</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>HTTP response writer 也是 stream，適合用 encoder：</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">writeJSON</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">status</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">data</span> <span class="kt">any</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">w</span><span class="p">.</span><span class="nf">Header</span><span class="p">().</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;Content-Type&#34;</span><span class="p">,</span> <span class="s">&#34;application/json&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">status</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">_</span> <span class="p">=</span> <span class="nx">json</span><span class="p">.</span><span class="nf">NewEncoder</span><span class="p">(</span><span class="nx">w</span><span class="p">).</span><span class="nf">Encode</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h2 id="執行在-http-handler-中處理-json">【執行】在 HTTP handler 中處理 JSON</h2>
<p>HTTP JSON handler 的核心規則是：解析錯誤屬於 client input 問題，通常回 400；內部處理錯誤屬於 server 問題，通常回 500。</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">CreateUserRequest</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">Name</span> <span class="kt">string</span> <span class="s">`json:&#34;name&#34;`</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">CreateUserResponse</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">ID</span>   <span class="kt">string</span> <span class="s">`json:&#34;id&#34;`</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">Name</span> <span class="kt">string</span> <span class="s">`json:&#34;name&#34;`</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">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">11</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">12</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">json</span><span class="p">.</span><span class="nf">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">13</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">StatusBadRequest</span><span class="p">,</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">            <span class="s">&#34;error&#34;</span><span class="p">:</span> <span class="s">&#34;invalid JSON&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="p">})</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">if</span> <span class="nx">req</span><span class="p">.</span><span class="nx">Name</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</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">StatusBadRequest</span><span class="p">,</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">            <span class="s">&#34;error&#34;</span><span class="p">:</span> <span class="s">&#34;name is required&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="p">})</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="nx">resp</span> <span class="o">:=</span> <span class="nx">CreateUserResponse</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">        <span class="nx">ID</span><span class="p">:</span>   <span class="s">&#34;u_1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">        <span class="nx">Name</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">Name</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">30</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">resp</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個 handler 把 JSON 邊界處理清楚：先解碼，再驗證，再執行核心邏輯，最後輸出 JSON。</p>
<h2 id="設計檢查">設計檢查</h2>
<h3 id="忘記傳-pointer-給-unmarshal">忘記傳 pointer 給 Unmarshal</h3>
<p><code>json.Unmarshal</code> 必須把結果寫進目標值，所以目標要傳 pointer：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">cfg</span> <span class="nx">Config</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><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">cfg</span><span class="p">)</span></span></span></code></pre></div><h3 id="忽略-decode-錯誤">忽略 Decode 錯誤</h3>
<p>JSON 來自外部輸入，解析錯誤是正常情境。忽略錯誤會讓後續程式拿到零值 struct，造成更難追蹤的 bug。</p>
<h3 id="把內部錯誤直接回給外部">把內部錯誤直接回給外部</h3>
<p>對外 response 應該穩定且安全；內部錯誤細節留在 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 或 error chain 裡，不直接暴露給使用者。</p>
]]></content:encoded></item><item><title>3.5 net/http 與 handler 設計</title><link>https://tarrragon.github.io/blog/go/03-stdlib/http-handler/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/03-stdlib/http-handler/</guid><description>&lt;p>Go 的 &lt;code>net/http&lt;/code> 把 HTTP endpoint 簡化成一個核心模型：handler 接收 request，然後寫出 response。後端服務可以有複雜的資料庫、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a>、背景工作或即時連線，但 HTTP 入口本身應該先保持清楚。&lt;/p>
&lt;h2 id="handler-是-http-邊界">handler 是 HTTP 邊界&lt;/h2>
&lt;p>HTTP handler 的核心責任是處理協定邊界。它應該讀取 request、驗證輸入、呼叫內部邏輯，最後寫出 status code、header 與 body。&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">handleHealth&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="k">if&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Method&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">MethodGet&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">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;method not allowed&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">StatusMethodNotAllowed&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Header&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nf">Set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Content-Type&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;application/json&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteHeader&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">StatusOK&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Fprint&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;status&amp;#34;:&amp;#34;ok&amp;#34;}`&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 handler 只處理健康檢查：確認 HTTP method，設定回應格式，寫出 JSON。它沒有讀取資料庫，也沒有啟動背景工作，因為健康檢查的責任就是讓呼叫者知道服務是否能回應。&lt;/p>
&lt;p>handler 可以呼叫內部服務，但不應該把所有業務規則都塞在 HTTP 層。HTTP 層越薄，測試越容易，未來改成 CLI、queue &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a> 或其他入口時也比較不會重寫核心邏輯。&lt;/p>
&lt;h2 id="httphandlerfunc-是函式轉接器">&lt;code>http.HandlerFunc&lt;/code> 是函式轉接器&lt;/h2>
&lt;p>&lt;code>http.HandlerFunc&lt;/code> 的核心意義是讓普通函式符合 &lt;code>http.Handler&lt;/code> 介面。只要函式形狀是 &lt;code>func(http.ResponseWriter, *http.Request)&lt;/code>，就能成為 HTTP handler。&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">hello&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="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Fprint&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;hello&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">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">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;/hello&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">hello&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">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">ListenAndServe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;:8080&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">nil&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">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>http.HandleFunc&lt;/code> 會把 &lt;code>hello&lt;/code> 轉成 handler 並註冊到預設 mux。小範例可以這樣寫，但實際應用通常會建立自己的 &lt;code>ServeMux&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">newRouter&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">Handler&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">mux&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewServeMux&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">mux&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;/health&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">handleHealth&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">mux&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">handleUsers&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">mux&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>回傳 &lt;code>http.Handler&lt;/code> 可以隱藏路由實作，呼叫端只需要知道這是一個可被 server 使用的 handler。&lt;/p>
&lt;h2 id="servemux-負責路由分派">&lt;code>ServeMux&lt;/code> 負責路由分派&lt;/h2>
&lt;p>&lt;code>ServeMux&lt;/code> 的核心責任是把 request path 對應到 handler。標準庫的 &lt;code>http.NewServeMux&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">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">mux&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewServeMux&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">mux&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;GET /health&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">handleHealth&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">mux&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;POST /users&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">handleCreateUser&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">server&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Server&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">Addr&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s">&amp;#34;:8080&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">Handler&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">mux&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">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">server&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">ListenAndServe&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>新版 Go 的 &lt;code>ServeMux&lt;/code> 支援在 pattern 裡寫 HTTP method，例如 &lt;code>GET /health&lt;/code>。這能讓 method 與 path 在註冊處一起呈現。&lt;/p>
&lt;p>若你的專案需要 middleware group、path parameter 或更完整的路由功能，可以使用第三方 router。入門階段先理解標準庫模型，會更容易看懂任何 router 的抽象。&lt;/p>
&lt;h2 id="request-讀取要有明確限制">request 讀取要有明確限制&lt;/h2>
&lt;p>讀取 request 的核心原則是只接受你預期的內容。handler 應該檢查 method、content type、body 大小與 JSON 格式，避免把任意輸入直接交給內部邏輯。&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">createUserRequest&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">Name&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="s">`json:&amp;#34;name&amp;#34;`&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">Email&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="s">`json:&amp;#34;email&amp;#34;`&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">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"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Method&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">MethodPost&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;method not allowed&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">StatusMethodNotAllowed&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Close&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &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="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">MaxBytesReader&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">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="mi">1&lt;/span>&lt;span class="o">&amp;lt;&amp;lt;&lt;/span>&lt;span class="mi">20&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="kd">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">16&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">17&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">18&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">19&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="k">if&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="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">22&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;email is required&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">23&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">24&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteHeader&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">StatusCreated&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>http.MaxBytesReader&lt;/code> 限制 body 大小，避免大型輸入消耗過多記憶體。&lt;code>json.Decoder&lt;/code> 解析 body，失敗時回傳 &lt;code>400 Bad Request&lt;/code>。欄位驗證通過後，handler 才進入真正的建立流程。&lt;/p></description><content:encoded><![CDATA[<p>Go 的 <code>net/http</code> 把 HTTP endpoint 簡化成一個核心模型：handler 接收 request，然後寫出 response。後端服務可以有複雜的資料庫、<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a>、背景工作或即時連線，但 HTTP 入口本身應該先保持清楚。</p>
<h2 id="handler-是-http-邊界">handler 是 HTTP 邊界</h2>
<p>HTTP handler 的核心責任是處理協定邊界。它應該讀取 request、驗證輸入、呼叫內部邏輯，最後寫出 status code、header 與 body。</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">handleHealth</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="k">if</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Method</span> <span class="o">!=</span> <span class="nx">http</span><span class="p">.</span><span class="nx">MethodGet</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</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;method not allowed&#34;</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusMethodNotAllowed</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">w</span><span class="p">.</span><span class="nf">Header</span><span class="p">().</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;Content-Type&#34;</span><span class="p">,</span> <span class="s">&#34;application/json&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusOK</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Fprint</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">`{&#34;status&#34;:&#34;ok&#34;}`</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個 handler 只處理健康檢查：確認 HTTP method，設定回應格式，寫出 JSON。它沒有讀取資料庫，也沒有啟動背景工作，因為健康檢查的責任就是讓呼叫者知道服務是否能回應。</p>
<p>handler 可以呼叫內部服務，但不應該把所有業務規則都塞在 HTTP 層。HTTP 層越薄，測試越容易，未來改成 CLI、queue <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 或其他入口時也比較不會重寫核心邏輯。</p>
<h2 id="httphandlerfunc-是函式轉接器"><code>http.HandlerFunc</code> 是函式轉接器</h2>
<p><code>http.HandlerFunc</code> 的核心意義是讓普通函式符合 <code>http.Handler</code> 介面。只要函式形狀是 <code>func(http.ResponseWriter, *http.Request)</code>，就能成為 HTTP handler。</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">hello</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="nx">fmt</span><span class="p">.</span><span class="nf">Fprint</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">&#34;hello&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="kd">func</span> <span class="nf">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">http</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/hello&#34;</span><span class="p">,</span> <span class="nx">hello</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nx">http</span><span class="p">.</span><span class="nf">ListenAndServe</span><span class="p">(</span><span class="s">&#34;:8080&#34;</span><span class="p">,</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="p">}</span></span></span></code></pre></div><p><code>http.HandleFunc</code> 會把 <code>hello</code> 轉成 handler 並註冊到預設 mux。小範例可以這樣寫，但實際應用通常會建立自己的 <code>ServeMux</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">newRouter</span><span class="p">()</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Handler</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">mux</span> <span class="o">:=</span> <span class="nx">http</span><span class="p">.</span><span class="nf">NewServeMux</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">mux</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/health&#34;</span><span class="p">,</span> <span class="nx">handleHealth</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">mux</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">handleUsers</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span> <span class="nx">mux</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>回傳 <code>http.Handler</code> 可以隱藏路由實作，呼叫端只需要知道這是一個可被 server 使用的 handler。</p>
<h2 id="servemux-負責路由分派"><code>ServeMux</code> 負責路由分派</h2>
<p><code>ServeMux</code> 的核心責任是把 request path 對應到 handler。標準庫的 <code>http.NewServeMux</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">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">mux</span> <span class="o">:=</span> <span class="nx">http</span><span class="p">.</span><span class="nf">NewServeMux</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">mux</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;GET /health&#34;</span><span class="p">,</span> <span class="nx">handleHealth</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">mux</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;POST /users&#34;</span><span class="p">,</span> <span class="nx">handleCreateUser</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">server</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">http</span><span class="p">.</span><span class="nx">Server</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">Addr</span><span class="p">:</span>    <span class="s">&#34;:8080&#34;</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="p">:</span> <span class="nx">mux</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">server</span><span class="p">.</span><span class="nf">ListenAndServe</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>新版 Go 的 <code>ServeMux</code> 支援在 pattern 裡寫 HTTP method，例如 <code>GET /health</code>。這能讓 method 與 path 在註冊處一起呈現。</p>
<p>若你的專案需要 middleware group、path parameter 或更完整的路由功能，可以使用第三方 router。入門階段先理解標準庫模型，會更容易看懂任何 router 的抽象。</p>
<h2 id="request-讀取要有明確限制">request 讀取要有明確限制</h2>
<p>讀取 request 的核心原則是只接受你預期的內容。handler 應該檢查 method、content type、body 大小與 JSON 格式，避免把任意輸入直接交給內部邏輯。</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">createUserRequest</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">Name</span>  <span class="kt">string</span> <span class="s">`json:&#34;name&#34;`</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">Email</span> <span class="kt">string</span> <span class="s">`json:&#34;email&#34;`</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</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"> 7</span><span class="cl">    <span class="k">if</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Method</span> <span class="o">!=</span> <span class="nx">http</span><span class="p">.</span><span class="nx">MethodPost</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">http</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">&#34;method not allowed&#34;</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusMethodNotAllowed</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">defer</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Body</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nx">Body</span> <span class="p">=</span> <span class="nx">http</span><span class="p">.</span><span class="nf">MaxBytesReader</span><span class="p">(</span><span class="nx">w</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="mi">1</span><span class="o">&lt;&lt;</span><span class="mi">20</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="kd">var</span> <span class="nx">req</span> <span class="nx">createUserRequest</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="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">17</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">18</span><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="k">if</span> <span class="nx">req</span><span class="p">.</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">22</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;email is required&#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">23</span><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusCreated</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>http.MaxBytesReader</code> 限制 body 大小，避免大型輸入消耗過多記憶體。<code>json.Decoder</code> 解析 body，失敗時回傳 <code>400 Bad Request</code>。欄位驗證通過後，handler 才進入真正的建立流程。</p>
<p>這段範例省略了資料儲存，因為本章重點是 HTTP 邊界。實務上通常會把建立使用者的規則放到 service 函式，handler 只負責轉換 request 與 response。</p>
<h2 id="response-要先決定狀態碼">response 要先決定狀態碼</h2>
<p>寫 response 的核心規則是先決定 status code，再寫 header 與 body。只要 body 開始寫出，Go 就會送出預設或目前設定的 status 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">writeJSON</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">status</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">value</span> <span class="kt">any</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">w</span><span class="p">.</span><span class="nf">Header</span><span class="p">().</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;Content-Type&#34;</span><span class="p">,</span> <span class="s">&#34;application/json&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">status</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">json</span><span class="p">.</span><span class="nf">NewEncoder</span><span class="p">(</span><span class="nx">w</span><span class="p">).</span><span class="nf">Encode</span><span class="p">(</span><span class="nx">value</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="c1">// response 已經開始寫出，這裡通常只能記錄錯誤。</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;write json response: %v&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>WriteHeader</code> 應該在 <code>Encode</code> 之前呼叫。若先寫 body，再呼叫 <code>WriteHeader</code>，狀態碼可能已經固定為 <code>200 OK</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="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="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="s">&#34;id&#34;</span><span class="p">:</span> <span class="s">&#34;user_123&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><p>小型範例可以直接在 handler 裡寫 response；當多個 handler 都要輸出 JSON 時，抽出 <code>writeJSON</code> 這類 helper 可以減少重複。</p>
<h2 id="handler-可以依賴介面">handler 可以依賴介面</h2>
<p>handler 依賴介面的核心好處是測試與替換更容易。HTTP 層不需要知道資料來自資料庫、記憶體或遠端 API，只需要知道它可以呼叫某個能力。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">type</span> <span class="nx">UserCreator</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">CreateUser</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">name</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">email</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</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">creator</span> <span class="nx">UserCreator</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">UserHandler</span><span class="p">)</span> <span class="nf">Create</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">10</span><span class="cl">    <span class="c1">// 解析與驗證 request 後：</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nx">id</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">creator</span><span class="p">.</span><span class="nf">CreateUser</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">(),</span> <span class="s">&#34;alice&#34;</span><span class="p">,</span> <span class="s">&#34;alice@example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="nx">http</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">&#34;create user&#34;</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusInternalServerError</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</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="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;id&#34;</span><span class="p">:</span> <span class="nx">id</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這裡的 <code>UserHandler</code> 不知道使用者如何被建立，只知道有一個 <code>UserCreator</code>。測試時可以提供假的 creator，正式環境再接上真正實作。</p>
<p>介面不需要一開始就為所有東西建立。當 handler 真的需要隔離外部依賴，或測試需要替換依賴時，再抽出小介面會更自然。</p>
<h2 id="小結">小結</h2>
<p>下一章會回到 logging，說明如何用 <code>slog</code> 讓服務輸出可搜尋、可關聯的結構化資訊。</p>
]]></content:encoded></item><item><title>3.6 log/slog：結構化日誌</title><link>https://tarrragon.github.io/blog/go/03-stdlib/slog/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/03-stdlib/slog/</guid><description>&lt;p>&lt;code>log/slog&lt;/code> 是 Go 標準庫提供的結構化日誌 package。它的核心用途是把 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a> 寫成「訊息 + key-value 欄位」，讓人類能讀，也讓工具能搜尋、過濾與聚合。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>建立 text 或 JSON logger&lt;/li>
&lt;li>使用 log level 區分訊號重要性&lt;/li>
&lt;li>用 key-value 欄位保存可查詢資訊&lt;/li>
&lt;li>設計穩定的 log 欄位名稱&lt;/li>
&lt;li>避免把所有資訊塞進自由文字&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察結構化日誌把資訊放進欄位">【觀察】結構化日誌把資訊放進欄位&lt;/h2>
&lt;p>結構化日誌的核心規則是：穩定資訊放欄位，敘述文字只描述事件。以下範例記錄一筆 user 建立事件：&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">logger&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">slog&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">New&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">slog&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewJSONHandler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">os&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Stdout&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">nil&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Info&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="s">&amp;#34;user created&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="s">&amp;#34;userID&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;u_1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="s">&amp;#34;email&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;alice@example.com&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="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>JSON handler 會輸出類似：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&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="nt">&amp;#34;time&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2026-04-22T10:00:00Z&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="nt">&amp;#34;level&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;INFO&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="nt">&amp;#34;msg&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user created&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="nt">&amp;#34;userID&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;u_1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;email&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;alice@example.com&amp;#34;&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>fmt.Printf(&amp;quot;user u_1 alice@example.com created&amp;quot;)&lt;/code> 更容易被查詢。&lt;/p>
&lt;h2 id="判讀level-表示事件嚴重度">【判讀】level 表示事件嚴重度&lt;/h2>
&lt;p>log level 的核心規則是：level 表示事件需要多少注意力，不表示程式碼所在位置。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>level&lt;/th>
 &lt;th>適用情境&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Debug&lt;/td>
 &lt;td>開發或診斷細節&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Info&lt;/td>
 &lt;td>正常但重要的狀態變化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Warn&lt;/td>
 &lt;td>可恢復但需要注意的異常&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Error&lt;/td>
 &lt;td>操作失敗或需要處理的錯誤&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&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">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Debug&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;cache miss&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;key&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">key&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">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;server started&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;addr&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">addr&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Warn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;queue full&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;dropped&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">count&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">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;write file failed&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;path&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="s">&amp;#34;error&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Error&lt;/code> log 應該包含 error 欄位，讓讀者知道失敗原因。&lt;/p>
&lt;h2 id="策略欄位名稱要穩定">【策略】欄位名稱要穩定&lt;/h2>
&lt;p>log 欄位設計的核心規則是：同一個概念使用同一個欄位名稱，不要在不同地方混用別名。&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>使用者 ID&lt;/td>
 &lt;td>&lt;code>userID&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request ID&lt;/a>&lt;/td>
 &lt;td>&lt;code>requestID&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>工作 ID&lt;/td>
 &lt;td>&lt;code>jobID&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件名稱&lt;/td>
 &lt;td>&lt;code>component&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>錯誤&lt;/td>
 &lt;td>&lt;code>error&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&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">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;job queued&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">job&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ID&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nx">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;job started&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;job_id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">job&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ID&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;job done&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;jobID&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">job&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ID&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>應該統一：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;job queued&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;jobID&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">job&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ID&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nx">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;job started&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;jobID&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">job&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ID&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;job done&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;jobID&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">job&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ID&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>欄位穩定後，grep、log query、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a> 才能可靠。&lt;/p>
&lt;h2 id="執行建立帶預設欄位的-logger">【執行】建立帶預設欄位的 logger&lt;/h2>
&lt;p>預設欄位的核心規則是：每筆 log 都需要的上下文，應該掛在 logger 上，而不是每次手動重複。&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">base&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">slog&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">New&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">slog&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewJSONHandler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">os&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Stdout&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">slog&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">HandlerOptions&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">Level&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">slog&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">LevelInfo&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="nx">logger&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">base&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">With&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="s">&amp;#34;component&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;worker&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="s">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;1.0.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;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">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;job started&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;jobID&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;j_1&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="nx">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;job finished&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;jobID&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;j_1&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>With&lt;/code> 會回傳帶有固定欄位的新 logger。這適合 component、version、requestID 這類上下文。&lt;/p>
&lt;h2 id="設計檢查">設計檢查&lt;/h2>
&lt;h3 id="把所有資訊塞進-msg">把所有資訊塞進 msg&lt;/h3>
&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">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;job j_1 for user u_1 started&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>較佳：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;job started&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;jobID&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;j_1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;userID&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;u_1&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第二種寫法可以直接查 &lt;code>jobID=j_1&lt;/code> 或 &lt;code>userID=u_1&lt;/code>。&lt;/p>
&lt;h3 id="欄位名稱不穩定">欄位名稱不穩定&lt;/h3>
&lt;p>欄位名稱不穩定會讓查詢失效。選定 &lt;code>userID&lt;/code> 就一路使用 &lt;code>userID&lt;/code>，不要混用 &lt;code>uid&lt;/code>、&lt;code>user_id&lt;/code>、&lt;code>user&lt;/code>。&lt;/p>
&lt;h3 id="忽略敏感資訊">忽略敏感資訊&lt;/h3>
&lt;p>log 會被保存與轉發。密碼、token、完整信用卡號等敏感資訊不應寫入 log。&lt;/p>
&lt;h2 id="延伸閱讀">延伸閱讀&lt;/h2>
&lt;p>本章只介紹標準庫 &lt;code>log/slog&lt;/code> 的基本用法。服務開始有 domain event、state repository 或查詢需求時，可以接著閱讀 &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 與狀態資料">如何新增結構化記錄欄位&lt;/a>；進入生產操作後，再閱讀 &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、可聚合、可追蹤">Go 進階：結構化日誌欄位設計&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、metrics 與 tracing&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p><code>log/slog</code> 是 Go 標準庫提供的結構化日誌 package。它的核心用途是把 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 寫成「訊息 + key-value 欄位」，讓人類能讀，也讓工具能搜尋、過濾與聚合。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>建立 text 或 JSON logger</li>
<li>使用 log level 區分訊號重要性</li>
<li>用 key-value 欄位保存可查詢資訊</li>
<li>設計穩定的 log 欄位名稱</li>
<li>避免把所有資訊塞進自由文字</li>
</ol>
<hr>
<h2 id="觀察結構化日誌把資訊放進欄位">【觀察】結構化日誌把資訊放進欄位</h2>
<p>結構化日誌的核心規則是：穩定資訊放欄位，敘述文字只描述事件。以下範例記錄一筆 user 建立事件：</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">logger</span> <span class="o">:=</span> <span class="nx">slog</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="nx">slog</span><span class="p">.</span><span class="nf">NewJSONHandler</span><span class="p">(</span><span class="nx">os</span><span class="p">.</span><span class="nx">Stdout</span><span class="p">,</span> <span class="kc">nil</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="s">&#34;user created&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="s">&#34;userID&#34;</span><span class="p">,</span> <span class="s">&#34;u_1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="s">&#34;email&#34;</span><span class="p">,</span> <span class="s">&#34;alice@example.com&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><p>JSON handler 會輸出類似：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;time&#34;</span><span class="p">:</span> <span class="s2">&#34;2026-04-22T10:00:00Z&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;level&#34;</span><span class="p">:</span> <span class="s2">&#34;INFO&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nt">&#34;msg&#34;</span><span class="p">:</span> <span class="s2">&#34;user created&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nt">&#34;userID&#34;</span><span class="p">:</span> <span class="s2">&#34;u_1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="nt">&#34;email&#34;</span><span class="p">:</span> <span class="s2">&#34;alice@example.com&#34;</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>fmt.Printf(&quot;user u_1 alice@example.com created&quot;)</code> 更容易被查詢。</p>
<h2 id="判讀level-表示事件嚴重度">【判讀】level 表示事件嚴重度</h2>
<p>log level 的核心規則是：level 表示事件需要多少注意力，不表示程式碼所在位置。</p>
<table>
  <thead>
      <tr>
          <th>level</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Debug</td>
          <td>開發或診斷細節</td>
      </tr>
      <tr>
          <td>Info</td>
          <td>正常但重要的狀態變化</td>
      </tr>
      <tr>
          <td>Warn</td>
          <td>可恢復但需要注意的異常</td>
      </tr>
      <tr>
          <td>Error</td>
          <td>操作失敗或需要處理的錯誤</td>
      </tr>
  </tbody>
</table>
<p>範例：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">logger</span><span class="p">.</span><span class="nf">Debug</span><span class="p">(</span><span class="s">&#34;cache miss&#34;</span><span class="p">,</span> <span class="s">&#34;key&#34;</span><span class="p">,</span> <span class="nx">key</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;server started&#34;</span><span class="p">,</span> <span class="s">&#34;addr&#34;</span><span class="p">,</span> <span class="nx">addr</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">logger</span><span class="p">.</span><span class="nf">Warn</span><span class="p">(</span><span class="s">&#34;queue full&#34;</span><span class="p">,</span> <span class="s">&#34;dropped&#34;</span><span class="p">,</span> <span class="nx">count</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nx">logger</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="s">&#34;write file failed&#34;</span><span class="p">,</span> <span class="s">&#34;path&#34;</span><span class="p">,</span> <span class="nx">path</span><span class="p">,</span> <span class="s">&#34;error&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span></span></span></code></pre></div><p><code>Error</code> log 應該包含 error 欄位，讓讀者知道失敗原因。</p>
<h2 id="策略欄位名稱要穩定">【策略】欄位名稱要穩定</h2>
<p>log 欄位設計的核心規則是：同一個概念使用同一個欄位名稱，不要在不同地方混用別名。</p>
<table>
  <thead>
      <tr>
          <th>概念</th>
          <th>建議欄位</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>使用者 ID</td>
          <td><code>userID</code></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request ID</a></td>
          <td><code>requestID</code></td>
      </tr>
      <tr>
          <td>工作 ID</td>
          <td><code>jobID</code></td>
      </tr>
      <tr>
          <td>元件名稱</td>
          <td><code>component</code></td>
      </tr>
      <tr>
          <td>錯誤</td>
          <td><code>error</code></td>
      </tr>
  </tbody>
</table>
<p>不要這樣混用：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;job queued&#34;</span><span class="p">,</span> <span class="s">&#34;id&#34;</span><span class="p">,</span> <span class="nx">job</span><span class="p">.</span><span class="nx">ID</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;job started&#34;</span><span class="p">,</span> <span class="s">&#34;job_id&#34;</span><span class="p">,</span> <span class="nx">job</span><span class="p">.</span><span class="nx">ID</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;job done&#34;</span><span class="p">,</span> <span class="s">&#34;jobID&#34;</span><span class="p">,</span> <span class="nx">job</span><span class="p">.</span><span class="nx">ID</span><span class="p">)</span></span></span></code></pre></div><p>應該統一：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;job queued&#34;</span><span class="p">,</span> <span class="s">&#34;jobID&#34;</span><span class="p">,</span> <span class="nx">job</span><span class="p">.</span><span class="nx">ID</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;job started&#34;</span><span class="p">,</span> <span class="s">&#34;jobID&#34;</span><span class="p">,</span> <span class="nx">job</span><span class="p">.</span><span class="nx">ID</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;job done&#34;</span><span class="p">,</span> <span class="s">&#34;jobID&#34;</span><span class="p">,</span> <span class="nx">job</span><span class="p">.</span><span class="nx">ID</span><span class="p">)</span></span></span></code></pre></div><p>欄位穩定後，grep、log query、<a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a> 才能可靠。</p>
<h2 id="執行建立帶預設欄位的-logger">【執行】建立帶預設欄位的 logger</h2>
<p>預設欄位的核心規則是：每筆 log 都需要的上下文，應該掛在 logger 上，而不是每次手動重複。</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">base</span> <span class="o">:=</span> <span class="nx">slog</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="nx">slog</span><span class="p">.</span><span class="nf">NewJSONHandler</span><span class="p">(</span><span class="nx">os</span><span class="p">.</span><span class="nx">Stdout</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">slog</span><span class="p">.</span><span class="nx">HandlerOptions</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">Level</span><span class="p">:</span> <span class="nx">slog</span><span class="p">.</span><span class="nx">LevelInfo</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="nx">logger</span> <span class="o">:=</span> <span class="nx">base</span><span class="p">.</span><span class="nf">With</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="s">&#34;component&#34;</span><span class="p">,</span> <span class="s">&#34;worker&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="s">&#34;version&#34;</span><span class="p">,</span> <span class="s">&#34;1.0.0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">)</span>
</span></span><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">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;job started&#34;</span><span class="p">,</span> <span class="s">&#34;jobID&#34;</span><span class="p">,</span> <span class="s">&#34;j_1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nx">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;job finished&#34;</span><span class="p">,</span> <span class="s">&#34;jobID&#34;</span><span class="p">,</span> <span class="s">&#34;j_1&#34;</span><span class="p">)</span></span></span></code></pre></div><p><code>With</code> 會回傳帶有固定欄位的新 logger。這適合 component、version、requestID 這類上下文。</p>
<h2 id="設計檢查">設計檢查</h2>
<h3 id="把所有資訊塞進-msg">把所有資訊塞進 msg</h3>
<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">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;job j_1 for user u_1 started&#34;</span><span class="p">)</span></span></span></code></pre></div><p>較佳：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;job started&#34;</span><span class="p">,</span> <span class="s">&#34;jobID&#34;</span><span class="p">,</span> <span class="s">&#34;j_1&#34;</span><span class="p">,</span> <span class="s">&#34;userID&#34;</span><span class="p">,</span> <span class="s">&#34;u_1&#34;</span><span class="p">)</span></span></span></code></pre></div><p>第二種寫法可以直接查 <code>jobID=j_1</code> 或 <code>userID=u_1</code>。</p>
<h3 id="欄位名稱不穩定">欄位名稱不穩定</h3>
<p>欄位名稱不穩定會讓查詢失效。選定 <code>userID</code> 就一路使用 <code>userID</code>，不要混用 <code>uid</code>、<code>user_id</code>、<code>user</code>。</p>
<h3 id="忽略敏感資訊">忽略敏感資訊</h3>
<p>log 會被保存與轉發。密碼、token、完整信用卡號等敏感資訊不應寫入 log。</p>
<h2 id="延伸閱讀">延伸閱讀</h2>
<p>本章只介紹標準庫 <code>log/slog</code> 的基本用法。服務開始有 domain event、state repository 或查詢需求時，可以接著閱讀 <a href="/blog/go/06-practical/structured-recording/" data-link-title="6.5 如何新增結構化記錄欄位" data-link-desc="區分 operational log、domain event log 與狀態資料">如何新增結構化記錄欄位</a>；進入生產操作後，再閱讀 <a href="/blog/go-advanced/06-production-operations/log-fields/" data-link-title="6.3 結構化日誌欄位設計" data-link-desc="讓 log 可 grep、可聚合、可追蹤">Go 進階：結構化日誌欄位設計</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、metrics 與 tracing</a>。</p>
]]></content:encoded></item><item><title>3.7 context：取消、逾時與生命週期</title><link>https://tarrragon.github.io/blog/go/03-stdlib/context/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/03-stdlib/context/</guid><description>&lt;p>&lt;code>context.Context&lt;/code> 是 Go 用來傳遞取消訊號、逾時與 request-scoped 資訊的標準機制。它的核心用途是讓一串呼叫知道「這件工作是否應該停止」。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>理解 context 的取消語義&lt;/li>
&lt;li>使用 &lt;code>context.WithCancel&lt;/code>&lt;/li>
&lt;li>使用 &lt;code>context.WithTimeout&lt;/code>&lt;/li>
&lt;li>在 goroutine 和函式呼叫鏈中傳遞 context&lt;/li>
&lt;li>避免把 context 當成一般資料容器&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察context-表示工作生命週期">【觀察】context 表示工作生命週期&lt;/h2>
&lt;p>context 的核心規則是：被取消的 context 代表這件工作不應繼續進行。長時間工作應定期檢查 &lt;code>ctx.Done()&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">Run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">error&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">select&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Done&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">ctx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Err&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">default&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nf">doOneStep&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>ctx.Done()&lt;/code> 是一個 channel。當 context 被取消或逾時，這個 channel 會被關閉。&lt;/p>
&lt;h2 id="判讀取消是由上層傳給下層">【判讀】取消是由上層傳給下層&lt;/h2>
&lt;p>context 的方向規則是：上層建立 context，下層接收 context；下層不應保存 context，也不應自行決定整個系統的生命週期。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">cancel&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WithCancel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Background&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nf">cancel&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="k">go&lt;/span> &lt;span class="nf">worker&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="nf">waitForSignal&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> &lt;span class="nf">cancel&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>context.Background()&lt;/code> 是根 context。&lt;code>context.WithCancel&lt;/code> 回傳子 context 和 cancel 函式。當 &lt;code>cancel()&lt;/code> 被呼叫，所有使用該 context 的下層工作都會收到停止訊號。&lt;/p>
&lt;h2 id="策略逾時用-withtimeout主動停止用-withcancel">【策略】逾時用 WithTimeout，主動停止用 WithCancel&lt;/h2>
&lt;p>context 建立方式的核心規則是：不知道何時停止但需要手動停止，用 &lt;code>WithCancel&lt;/code>；有明確時間限制，用 &lt;code>WithTimeout&lt;/code>。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">cancel&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WithTimeout&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Background&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">defer&lt;/span> &lt;span class="nf">cancel&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">fetchData&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">err&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>下層函式應該接收 context：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">fetchData&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">error&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">req&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewRequestWithContext&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">MethodGet&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;https://example.com&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">nil&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">err&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">resp&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">DefaultClient&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Do&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">err&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">resp&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Close&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>當 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 到達，HTTP request 會被取消。&lt;/p>
&lt;h2 id="執行讓背景-goroutine-有序退出">【執行】讓背景 goroutine 有序退出&lt;/h2>
&lt;p>背景 goroutine 的核心規則是：啟動時接收 context，迴圈中用 &lt;code>select&lt;/code> 同時等待工作與取消訊號。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">worker&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">jobs&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="nx">Job&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">select&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Done&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="nx">job&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ok&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">jobs&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">!&lt;/span>&lt;span class="nx">ok&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nf">handleJob&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">job&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 worker 有兩種退出路徑：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>退出原因&lt;/th>
 &lt;th>對應 case&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>上層取消&lt;/td>
 &lt;td>&lt;code>&amp;lt;-ctx.Done()&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>job channel 關閉&lt;/td>
 &lt;td>&lt;code>ok == false&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這比讓 goroutine 無限跑更安全，也比較容易測試。&lt;/p>
&lt;h2 id="設計檢查">設計檢查&lt;/h2>
&lt;h3 id="把-context-存進-struct">把 context 存進 struct&lt;/h3>
&lt;p>context 的生命週期屬於單次操作，不應長期存在 struct 裡。通常把 context 作為函式第一個參數：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">s&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Service&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Do&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="nx">Input&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">error&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="忘記呼叫-cancel">忘記呼叫 cancel&lt;/h3>
&lt;p>&lt;code>WithCancel&lt;/code>、&lt;code>WithTimeout&lt;/code>、&lt;code>WithDeadline&lt;/code> 回傳的 cancel 應該被呼叫，釋放相關資源：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">cancel&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WithTimeout&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">defer&lt;/span> &lt;span class="nf">cancel&lt;/span>&lt;span class="p">()&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="用-context-傳一般參數">用 context 傳一般參數&lt;/h3>
&lt;p>context value 適合 request-scoped metadata，例如 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request ID&lt;/a>。一般業務參數應放在函式參數或 struct 裡。&lt;/p></description><content:encoded><![CDATA[<p><code>context.Context</code> 是 Go 用來傳遞取消訊號、逾時與 request-scoped 資訊的標準機制。它的核心用途是讓一串呼叫知道「這件工作是否應該停止」。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>理解 context 的取消語義</li>
<li>使用 <code>context.WithCancel</code></li>
<li>使用 <code>context.WithTimeout</code></li>
<li>在 goroutine 和函式呼叫鏈中傳遞 context</li>
<li>避免把 context 當成一般資料容器</li>
</ol>
<hr>
<h2 id="觀察context-表示工作生命週期">【觀察】context 表示工作生命週期</h2>
<p>context 的核心規則是：被取消的 context 代表這件工作不應繼續進行。長時間工作應定期檢查 <code>ctx.Done()</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">Run</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Done</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            <span class="k">return</span> <span class="nx">ctx</span><span class="p">.</span><span class="nf">Err</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="nf">doOneStep</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>ctx.Done()</code> 是一個 channel。當 context 被取消或逾時，這個 channel 會被關閉。</p>
<h2 id="判讀取消是由上層傳給下層">【判讀】取消是由上層傳給下層</h2>
<p>context 的方向規則是：上層建立 context，下層接收 context；下層不應保存 context，也不應自行決定整個系統的生命週期。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">ctx</span><span class="p">,</span> <span class="nx">cancel</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">WithCancel</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">())</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">defer</span> <span class="nf">cancel</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">go</span> <span class="nf">worker</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nf">waitForSignal</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="nf">cancel</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>context.Background()</code> 是根 context。<code>context.WithCancel</code> 回傳子 context 和 cancel 函式。當 <code>cancel()</code> 被呼叫，所有使用該 context 的下層工作都會收到停止訊號。</p>
<h2 id="策略逾時用-withtimeout主動停止用-withcancel">【策略】逾時用 WithTimeout，主動停止用 WithCancel</h2>
<p>context 建立方式的核心規則是：不知道何時停止但需要手動停止，用 <code>WithCancel</code>；有明確時間限制，用 <code>WithTimeout</code>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cancel</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">WithTimeout</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="mi">2</span><span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">defer</span> <span class="nf">cancel</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">fetchData</span><span class="p">(</span><span class="nx">ctx</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>下層函式應該接收 context：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">fetchData</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">req</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">http</span><span class="p">.</span><span class="nf">NewRequestWithContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">MethodGet</span><span class="p">,</span> <span class="s">&#34;https://example.com&#34;</span><span class="p">,</span> <span class="kc">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">resp</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">http</span><span class="p">.</span><span class="nx">DefaultClient</span><span class="p">.</span><span class="nf">Do</span><span class="p">(</span><span class="nx">req</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">defer</span> <span class="nx">resp</span><span class="p">.</span><span class="nx">Body</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>當 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 到達，HTTP request 會被取消。</p>
<h2 id="執行讓背景-goroutine-有序退出">【執行】讓背景 goroutine 有序退出</h2>
<p>背景 goroutine 的核心規則是：啟動時接收 context，迴圈中用 <code>select</code> 同時等待工作與取消訊號。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">worker</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">jobs</span> <span class="o">&lt;-</span><span class="kd">chan</span> <span class="nx">Job</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Done</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">case</span> <span class="nx">job</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">jobs</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">                <span class="k">return</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="nf">handleJob</span><span class="p">(</span><span class="nx">job</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個 worker 有兩種退出路徑：</p>
<table>
  <thead>
      <tr>
          <th>退出原因</th>
          <th>對應 case</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>上層取消</td>
          <td><code>&lt;-ctx.Done()</code></td>
      </tr>
      <tr>
          <td>job channel 關閉</td>
          <td><code>ok == false</code></td>
      </tr>
  </tbody>
</table>
<p>這比讓 goroutine 無限跑更安全，也比較容易測試。</p>
<h2 id="設計檢查">設計檢查</h2>
<h3 id="把-context-存進-struct">把 context 存進 struct</h3>
<p>context 的生命週期屬於單次操作，不應長期存在 struct 裡。通常把 context 作為函式第一個參數：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">Service</span><span class="p">)</span> <span class="nf">Do</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">input</span> <span class="nx">Input</span><span class="p">)</span> <span class="kt">error</span></span></span></code></pre></div><h3 id="忘記呼叫-cancel">忘記呼叫 cancel</h3>
<p><code>WithCancel</code>、<code>WithTimeout</code>、<code>WithDeadline</code> 回傳的 cancel 應該被呼叫，釋放相關資源：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cancel</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">WithTimeout</span><span class="p">(</span><span class="nx">parent</span><span class="p">,</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">defer</span> <span class="nf">cancel</span><span class="p">()</span></span></span></code></pre></div><h3 id="用-context-傳一般參數">用 context 傳一般參數</h3>
<p>context value 適合 request-scoped metadata，例如 <a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request ID</a>。一般業務參數應放在函式參數或 struct 裡。</p>
]]></content:encoded></item><item><title>3.8 defer 與資源清理</title><link>https://tarrragon.github.io/blog/go/03-stdlib/defer-cleanup/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/03-stdlib/defer-cleanup/</guid><description>&lt;p>&lt;code>defer&lt;/code> 的核心用途是把資源清理放在取得資源的附近。檔案、鎖、response body、temporary resource 與測試 cleanup 都適合用 &lt;code>defer&lt;/code> 表達「離開這個 scope 前要完成的事」。&lt;/p>
&lt;h2 id="預計補充內容">預計補充內容&lt;/h2>
&lt;p>這些資源清理邊界會在下列章節展開：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/select-loop/" data-link-title="1.2 select loop 的生命週期設計" data-link-desc="理解長時間運行 goroutine 如何同時處理事件、ticker 與取消">Go 進階：select loop 的生命週期設計&lt;/a>：長生命週期的 goroutine 會怎麼收尾，和 &lt;code>defer&lt;/code> 的 scope 觀念直接相關。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">Go 進階：graceful shutdown 與 signal handling&lt;/a>：當 process 要停下來時，&lt;code>defer&lt;/code> 常常是 cleanup 的最後一道保險。&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>t.Cleanup&lt;/code>，會比單純 close 更能說清楚責任。&lt;/li>
&lt;/ul>
&lt;h2 id="與-go-進階的關係">與 Go 進階的關係&lt;/h2>
&lt;p>本章建立基本資源清理語感。長時間 worker、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket&lt;/a> pump 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown&lt;/a> 會在 &lt;a href="https://tarrragon.github.io/blog/go-advanced/01-concurrency-patterns/select-loop/" data-link-title="1.2 select loop 的生命週期設計" data-link-desc="理解長時間運行 goroutine 如何同時處理事件、ticker 與取消">Go 進階：select loop 的生命週期設計&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown 與 signal handling&lt;/a> 中延伸。&lt;/p>
&lt;h2 id="和-go-教材的關係">和 Go 教材的關係&lt;/h2>
&lt;p>這一章承接的是資源生命週期、goroutine 停止與 shutdown；如果你要先回看語言教材，可以讀：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/04-concurrency/goroutine/" data-link-title="4.1 goroutine：輕量並發工作" data-link-desc="用 goroutine 啟動並發工作，並設計清楚的退出條件">Go：goroutine：輕量並發工作&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/04-concurrency/select/" data-link-title="4.3 select：同時等待多種事件" data-link-desc="用 select 建立事件迴圈">Go：select：同時等待多種事件&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/03-runtime-profiling/goroutine-leak/" data-link-title="3.3 goroutine leak 偵測" data-link-desc="判斷背景工作與 client pump 是否正確退出">Go 進階：goroutine leak 偵測&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">Go 進階：graceful shutdown 與 signal handling&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p><code>defer</code> 的核心用途是把資源清理放在取得資源的附近。檔案、鎖、response body、temporary resource 與測試 cleanup 都適合用 <code>defer</code> 表達「離開這個 scope 前要完成的事」。</p>
<h2 id="預計補充內容">預計補充內容</h2>
<p>這些資源清理邊界會在下列章節展開：</p>
<ul>
<li><a href="/blog/go-advanced/01-concurrency-patterns/select-loop/" data-link-title="1.2 select loop 的生命週期設計" data-link-desc="理解長時間運行 goroutine 如何同時處理事件、ticker 與取消">Go 進階：select loop 的生命週期設計</a>：長生命週期的 goroutine 會怎麼收尾，和 <code>defer</code> 的 scope 觀念直接相關。</li>
<li><a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">Go 進階：graceful shutdown 與 signal handling</a>：當 process 要停下來時，<code>defer</code> 常常是 cleanup 的最後一道保險。</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>t.Cleanup</code>，會比單純 close 更能說清楚責任。</li>
</ul>
<h2 id="與-go-進階的關係">與 Go 進階的關係</h2>
<p>本章建立基本資源清理語感。長時間 worker、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> pump 與 <a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a> 會在 <a href="/blog/go-advanced/01-concurrency-patterns/select-loop/" data-link-title="1.2 select loop 的生命週期設計" data-link-desc="理解長時間運行 goroutine 如何同時處理事件、ticker 與取消">Go 進階：select loop 的生命週期設計</a> 與 <a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown 與 signal handling</a> 中延伸。</p>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是資源生命週期、goroutine 停止與 shutdown；如果你要先回看語言教材，可以讀：</p>
<ul>
<li><a href="/blog/go/04-concurrency/goroutine/" data-link-title="4.1 goroutine：輕量並發工作" data-link-desc="用 goroutine 啟動並發工作，並設計清楚的退出條件">Go：goroutine：輕量並發工作</a></li>
<li><a href="/blog/go/04-concurrency/select/" data-link-title="4.3 select：同時等待多種事件" data-link-desc="用 select 建立事件迴圈">Go：select：同時等待多種事件</a></li>
<li><a href="/blog/go-advanced/03-runtime-profiling/goroutine-leak/" data-link-title="3.3 goroutine leak 偵測" data-link-desc="判斷背景工作與 client pump 是否正確退出">Go 進階：goroutine leak 偵測</a></li>
<li><a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">Go 進階：graceful shutdown 與 signal handling</a></li>
</ul>
]]></content:encoded></item><item><title>3.9 flag、os/env 與設定邊界</title><link>https://tarrragon.github.io/blog/go/03-stdlib/config-flags-env/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/03-stdlib/config-flags-env/</guid><description>&lt;p>設定讀取的核心責任是把外部字串轉成程式內部的 typed config。環境變數、命令列 flag、設定檔與預設值都只是輸入來源；application 應依賴已驗證的 config struct。&lt;/p>
&lt;h2 id="預計補充內容">預計補充內容&lt;/h2>
&lt;p>這些設定邊界會在下列章節展開：&lt;/p>
&lt;ul>
&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;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/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口&lt;/a>：像 secret manager、ConfigMap 與 rollout 這類平台責任應該留給 Backend。&lt;/li>
&lt;/ul>
&lt;h2 id="與-backend-教材的分工">與 Backend 教材的分工&lt;/h2>
&lt;p>本章只處理 Go 程式內的設定邊界。secret manager、Kubernetes ConfigMap、container environment、遠端動態設定與部署平台 rollout 會放在 &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>這一章承接的是入口流程與 composition root；如果你要先回看語言教材，可以讀：&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/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;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;/ul></description><content:encoded><![CDATA[<p>設定讀取的核心責任是把外部字串轉成程式內部的 typed config。環境變數、命令列 flag、設定檔與預設值都只是輸入來源；application 應依賴已驗證的 config struct。</p>
<h2 id="預計補充內容">預計補充內容</h2>
<p>這些設定邊界會在下列章節展開：</p>
<ul>
<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>
<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/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">Backend：部署平台與網路入口</a>：像 secret manager、ConfigMap 與 rollout 這類平台責任應該留給 Backend。</li>
</ul>
<h2 id="與-backend-教材的分工">與 Backend 教材的分工</h2>
<p>本章只處理 Go 程式內的設定邊界。secret manager、Kubernetes ConfigMap、container environment、遠端動態設定與部署平台 rollout 會放在 <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>這一章承接的是入口流程與 composition root；如果你要先回看語言教材，可以讀：</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/07-refactoring/composition-root/" data-link-title="7.7 composition root 與依賴組裝" data-link-desc="把具體 adapter、config 與 usecase wiring 留在應用入口層">Go：composition root 與依賴組裝</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>
</ul>
]]></content:encoded></item><item><title>3.10 標準庫如何支撐服務型 Go</title><link>https://tarrragon.github.io/blog/go/03-stdlib/service-support/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/03-stdlib/service-support/</guid><description>&lt;p>Go 標準庫的服務價值在於它直接提供 HTTP、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a>、取消、日誌與資源管理的基本能力。這一章把前面學過的工具串成服務底座，讓讀者理解標準庫如何支撐後端程式，而不只是個別 API 的使用方式。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>看出標準庫為什麼是 Go 服務的底座&lt;/li>
&lt;li>把 &lt;code>context&lt;/code>、&lt;code>net/http&lt;/code>、&lt;code>log/slog&lt;/code>、&lt;code>defer&lt;/code> 與 &lt;code>time&lt;/code> 串成一個服務模型&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;code>net/http&lt;/code> 可以直接建立服務，&lt;code>context&lt;/code> 可以控制取消與 timeout，&lt;code>log/slog&lt;/code> 可以支援結構化日誌，&lt;code>defer&lt;/code> 可以整理資源釋放，&lt;code>time&lt;/code> 可以處理期限與排程。&lt;/p>
&lt;p>這些能力拼在一起，就是一個後端服務最基本的底盤。&lt;/p>
&lt;h2 id="判讀context-是服務生命週期的中心">【判讀】context 是服務生命週期的中心&lt;/h2>
&lt;p>在服務型 Go 裡，&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> 的共同語言。當 handler、worker、DB、Redis 都接受 context 時，整個流程就能在同一個生命週期邊界內運作；缺少 context 的長時間流程會讓取消與逾時難以傳遞。&lt;/p>
&lt;h2 id="判讀nethttp-讓入口保持簡單">【判讀】net/http 讓入口保持簡單&lt;/h2>
&lt;p>&lt;code>net/http&lt;/code> 的 handler 模型很薄，這是優點。它讓你能快速建立路由、驗證 request、回傳 response，而不需要先學一大套框架約定。對服務型 Go 來說，這種簡單性會直接降低協作成本。&lt;/p>
&lt;h2 id="策略log-與-defer-讓邊界更完整">【策略】log 與 defer 讓邊界更完整&lt;/h2>
&lt;p>&lt;code>log/slog&lt;/code> 提供結構化日誌，讓高併發服務的診斷更容易；&lt;code>defer&lt;/code> 則讓 close、unlock、cancel 等收尾操作更安全。這兩個工具都是 Go 在長時間運行服務中很重要的可靠性支撐。&lt;/p>
&lt;h2 id="小結">小結&lt;/h2>
&lt;p>標準庫是 Go 成為服務語言的核心原因之一。當你把 &lt;code>context&lt;/code>、&lt;code>net/http&lt;/code>、&lt;code>log/slog&lt;/code>、&lt;code>defer&lt;/code> 與 &lt;code>time&lt;/code> 看成一組工具時，就更容易理解 Go 為什麼適合做後端服務。&lt;/p></description><content:encoded><![CDATA[<p>Go 標準庫的服務價值在於它直接提供 HTTP、<a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a>、取消、日誌與資源管理的基本能力。這一章把前面學過的工具串成服務底座，讓讀者理解標準庫如何支撐後端程式，而不只是個別 API 的使用方式。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>看出標準庫為什麼是 Go 服務的底座</li>
<li>把 <code>context</code>、<code>net/http</code>、<code>log/slog</code>、<code>defer</code> 與 <code>time</code> 串成一個服務模型</li>
<li>理解為什麼這些工具會讓服務更可維護</li>
<li>把標準庫能力轉成實際服務邊界</li>
<li>知道何時標準庫已足夠，何時才需要外部框架</li>
</ol>
<hr>
<h2 id="觀察標準庫本身就能做服務">【觀察】標準庫本身就能做服務</h2>
<p>Go 的標準庫已經包含服務程式需要的主要基礎能力。<code>net/http</code> 可以直接建立服務，<code>context</code> 可以控制取消與 timeout，<code>log/slog</code> 可以支援結構化日誌，<code>defer</code> 可以整理資源釋放，<code>time</code> 可以處理期限與排程。</p>
<p>這些能力拼在一起，就是一個後端服務最基本的底盤。</p>
<h2 id="判讀context-是服務生命週期的中心">【判讀】context 是服務生命週期的中心</h2>
<p>在服務型 Go 裡，<code>context</code> 是請求、取消與 <a href="/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">deadline</a> 的共同語言。當 handler、worker、DB、Redis 都接受 context 時，整個流程就能在同一個生命週期邊界內運作；缺少 context 的長時間流程會讓取消與逾時難以傳遞。</p>
<h2 id="判讀nethttp-讓入口保持簡單">【判讀】net/http 讓入口保持簡單</h2>
<p><code>net/http</code> 的 handler 模型很薄，這是優點。它讓你能快速建立路由、驗證 request、回傳 response，而不需要先學一大套框架約定。對服務型 Go 來說，這種簡單性會直接降低協作成本。</p>
<h2 id="策略log-與-defer-讓邊界更完整">【策略】log 與 defer 讓邊界更完整</h2>
<p><code>log/slog</code> 提供結構化日誌，讓高併發服務的診斷更容易；<code>defer</code> 則讓 close、unlock、cancel 等收尾操作更安全。這兩個工具都是 Go 在長時間運行服務中很重要的可靠性支撐。</p>
<h2 id="小結">小結</h2>
<p>標準庫是 Go 成為服務語言的核心原因之一。當你把 <code>context</code>、<code>net/http</code>、<code>log/slog</code>、<code>defer</code> 與 <code>time</code> 看成一組工具時，就更容易理解 Go 為什麼適合做後端服務。</p>
]]></content:encoded></item></channel></rss>