<?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/02-types-data/</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/02-types-data/index.xml" rel="self" type="application/rss+xml"/><item><title>2.1 struct 與 JSON tag</title><link>https://tarrragon.github.io/blog/go/02-types-data/struct-json/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/02-types-data/struct-json/</guid><description>&lt;p>Go 的 struct 用來描述資料形狀：有哪些欄位、欄位是什麼型別、哪些資料應該放在一起。當資料需要存成 JSON 或透過 API 傳輸時，JSON tag 會把 Go 的欄位命名對應到外部格式。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>定義可序列化成 JSON 的 struct&lt;/li>
&lt;li>理解 &lt;code>omitempty&lt;/code> 的 API 語義&lt;/li>
&lt;li>分辨內部欄位命名與外部 JSON 命名&lt;/li>
&lt;li>看懂設定檔、API request 與事件資料模型&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察struct-用欄位集合定義資料形狀">【觀察】struct 用欄位集合定義資料形狀&lt;/h2>
&lt;p>struct 的核心規則是：每個欄位都用名稱和型別描述一部分資料。以下範例用 struct 定義一份應用設定：&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;/code>&lt;/pre>&lt;/div>&lt;p>這段程式同時回答兩個問題：Go 程式內用哪些欄位處理設定，以及 JSON 檔案裡的欄位名稱是什麼。&lt;/p>
&lt;h2 id="判讀json-tag-是外部資料格式-contract">【判讀】JSON tag 是外部資料格式 contract&lt;/h2>
&lt;p>JSON tag 的核心規則是：Go 欄位名稱服務程式碼可見性，JSON 欄位名稱服務外部資料格式。&lt;code>AppName&lt;/code> 對應 &lt;code>appName&lt;/code> 是兩個命名慣例的交界：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層次&lt;/th>
 &lt;th>命名&lt;/th>
 &lt;th>原因&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Go struct 欄位&lt;/td>
 &lt;td>&lt;code>AppName&lt;/code>&lt;/td>
 &lt;td>exported 欄位必須大寫開頭&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>JSON payload&lt;/td>
 &lt;td>&lt;code>appName&lt;/code>&lt;/td>
 &lt;td>JSON 與 API 常用 camelCase&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>omitempty&lt;/code> 宣告「這個欄位在某些資料情境中不是必要資料」。它是可選欄位的語義標記；欄位為零值時，JSON 序列化會跳過輸出。&lt;/p>
&lt;h2 id="策略先用資料語意決定欄位是否必要">【策略】先用資料語意決定欄位是否必要&lt;/h2>
&lt;p>設計 JSON 資料時，先分辨欄位角色：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>欄位角色&lt;/th>
 &lt;th>tag 策略&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>每筆資料都需要&lt;/td>
 &lt;td>不加 &lt;code>omitempty&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>只有部分情境需要&lt;/td>
 &lt;td>加 &lt;code>omitempty&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>內部使用，不輸出 JSON&lt;/td>
 &lt;td>使用 &lt;code>json:&amp;quot;-&amp;quot;&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>外部名稱需要穩定&lt;/td>
 &lt;td>明確寫 tag，不依賴預設&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這樣資料 contract 會比「把 struct 全部輸出」更清楚。&lt;/p>
&lt;h2 id="執行事件資料建模">【執行】事件資料建模&lt;/h2>
&lt;p>事件資料模型的核心規則是：事件本身必備欄位不使用 &lt;code>omitempty&lt;/code>，事件內容可依類型使用可選欄位。&lt;code>UserEvent&lt;/code> 表示一筆使用者行為事件，可以來自檔案、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;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">UserEvent&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">UserID&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="s">`json:&amp;#34;userId&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">Type&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="s">`json:&amp;#34;type&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">Timestamp&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="s">`json:&amp;#34;timestamp&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="nx">Source&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="s">`json:&amp;#34;source&amp;#34;`&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">Payload&lt;/span> &lt;span class="nx">Payload&lt;/span> &lt;span class="s">`json:&amp;#34;payload&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>這個 struct 的欄位都沒有 &lt;code>omitempty&lt;/code>，表示它是事件流中的完整資料單位。相比之下，&lt;code>Payload&lt;/code> 可以依事件類型使用 &lt;code>omitempty&lt;/code>，因為不同事件只會填入部分欄位。&lt;/p>
&lt;h2 id="巢狀-struct">巢狀 struct&lt;/h2>
&lt;p>巢狀 struct 的核心規則是：資料本身有層次時，Go 型別也應保留同樣層次。以下設定把 server 相關欄位集中到 &lt;code>ServerConfig&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">ServerConfig&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">Host&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="s">`json:&amp;#34;host&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="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">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"> 7&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"> 8&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"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">Server&lt;/span> &lt;span class="nx">ServerConfig&lt;/span> &lt;span class="s">`json:&amp;#34;server&amp;#34;`&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>對應 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;appName&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;notify&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;debug&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&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;server&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">5&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;host&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;localhost&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;port&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">8080&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這樣的設計讓資料層次在 Go 程式中也看得見。&lt;/p></description><content:encoded><![CDATA[<p>Go 的 struct 用來描述資料形狀：有哪些欄位、欄位是什麼型別、哪些資料應該放在一起。當資料需要存成 JSON 或透過 API 傳輸時，JSON tag 會把 Go 的欄位命名對應到外部格式。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>定義可序列化成 JSON 的 struct</li>
<li>理解 <code>omitempty</code> 的 API 語義</li>
<li>分辨內部欄位命名與外部 JSON 命名</li>
<li>看懂設定檔、API request 與事件資料模型</li>
</ol>
<hr>
<h2 id="觀察struct-用欄位集合定義資料形狀">【觀察】struct 用欄位集合定義資料形狀</h2>
<p>struct 的核心規則是：每個欄位都用名稱和型別描述一部分資料。以下範例用 struct 定義一份應用設定：</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></code></pre></div><p>這段程式同時回答兩個問題：Go 程式內用哪些欄位處理設定，以及 JSON 檔案裡的欄位名稱是什麼。</p>
<h2 id="判讀json-tag-是外部資料格式-contract">【判讀】JSON tag 是外部資料格式 contract</h2>
<p>JSON tag 的核心規則是：Go 欄位名稱服務程式碼可見性，JSON 欄位名稱服務外部資料格式。<code>AppName</code> 對應 <code>appName</code> 是兩個命名慣例的交界：</p>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>命名</th>
          <th>原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Go struct 欄位</td>
          <td><code>AppName</code></td>
          <td>exported 欄位必須大寫開頭</td>
      </tr>
      <tr>
          <td>JSON payload</td>
          <td><code>appName</code></td>
          <td>JSON 與 API 常用 camelCase</td>
      </tr>
  </tbody>
</table>
<p><code>omitempty</code> 宣告「這個欄位在某些資料情境中不是必要資料」。它是可選欄位的語義標記；欄位為零值時，JSON 序列化會跳過輸出。</p>
<h2 id="策略先用資料語意決定欄位是否必要">【策略】先用資料語意決定欄位是否必要</h2>
<p>設計 JSON 資料時，先分辨欄位角色：</p>
<table>
  <thead>
      <tr>
          <th>欄位角色</th>
          <th>tag 策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>每筆資料都需要</td>
          <td>不加 <code>omitempty</code></td>
      </tr>
      <tr>
          <td>只有部分情境需要</td>
          <td>加 <code>omitempty</code></td>
      </tr>
      <tr>
          <td>內部使用，不輸出 JSON</td>
          <td>使用 <code>json:&quot;-&quot;</code></td>
      </tr>
      <tr>
          <td>外部名稱需要穩定</td>
          <td>明確寫 tag，不依賴預設</td>
      </tr>
  </tbody>
</table>
<p>這樣資料 contract 會比「把 struct 全部輸出」更清楚。</p>
<h2 id="執行事件資料建模">【執行】事件資料建模</h2>
<p>事件資料模型的核心規則是：事件本身必備欄位不使用 <code>omitempty</code>，事件內容可依類型使用可選欄位。<code>UserEvent</code> 表示一筆使用者行為事件，可以來自檔案、HTTP API 或 message <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">type</span> <span class="nx">UserEvent</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">UserID</span>    <span class="kt">string</span>    <span class="s">`json:&#34;userId&#34;`</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">Type</span>      <span class="kt">string</span>    <span class="s">`json:&#34;type&#34;`</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">Timestamp</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> <span class="s">`json:&#34;timestamp&#34;`</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nx">Source</span>    <span class="kt">string</span>    <span class="s">`json:&#34;source&#34;`</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">Payload</span>   <span class="nx">Payload</span>   <span class="s">`json:&#34;payload&#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>這個 struct 的欄位都沒有 <code>omitempty</code>，表示它是事件流中的完整資料單位。相比之下，<code>Payload</code> 可以依事件類型使用 <code>omitempty</code>，因為不同事件只會填入部分欄位。</p>
<h2 id="巢狀-struct">巢狀 struct</h2>
<p>巢狀 struct 的核心規則是：資料本身有層次時，Go 型別也應保留同樣層次。以下設定把 server 相關欄位集中到 <code>ServerConfig</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">ServerConfig</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">Host</span> <span class="kt">string</span> <span class="s">`json:&#34;host&#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="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="kd">type</span> <span class="nx">Config</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">AppName</span> <span class="kt">string</span>       <span class="s">`json:&#34;appName&#34;`</span>
</span></span><span class="line"><span class="ln"> 8</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"> 9</span><span class="cl">    <span class="nx">Server</span>  <span class="nx">ServerConfig</span> <span class="s">`json:&#34;server&#34;`</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>對應 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;appName&#34;</span><span class="p">:</span> <span class="s2">&#34;notify&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;debug&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nt">&#34;server&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nt">&#34;host&#34;</span><span class="p">:</span> <span class="s2">&#34;localhost&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nt">&#34;port&#34;</span><span class="p">:</span> <span class="mi">8080</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這樣的設計讓資料層次在 Go 程式中也看得見。</p>
]]></content:encoded></item><item><title>2.2 slice 與 map</title><link>https://tarrragon.github.io/blog/go/02-types-data/slices-maps/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/02-types-data/slices-maps/</guid><description>&lt;p>slice 和 map 是 Go 最常用的集合型別。slice 表達有順序的資料列表，map 表達 key-value 查詢表。理解它們的行為，是寫出可靠 Go 程式的基本功。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>建立與操作 slice&lt;/li>
&lt;li>理解 slice 的長度、容量與 append&lt;/li>
&lt;li>建立與操作 map&lt;/li>
&lt;li>判斷何時使用 slice，何時使用 map&lt;/li>
&lt;li>避免 nil slice、nil map 與共享底層資料的常見問題&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察slice-表達有順序的資料">【觀察】slice 表達有順序的資料&lt;/h2>
&lt;p>slice 是 Go 中用來表示有順序元素列表的集合型別。以下範例建立一個 &lt;code>[]string&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">names&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s">&amp;#34;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>&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">i&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="nx">names&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">i&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>slice 的常見操作是讀取元素、取得長度、用 &lt;code>append&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">names&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">names&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;Dave&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="nx">first&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">names&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">3&lt;/span>&lt;span class="cl">&lt;span class="nx">count&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">names&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>append&lt;/code> 的核心規則是：它會回傳 append 後的 slice，呼叫端必須接回結果。&lt;code>len(names)&lt;/code> 取得元素數量；&lt;code>append&lt;/code> 可能重用原底層 array，也可能配置新底層 array：&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">names&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">names&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;Eve&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="判讀slice-是對底層-array-的視窗">【判讀】slice 是對底層 array 的視窗&lt;/h2>
&lt;p>slice 的核心模型是「指向底層 array 的視窗」，不是 array 本身。它比較像一個描述底層 array 區段的 header，包含：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">pointer -&amp;gt; 底層 array
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">len -&amp;gt; 目前看得到幾個元素
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">cap -&amp;gt; 從起點到底層 array 結尾還有多少容量&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>長度與容量分別描述「目前元素數」與「不重新配置時還能擴張多少」。以下範例可以觀察 &lt;code>len&lt;/code> 和 &lt;code>cap&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">items&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">([]&lt;/span>&lt;span class="kt">int&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">3&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="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">items&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="nb">cap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">items&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1">// 0 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">items&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">items&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">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="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">items&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="nb">cap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">items&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1">// 1 3&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>append&lt;/code> 超過容量時，Go 可能會配置新的底層 array：&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">items&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">items&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">20&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">40&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="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">items&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="nb">cap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">items&lt;/span>&lt;span class="p">))&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這就是 &lt;code>append&lt;/code> 必須接回原變數的原因：append 後的 slice 可能已經指向新的底層資料。&lt;/p>
&lt;h2 id="策略用-slice-保存順序用-map-做查詢">【策略】用 slice 保存順序，用 map 做查詢&lt;/h2>
&lt;p>選擇集合型別的核心規則是：在意順序用 slice，需要 key-value 查詢用 map。如果你在意資料順序，用 slice：&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">tasks&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s">&amp;#34;read&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;write&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;test&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果你要用 key 快速查資料，用 map：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">scores&lt;/span> &lt;span class="o">:=&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">int&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="s">&amp;#34;Alice&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">90&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="s">&amp;#34;Bob&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">85&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>讀取 map：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">score&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ok&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">scores&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">2&lt;/span>&lt;span class="cl">&lt;span class="k">if&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">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">score&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>map 讀取的核心規則是：需要分辨「不存在」和「零值」時，必須使用 &lt;code>value, ok&lt;/code>。key 不存在時，map 會回傳 value type 的零值：&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">score&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">scores&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">&amp;#34;Unknown&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1">// 0&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果不檢查 &lt;code>ok&lt;/code>，你無法分辨「不存在」和「存在但分數是 0」。&lt;/p>
&lt;h2 id="執行nil-slice-與-nil-map-的差異">【執行】nil slice 與 nil map 的差異&lt;/h2>
&lt;p>nil slice 和 nil map 的核心差異是：nil slice 可以 append，nil map 不能寫入。nil slice 可以 append：&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">names&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nx">names&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">names&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;Alice&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>nil map 不能直接寫入：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">scores&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">int&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">scores&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="p">=&lt;/span> &lt;span class="mi">90&lt;/span> &lt;span class="c1">// panic&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>map 寫入前必須先初始化：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">scores&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="kt">int&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nx">scores&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="p">=&lt;/span> &lt;span class="mi">90&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>或用 literal：&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">scores&lt;/span> &lt;span class="o">:=&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">int&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="s">&amp;#34;Alice&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">90&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;h2 id="slice-和-map-的常見組合">slice 和 map 的常見組合&lt;/h2>
&lt;h3 id="用-slice-保存輸出順序">用 slice 保存輸出順序&lt;/h3>
&lt;p>map 的迭代順序不保證穩定；如果輸出順序重要，必須額外用 slice 保存或排序 key：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">score&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">scores&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">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">score&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>先整理 key：&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">names&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">([]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scores&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="nx">name&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">scores&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">names&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">names&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">name&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 class="nx">sort&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Strings&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">names&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="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="nx">names&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="nx">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">scores&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">9&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="用-map-當-set">用 map 當 set&lt;/h3>
&lt;p>Go 沒有內建 set；需要集合語義時，常用 &lt;code>map[string]struct{}&lt;/code> 表示「某個 key 是否存在」：&lt;/p></description><content:encoded><![CDATA[<p>slice 和 map 是 Go 最常用的集合型別。slice 表達有順序的資料列表，map 表達 key-value 查詢表。理解它們的行為，是寫出可靠 Go 程式的基本功。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>建立與操作 slice</li>
<li>理解 slice 的長度、容量與 append</li>
<li>建立與操作 map</li>
<li>判斷何時使用 slice，何時使用 map</li>
<li>避免 nil slice、nil map 與共享底層資料的常見問題</li>
</ol>
<hr>
<h2 id="觀察slice-表達有順序的資料">【觀察】slice 表達有順序的資料</h2>
<p>slice 是 Go 中用來表示有順序元素列表的集合型別。以下範例建立一個 <code>[]string</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">names</span> <span class="o">:=</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></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">i</span><span class="p">,</span> <span class="nx">name</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">names</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">i</span><span class="p">,</span> <span class="nx">name</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>slice 的常見操作是讀取元素、取得長度、用 <code>append</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">names</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">names</span><span class="p">,</span> <span class="s">&#34;Dave&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">first</span> <span class="o">:=</span> <span class="nx">names</span><span class="p">[</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">count</span> <span class="o">:=</span> <span class="nb">len</span><span class="p">(</span><span class="nx">names</span><span class="p">)</span></span></span></code></pre></div><p><code>append</code> 的核心規則是：它會回傳 append 後的 slice，呼叫端必須接回結果。<code>len(names)</code> 取得元素數量；<code>append</code> 可能重用原底層 array，也可能配置新底層 array：</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">names</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">names</span><span class="p">,</span> <span class="s">&#34;Eve&#34;</span><span class="p">)</span></span></span></code></pre></div><h2 id="判讀slice-是對底層-array-的視窗">【判讀】slice 是對底層 array 的視窗</h2>
<p>slice 的核心模型是「指向底層 array 的視窗」，不是 array 本身。它比較像一個描述底層 array 區段的 header，包含：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">pointer -&gt; 底層 array
</span></span><span class="line"><span class="ln">2</span><span class="cl">len     -&gt; 目前看得到幾個元素
</span></span><span class="line"><span class="ln">3</span><span class="cl">cap     -&gt; 從起點到底層 array 結尾還有多少容量</span></span></code></pre></div><p>長度與容量分別描述「目前元素數」與「不重新配置時還能擴張多少」。以下範例可以觀察 <code>len</code> 和 <code>cap</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">items</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">int</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">3</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="nb">len</span><span class="p">(</span><span class="nx">items</span><span class="p">),</span> <span class="nb">cap</span><span class="p">(</span><span class="nx">items</span><span class="p">))</span> <span class="c1">// 0 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">items</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">items</span><span class="p">,</span> <span class="mi">10</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="nb">len</span><span class="p">(</span><span class="nx">items</span><span class="p">),</span> <span class="nb">cap</span><span class="p">(</span><span class="nx">items</span><span class="p">))</span> <span class="c1">// 1 3</span></span></span></code></pre></div><p><code>append</code> 超過容量時，Go 可能會配置新的底層 array：</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">items</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">items</span><span class="p">,</span> <span class="mi">20</span><span class="p">,</span> <span class="mi">30</span><span class="p">,</span> <span class="mi">40</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="nb">len</span><span class="p">(</span><span class="nx">items</span><span class="p">),</span> <span class="nb">cap</span><span class="p">(</span><span class="nx">items</span><span class="p">))</span></span></span></code></pre></div><p>這就是 <code>append</code> 必須接回原變數的原因：append 後的 slice 可能已經指向新的底層資料。</p>
<h2 id="策略用-slice-保存順序用-map-做查詢">【策略】用 slice 保存順序，用 map 做查詢</h2>
<p>選擇集合型別的核心規則是：在意順序用 slice，需要 key-value 查詢用 map。如果你在意資料順序，用 slice：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">tasks</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;read&#34;</span><span class="p">,</span> <span class="s">&#34;write&#34;</span><span class="p">,</span> <span class="s">&#34;test&#34;</span><span class="p">}</span></span></span></code></pre></div><p>如果你要用 key 快速查資料，用 map：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">scores</span> <span class="o">:=</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">int</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="s">&#34;Alice&#34;</span><span class="p">:</span> <span class="mi">90</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s">&#34;Bob&#34;</span><span class="p">:</span>   <span class="mi">85</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>讀取 map：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">score</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">scores</span><span class="p">[</span><span class="s">&#34;Alice&#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">ok</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="nx">score</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>map 讀取的核心規則是：需要分辨「不存在」和「零值」時，必須使用 <code>value, ok</code>。key 不存在時，map 會回傳 value type 的零值：</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">score</span> <span class="o">:=</span> <span class="nx">scores</span><span class="p">[</span><span class="s">&#34;Unknown&#34;</span><span class="p">]</span> <span class="c1">// 0</span></span></span></code></pre></div><p>如果不檢查 <code>ok</code>，你無法分辨「不存在」和「存在但分數是 0」。</p>
<h2 id="執行nil-slice-與-nil-map-的差異">【執行】nil slice 與 nil map 的差異</h2>
<p>nil slice 和 nil map 的核心差異是：nil slice 可以 append，nil map 不能寫入。nil slice 可以 append：</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">names</span> <span class="p">[]</span><span class="kt">string</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">names</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">names</span><span class="p">,</span> <span class="s">&#34;Alice&#34;</span><span class="p">)</span></span></span></code></pre></div><p>nil map 不能直接寫入：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">scores</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">int</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">scores</span><span class="p">[</span><span class="s">&#34;Alice&#34;</span><span class="p">]</span> <span class="p">=</span> <span class="mi">90</span> <span class="c1">// panic</span></span></span></code></pre></div><p>map 寫入前必須先初始化：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">scores</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">int</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">scores</span><span class="p">[</span><span class="s">&#34;Alice&#34;</span><span class="p">]</span> <span class="p">=</span> <span class="mi">90</span></span></span></code></pre></div><p>或用 literal：</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">scores</span> <span class="o">:=</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">int</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="s">&#34;Alice&#34;</span><span class="p">:</span> <span class="mi">90</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><h2 id="slice-和-map-的常見組合">slice 和 map 的常見組合</h2>
<h3 id="用-slice-保存輸出順序">用 slice 保存輸出順序</h3>
<p>map 的迭代順序不保證穩定；如果輸出順序重要，必須額外用 slice 保存或排序 key：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">for</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">score</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">scores</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">name</span><span class="p">,</span> <span class="nx">score</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>先整理 key：</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">names</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">string</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">scores</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="nx">name</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">scores</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">names</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">names</span><span class="p">,</span> <span class="nx">name</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nx">sort</span><span class="p">.</span><span class="nf">Strings</span><span class="p">(</span><span class="nx">names</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="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="nx">names</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="nx">name</span><span class="p">,</span> <span class="nx">scores</span><span class="p">[</span><span class="nx">name</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><h3 id="用-map-當-set">用 map 當 set</h3>
<p>Go 沒有內建 set；需要集合語義時，常用 <code>map[string]struct{}</code> 表示「某個 key 是否存在」：</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">seen</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kd">struct</span><span class="p">{})</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">seen</span><span class="p">[</span><span class="s">&#34;Alice&#34;</span><span class="p">]</span> <span class="p">=</span> <span class="kd">struct</span><span class="p">{}{}</span>
</span></span><span class="line"><span class="ln">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">_</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">seen</span><span class="p">[</span><span class="s">&#34;Alice&#34;</span><span class="p">];</span> <span class="nx">ok</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="s">&#34;already seen&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>如果想更直觀，也可以用 <code>map[string]bool</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">seen</span> <span class="o">:=</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">bool</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="s">&#34;Alice&#34;</span><span class="p">:</span> <span class="kc">true</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><h2 id="設計檢查">設計檢查</h2>
<h3 id="檢查一接住-append-回傳值">檢查一：接住 append 回傳值</h3>





<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="nb">append</span><span class="p">(</span><span class="nx">names</span><span class="p">,</span> <span class="s">&#34;Alice&#34;</span><span class="p">)</span> <span class="c1">// 編譯錯誤：append 結果未使用</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">names</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">names</span><span class="p">,</span> <span class="s">&#34;Alice&#34;</span><span class="p">)</span></span></span></code></pre></div><h3 id="檢查二寫入前初始化-map">檢查二：寫入前初始化 map</h3>





<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">m</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">int</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">m</span><span class="p">[</span><span class="s">&#34;x&#34;</span><span class="p">]</span> <span class="p">=</span> <span class="mi">1</span> <span class="c1">// panic</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">m</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">int</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">m</span><span class="p">[</span><span class="s">&#34;x&#34;</span><span class="p">]</span> <span class="p">=</span> <span class="mi">1</span></span></span></code></pre></div><h3 id="檢查三需要順序時先排序-key">檢查三：需要順序時先排序 key</h3>
<p>map 的順序不能拿來做穩定輸出、測試 snapshot 或 UI 排序。需要順序就額外維護 slice。</p>
]]></content:encoded></item><item><title>2.3 interface：用行為定義依賴</title><link>https://tarrragon.github.io/blog/go/02-types-data/interfaces/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/02-types-data/interfaces/</guid><description>&lt;p>Go 的 interface 描述的是行為，不是繼承關係。你不需要在 concrete type 上宣告「我實作了某個 interface」；只要方法集合符合，Go 就視為實作。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>理解 implicit interface 的設計精神&lt;/li>
&lt;li>寫出小而精準的 interface&lt;/li>
&lt;li>避免把 concrete type 暴露給不需要的呼叫者&lt;/li>
&lt;li>用 interface 改善測試與依賴邊界&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察interface-只描述需要的行為">【觀察】interface 只描述需要的行為&lt;/h2>
&lt;p>interface 的核心規則是：只描述呼叫者需要的行為，不描述實作者的完整身份。假設有一個函式要把訊息寫到某個目的地；它不需要知道目的地是檔案、記憶體 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a>，還是網路連線，只需要知道對方能 &lt;code>Write&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">Writer&lt;/span> &lt;span class="kd">interface&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nf">Write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">p&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">n&lt;/span> &lt;span class="kt">int&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="kt">error&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">WriteMessage&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span> &lt;span class="nx">Writer&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">message&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">error&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">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Write&lt;/span>&lt;span class="p">([]&lt;/span>&lt;span class="nb">byte&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">message&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&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">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>這個 interface 很小，只描述一個行為：寫入 bytes。&lt;/p>
&lt;h2 id="判讀go-interface-是由使用者定義的需求">【判讀】Go interface 是由使用者定義的需求&lt;/h2>
&lt;p>在 Go 裡，interface 常由「使用者」定義，而不是由「實作者」定義。&lt;/p>
&lt;p>這和很多語言不同。你不需要在某個型別上寫：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// Go 不需要這種宣告&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">type&lt;/span> &lt;span class="nx">File&lt;/span> &lt;span class="nx">implements&lt;/span> &lt;span class="nx">Writer&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>implicit interface 的核心規則是：只要型別有相同方法，就符合 interface，不需要顯式宣告實作關係。&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">MemoryWriter&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">data&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">byte&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">m&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">MemoryWriter&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">p&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="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">6&lt;/span>&lt;span class="cl"> &lt;span class="nx">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="o">...&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">p&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>MemoryWriter&lt;/code> 沒有提到 &lt;code>Writer&lt;/code>，但它已經符合 &lt;code>Writer&lt;/code>。&lt;/p>
&lt;h2 id="策略interface-越小依賴越清楚">【策略】interface 越小，依賴越清楚&lt;/h2>
&lt;p>小 interface 的核心規則是：interface 應由使用端需要的最小行為組成。Go 常見的好 interface 很小：&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">Reader&lt;/span> &lt;span class="kd">interface&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nf">Read&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">p&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">n&lt;/span> &lt;span class="kt">int&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="kt">error&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">Writer&lt;/span> &lt;span class="kd">interface&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nf">Write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">p&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">n&lt;/span> &lt;span class="kt">int&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&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"> 7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">Closer&lt;/span> &lt;span class="kd">interface&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nf">Close&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="kt">error&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>小 interface 的好處是：&lt;/p>
&lt;ul>
&lt;li>呼叫者只依賴自己真正需要的行為&lt;/li>
&lt;li>測試替身容易寫&lt;/li>
&lt;li>concrete type 可以在不同情境中被重用&lt;/li>
&lt;li>未來改內部結構時，外部影響較小&lt;/li>
&lt;/ul>
&lt;p>反例是把太多方法塞進一個 interface：&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">UserService&lt;/span> &lt;span class="kd">interface&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nf">CreateUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">error&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nf">UpdateUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">error&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nf">DeleteUser&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>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nf">FindUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">error&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nf">ListUsers&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">([]&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">error&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="nf">ExportUsers&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">([]&lt;/span>&lt;span class="kt">byte&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">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>如果某個函式只需要查詢 user，卻依賴整個 &lt;code>UserService&lt;/code>，它就知道太多了。&lt;/p>
&lt;h2 id="執行為查詢需求設計小介面">【執行】為查詢需求設計小介面&lt;/h2>
&lt;p>依賴邊界的核心規則是：使用端只依賴自己需要的方法。假設一個 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">type&lt;/span> &lt;span class="nx">UserLookup&lt;/span> &lt;span class="kd">interface&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nf">FindName&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">userID&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">bool&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">Handler&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">users&lt;/span> &lt;span class="nx">UserLookup&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">NewHandler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">users&lt;/span> &lt;span class="nx">UserLookup&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">*&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">10&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">Handler&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">users&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">users&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 handler 不知道 user 是存在 map、資料庫、檔案，還是測試假物件裡。它只知道自己需要 &lt;code>FindName&lt;/code>。&lt;/p>
&lt;p>測試時可以寫一個很小的 fake：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">fakeUsers&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>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">f&lt;/span> &lt;span class="nx">fakeUsers&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">FindName&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">userID&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">bool&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ok&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">f&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">userID&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">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ok&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">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>這就是 Go interface 最實用的地方：它讓依賴變小，讓測試變簡單。&lt;/p>
&lt;h2 id="何時先保留-concrete-type">何時先保留 concrete type&lt;/h2>
&lt;p>interface 的使用邊界是：替換需求或測試替身需求清楚時，再抽出小介面。以下情境通常適合先保留 concrete type：&lt;/p>
&lt;ul>
&lt;li>只有一個 concrete type，而且沒有測試替身需求&lt;/li>
&lt;li>interface 只是完整複製 concrete type 的所有方法&lt;/li>
&lt;li>你還不確定呼叫者真正需要哪些行為&lt;/li>
&lt;/ul>
&lt;p>Go 的常見做法是：先寫 concrete type，等使用端出現明確需求，再抽小 interface。&lt;/p></description><content:encoded><![CDATA[<p>Go 的 interface 描述的是行為，不是繼承關係。你不需要在 concrete type 上宣告「我實作了某個 interface」；只要方法集合符合，Go 就視為實作。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>理解 implicit interface 的設計精神</li>
<li>寫出小而精準的 interface</li>
<li>避免把 concrete type 暴露給不需要的呼叫者</li>
<li>用 interface 改善測試與依賴邊界</li>
</ol>
<hr>
<h2 id="觀察interface-只描述需要的行為">【觀察】interface 只描述需要的行為</h2>
<p>interface 的核心規則是：只描述呼叫者需要的行為，不描述實作者的完整身份。假設有一個函式要把訊息寫到某個目的地；它不需要知道目的地是檔案、記憶體 <a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a>，還是網路連線，只需要知道對方能 <code>Write</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">Writer</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">Write</span><span class="p">(</span><span class="nx">p</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">(</span><span class="nx">n</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">err</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">func</span> <span class="nf">WriteMessage</span><span class="p">(</span><span class="nx">w</span> <span class="nx">Writer</span><span class="p">,</span> <span class="nx">message</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">6</span><span class="cl">    <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">w</span><span class="p">.</span><span class="nf">Write</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">message</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個 interface 很小，只描述一個行為：寫入 bytes。</p>
<h2 id="判讀go-interface-是由使用者定義的需求">【判讀】Go interface 是由使用者定義的需求</h2>
<p>在 Go 裡，interface 常由「使用者」定義，而不是由「實作者」定義。</p>
<p>這和很多語言不同。你不需要在某個型別上寫：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// Go 不需要這種宣告</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kd">type</span> <span class="nx">File</span> <span class="nx">implements</span> <span class="nx">Writer</span></span></span></code></pre></div><p>implicit interface 的核心規則是：只要型別有相同方法，就符合 interface，不需要顯式宣告實作關係。</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">MemoryWriter</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">data</span> <span class="p">[]</span><span class="kt">byte</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="o">*</span><span class="nx">MemoryWriter</span><span class="p">)</span> <span class="nf">Write</span><span class="p">(</span><span class="nx">p</span> <span class="p">[]</span><span class="kt">byte</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">6</span><span class="cl">    <span class="nx">m</span><span class="p">.</span><span class="nx">data</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">m</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span> <span class="nx">p</span><span class="o">...</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="k">return</span> <span class="nb">len</span><span class="p">(</span><span class="nx">p</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>MemoryWriter</code> 沒有提到 <code>Writer</code>，但它已經符合 <code>Writer</code>。</p>
<h2 id="策略interface-越小依賴越清楚">【策略】interface 越小，依賴越清楚</h2>
<p>小 interface 的核心規則是：interface 應由使用端需要的最小行為組成。Go 常見的好 interface 很小：</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">Reader</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">Read</span><span class="p">(</span><span class="nx">p</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">(</span><span class="nx">n</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">err</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">Writer</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nf">Write</span><span class="p">(</span><span class="nx">p</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">(</span><span class="nx">n</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">err</span> <span class="kt">error</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="kd">type</span> <span class="nx">Closer</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nf">Close</span><span class="p">()</span> <span class="kt">error</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>小 interface 的好處是：</p>
<ul>
<li>呼叫者只依賴自己真正需要的行為</li>
<li>測試替身容易寫</li>
<li>concrete type 可以在不同情境中被重用</li>
<li>未來改內部結構時，外部影響較小</li>
</ul>
<p>反例是把太多方法塞進一個 interface：</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">UserService</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">User</span><span class="p">)</span> <span class="kt">error</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nf">UpdateUser</span><span class="p">(</span><span class="nx">User</span><span class="p">)</span> <span class="kt">error</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nf">DeleteUser</span><span class="p">(</span><span class="kt">string</span><span class="p">)</span> <span class="kt">error</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nf">FindUser</span><span class="p">(</span><span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nf">ListUsers</span><span class="p">()</span> <span class="p">([]</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nf">ExportUsers</span><span class="p">()</span> <span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>如果某個函式只需要查詢 user，卻依賴整個 <code>UserService</code>，它就知道太多了。</p>
<h2 id="執行為查詢需求設計小介面">【執行】為查詢需求設計小介面</h2>
<p>依賴邊界的核心規則是：使用端只依賴自己需要的方法。假設一個 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">type</span> <span class="nx">UserLookup</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">FindName</span><span class="p">(</span><span class="nx">userID</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">bool</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">Handler</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">users</span> <span class="nx">UserLookup</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="nf">NewHandler</span><span class="p">(</span><span class="nx">users</span> <span class="nx">UserLookup</span><span class="p">)</span> <span class="o">*</span><span class="nx">Handler</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="o">&amp;</span><span class="nx">Handler</span><span class="p">{</span><span class="nx">users</span><span class="p">:</span> <span class="nx">users</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個 handler 不知道 user 是存在 map、資料庫、檔案，還是測試假物件裡。它只知道自己需要 <code>FindName</code>。</p>
<p>測試時可以寫一個很小的 fake：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">type</span> <span class="nx">fakeUsers</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">f</span> <span class="nx">fakeUsers</span><span class="p">)</span> <span class="nf">FindName</span><span class="p">(</span><span class="nx">userID</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">bool</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">name</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">f</span><span class="p">[</span><span class="nx">userID</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">name</span><span class="p">,</span> <span class="nx">ok</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這就是 Go interface 最實用的地方：它讓依賴變小，讓測試變簡單。</p>
<h2 id="何時先保留-concrete-type">何時先保留 concrete type</h2>
<p>interface 的使用邊界是：替換需求或測試替身需求清楚時，再抽出小介面。以下情境通常適合先保留 concrete type：</p>
<ul>
<li>只有一個 concrete type，而且沒有測試替身需求</li>
<li>interface 只是完整複製 concrete type 的所有方法</li>
<li>你還不確定呼叫者真正需要哪些行為</li>
</ul>
<p>Go 的常見做法是：先寫 concrete type，等使用端出現明確需求，再抽小 interface。</p>
]]></content:encoded></item><item><title>2.4 常數與 typed string</title><link>https://tarrragon.github.io/blog/go/02-types-data/constants/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/02-types-data/constants/</guid><description>&lt;p>常數讓程式中的固定值有名稱。typed string 則讓一組字串值形成語意邊界，避免任意字串到處流動。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>用 &lt;code>const&lt;/code> 定義固定值&lt;/li>
&lt;li>理解 untyped constant 與 typed constant 的差異&lt;/li>
&lt;li>用 typed string 表達狀態與事件類型&lt;/li>
&lt;li>集中管理協定字串與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a> message&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察字串散落會增加維護成本">【觀察】字串散落會增加維護成本&lt;/h2>
&lt;p>協定字串的核心問題是：同一個語意若以裸字串散落在程式中，拼字、修改與合法值判斷都會變成隱性成本。以下是裸字串散落的典型樣子：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="nx">status&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s">&amp;#34;active&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="c1">// ...&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="k">if&lt;/span> &lt;span class="nx">eventType&lt;/span> &lt;span class="o">==&lt;/span> &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">6&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ...&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">log&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 created&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;ul>
&lt;li>拼字錯誤不容易被發現&lt;/li>
&lt;li>修改字串時要全專案搜尋&lt;/li>
&lt;li>無法從型別看出哪些值是合法的&lt;/li>
&lt;li>不同概念可能共用同一種 &lt;code>string&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>常數可以先解決命名與集中管理問題。&lt;/p>
&lt;h2 id="判讀const-是把意圖寫進名稱">【判讀】const 是把意圖寫進名稱&lt;/h2>
&lt;p>&lt;code>const&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">const&lt;/span> &lt;span class="nx">DefaultPort&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">8080&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="kd">const&lt;/span> &lt;span class="nx">EventUserCreated&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;user.created&amp;#34;&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="k">if&lt;/span> &lt;span class="nx">eventType&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="nx">EventUserCreated&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="c1">// ...&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>&amp;quot;user.created&amp;quot;&lt;/code> 更清楚，因為名稱說明了這個字串在系統中的角色。&lt;/p>
&lt;h2 id="策略用-typed-string-區分概念">【策略】用 typed string 區分概念&lt;/h2>
&lt;p>typed string 的核心用途是用型別區分不同語意的字串。當多組資料底層都是字串，但語意不同，可以定義不同型別：&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">TaskStatus&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="kd">const&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">TaskStatusPending&lt;/span> &lt;span class="nx">TaskStatus&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;pending&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="nx">TaskStatusRunning&lt;/span> &lt;span class="nx">TaskStatus&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;running&amp;#34;&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">TaskStatusDone&lt;/span> &lt;span class="nx">TaskStatus&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;done&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">TaskStatusFailed&lt;/span> &lt;span class="nx">TaskStatus&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;failed&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;/code>&lt;/pre>&lt;/div>&lt;p>函式簽名可以明確要求 &lt;code>TaskStatus&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">CanRetry&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">status&lt;/span> &lt;span class="nx">TaskStatus&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">status&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="nx">TaskStatusFailed&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>這不會讓 Go 變成 enum 語言，但能讓 API 更清楚。讀者看到 &lt;code>TaskStatus&lt;/code>，就知道這不是任意字串。&lt;/p>
&lt;h2 id="執行事件類型與-action-常數">【執行】事件類型與 action 常數&lt;/h2>
&lt;p>事件類型的核心規則是：同一組事件值應集中在同一個 typed string 型別下。事件驅動程式常需要管理事件類型：&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">EventType&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="kd">const&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">EventUserCreated&lt;/span> &lt;span class="nx">EventType&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;user.created&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="nx">EventUserUpdated&lt;/span> &lt;span class="nx">EventType&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;user.updated&amp;#34;&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">EventUserDeleted&lt;/span> &lt;span class="nx">EventType&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;user.deleted&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>API action 也可以用同樣方式：&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">Action&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="kd">const&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">ActionSubscribe&lt;/span> &lt;span class="nx">Action&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;subscribe&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="nx">ActionUnsubscribe&lt;/span> &lt;span class="nx">Action&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;unsubscribe&amp;#34;&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">ActionPing&lt;/span> &lt;span class="nx">Action&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;ping&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>處理時，switch 會變得可讀：&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">HandleAction&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">action&lt;/span> &lt;span class="nx">Action&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">switch&lt;/span> &lt;span class="nx">action&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="nx">ActionSubscribe&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="nx">ActionUnsubscribe&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="nx">ActionPing&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&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">default&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">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;unknown action: %s&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">action&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="log-message-也適合集中">log message 也適合集中&lt;/h2>
&lt;p>log message 的核心規則是：會被 grep、監控或文件引用的訊息應保持穩定。這類 message 可以用常數集中：&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">const&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">LogServerStarted&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;server started&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">LogEventDropped&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;event dropped&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">LogInvalidAction&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;invalid action&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>這樣做的價值是讓 log 訊號穩定。當 log 是除錯入口時，穩定字串就是系統 contract 的一部分。&lt;/p>
&lt;h2 id="常見取捨">常見取捨&lt;/h2>
&lt;h3 id="不必把所有字串都變常數">不必把所有字串都變常數&lt;/h3>
&lt;p>只出現一次、沒有協定意義、不需要搜尋的文字，可以直接寫在原處。過度常數化會讓讀者一直跳檔案。&lt;/p>
&lt;h3 id="常數名稱要說明清楚概念">常數名稱要說明清楚概念&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="kd">const&lt;/span> &lt;span class="nx">StringActive&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;active&amp;#34;&lt;/span> &lt;span class="c1">// 不佳&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>比較好的名稱要說明概念：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">const&lt;/span> &lt;span class="nx">TaskStatusActive&lt;/span> &lt;span class="nx">TaskStatus&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;active&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description><content:encoded><![CDATA[<p>常數讓程式中的固定值有名稱。typed string 則讓一組字串值形成語意邊界，避免任意字串到處流動。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>用 <code>const</code> 定義固定值</li>
<li>理解 untyped constant 與 typed constant 的差異</li>
<li>用 typed string 表達狀態與事件類型</li>
<li>集中管理協定字串與 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> message</li>
</ol>
<hr>
<h2 id="觀察字串散落會增加維護成本">【觀察】字串散落會增加維護成本</h2>
<p>協定字串的核心問題是：同一個語意若以裸字串散落在程式中，拼字、修改與合法值判斷都會變成隱性成本。以下是裸字串散落的典型樣子：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">if</span> <span class="nx">status</span> <span class="o">==</span> <span class="s">&#34;active&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="c1">// ...</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="k">if</span> <span class="nx">eventType</span> <span class="o">==</span> <span class="s">&#34;user.created&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="c1">// ...</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">log</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;user created&#34;</span><span class="p">)</span></span></span></code></pre></div><p>短期看起來很直接，但問題是：</p>
<ul>
<li>拼字錯誤不容易被發現</li>
<li>修改字串時要全專案搜尋</li>
<li>無法從型別看出哪些值是合法的</li>
<li>不同概念可能共用同一種 <code>string</code></li>
</ul>
<p>常數可以先解決命名與集中管理問題。</p>
<h2 id="判讀const-是把意圖寫進名稱">【判讀】const 是把意圖寫進名稱</h2>
<p><code>const</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">const</span> <span class="nx">DefaultPort</span> <span class="p">=</span> <span class="mi">8080</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kd">const</span> <span class="nx">EventUserCreated</span> <span class="p">=</span> <span class="s">&#34;user.created&#34;</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="k">if</span> <span class="nx">eventType</span> <span class="o">==</span> <span class="nx">EventUserCreated</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="c1">// ...</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>&quot;user.created&quot;</code> 更清楚，因為名稱說明了這個字串在系統中的角色。</p>
<h2 id="策略用-typed-string-區分概念">【策略】用 typed string 區分概念</h2>
<p>typed string 的核心用途是用型別區分不同語意的字串。當多組資料底層都是字串，但語意不同，可以定義不同型別：</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">TaskStatus</span> <span class="kt">string</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kd">const</span> <span class="p">(</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">TaskStatusPending</span> <span class="nx">TaskStatus</span> <span class="p">=</span> <span class="s">&#34;pending&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nx">TaskStatusRunning</span> <span class="nx">TaskStatus</span> <span class="p">=</span> <span class="s">&#34;running&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">TaskStatusDone</span>    <span class="nx">TaskStatus</span> <span class="p">=</span> <span class="s">&#34;done&#34;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nx">TaskStatusFailed</span>  <span class="nx">TaskStatus</span> <span class="p">=</span> <span class="s">&#34;failed&#34;</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>TaskStatus</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">CanRetry</span><span class="p">(</span><span class="nx">status</span> <span class="nx">TaskStatus</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">status</span> <span class="o">==</span> <span class="nx">TaskStatusFailed</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這不會讓 Go 變成 enum 語言，但能讓 API 更清楚。讀者看到 <code>TaskStatus</code>，就知道這不是任意字串。</p>
<h2 id="執行事件類型與-action-常數">【執行】事件類型與 action 常數</h2>
<p>事件類型的核心規則是：同一組事件值應集中在同一個 typed string 型別下。事件驅動程式常需要管理事件類型：</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">EventType</span> <span class="kt">string</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kd">const</span> <span class="p">(</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">EventUserCreated</span> <span class="nx">EventType</span> <span class="p">=</span> <span class="s">&#34;user.created&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nx">EventUserUpdated</span> <span class="nx">EventType</span> <span class="p">=</span> <span class="s">&#34;user.updated&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">EventUserDeleted</span> <span class="nx">EventType</span> <span class="p">=</span> <span class="s">&#34;user.deleted&#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>API action 也可以用同樣方式：</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">Action</span> <span class="kt">string</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kd">const</span> <span class="p">(</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">ActionSubscribe</span>   <span class="nx">Action</span> <span class="p">=</span> <span class="s">&#34;subscribe&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nx">ActionUnsubscribe</span> <span class="nx">Action</span> <span class="p">=</span> <span class="s">&#34;unsubscribe&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">ActionPing</span>        <span class="nx">Action</span> <span class="p">=</span> <span class="s">&#34;ping&#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>處理時，switch 會變得可讀：</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">HandleAction</span><span class="p">(</span><span class="nx">action</span> <span class="nx">Action</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">switch</span> <span class="nx">action</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">case</span> <span class="nx">ActionSubscribe</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">case</span> <span class="nx">ActionUnsubscribe</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">case</span> <span class="nx">ActionPing</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;unknown action: %s&#34;</span><span class="p">,</span> <span class="nx">action</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h2 id="log-message-也適合集中">log message 也適合集中</h2>
<p>log message 的核心規則是：會被 grep、監控或文件引用的訊息應保持穩定。這類 message 可以用常數集中：</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">const</span> <span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">LogServerStarted</span> <span class="p">=</span> <span class="s">&#34;server started&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">LogEventDropped</span>  <span class="p">=</span> <span class="s">&#34;event dropped&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">LogInvalidAction</span> <span class="p">=</span> <span class="s">&#34;invalid action&#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>這樣做的價值是讓 log 訊號穩定。當 log 是除錯入口時，穩定字串就是系統 contract 的一部分。</p>
<h2 id="常見取捨">常見取捨</h2>
<h3 id="不必把所有字串都變常數">不必把所有字串都變常數</h3>
<p>只出現一次、沒有協定意義、不需要搜尋的文字，可以直接寫在原處。過度常數化會讓讀者一直跳檔案。</p>
<h3 id="常數名稱要說明清楚概念">常數名稱要說明清楚概念</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="kd">const</span> <span class="nx">StringActive</span> <span class="p">=</span> <span class="s">&#34;active&#34;</span> <span class="c1">// 不佳</span></span></span></code></pre></div><p>比較好的名稱要說明概念：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">const</span> <span class="nx">TaskStatusActive</span> <span class="nx">TaskStatus</span> <span class="p">=</span> <span class="s">&#34;active&#34;</span></span></span></code></pre></div>]]></content:encoded></item><item><title>2.5 指標與資料複製邊界</title><link>https://tarrragon.github.io/blog/go/02-types-data/pointers-copy/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/02-types-data/pointers-copy/</guid><description>&lt;p>Go 的指標讓函式可以操作原本的資料，而不是資料複本。這很有效率，也很危險：當多個地方共享同一份資料時，你需要明確決定誰可以修改，誰只能讀取。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>理解值傳遞與指標傳遞的差異&lt;/li>
&lt;li>判斷何時使用 pointer&lt;/li>
&lt;li>理解 slice、map 本身已經帶有共享底層資料的特性&lt;/li>
&lt;li>用 copy 保護資料邊界&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察go-預設是值傳遞">【觀察】Go 預設是值傳遞&lt;/h2>
&lt;p>值傳遞的核心規則是：函式收到的是參數值的複本，修改複本不會改到呼叫端原值。以下範例中，&lt;code>Rename&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">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">Name&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="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">Rename&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">u&lt;/span> &lt;span class="nx">User&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">u&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;Bob&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;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="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">10&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">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">11&lt;/span>&lt;span class="cl"> &lt;span class="nf">Rename&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">user&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">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="nx">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Name&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// Alice&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>Rename&lt;/code> 修改的是複本，不是 &lt;code>main&lt;/code> 裡的 &lt;code>user&lt;/code>。&lt;/p>
&lt;p>指標傳遞的核心規則是：函式收到原值位址，因此可以修改呼叫端原值。如果想修改原本的值，就要傳指標：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">Rename&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">u&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">u&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;Bob&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">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">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">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 class="nf">Rename&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">user&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> &lt;span class="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">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Name&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// Bob&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>&amp;amp;user&lt;/code> 取得位址，&lt;code>*User&lt;/code> 表示指向 &lt;code>User&lt;/code> 的指標。&lt;/p>
&lt;h2 id="判讀pointer-表示共享修改權">【判讀】pointer 表示共享修改權&lt;/h2>
&lt;p>pointer 的核心語意是共享修改權，不只是效能工具。它表示被呼叫者可能看到或修改原本那份資料。&lt;/p>
&lt;p>適合使用 pointer 的情境：&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>方法需要修改 receiver&lt;/td>
 &lt;td>例如 &lt;code>Counter.Inc()&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>struct 很大，複製成本高&lt;/td>
 &lt;td>避免每次呼叫都複製大量資料&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要表示 optional object&lt;/td>
 &lt;td>&lt;code>nil&lt;/code> 可表示不存在&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多個方法共享同一份狀態&lt;/td>
 &lt;td>例如 repository、server、cache&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>不適合濫用 pointer 的情境：&lt;/p>
&lt;ul>
&lt;li>小型不可變資料，例如 &lt;code>time.Time&lt;/code> 常直接值傳遞&lt;/li>
&lt;li>只是為了「看起來像物件導向」&lt;/li>
&lt;li>不希望呼叫者能修改內部資料&lt;/li>
&lt;/ul>
&lt;h2 id="策略slice-和-map-要特別小心">【策略】slice 和 map 要特別小心&lt;/h2>
&lt;p>slice 和 map 的核心風險是：即使參數不是 pointer，也會共享底層資料。&lt;/p>
&lt;h3 id="slice-共享底層陣列">slice 共享底層陣列&lt;/h3>
&lt;p>slice 參數會複製 slice header，但 header 指向同一個底層 array；因此函式內修改元素，外面會看見。&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">Modify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">items&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="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">items&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;changed&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">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">names&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s">&amp;#34;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>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="nf">Modify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">names&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="nx">names&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="c1">// changed&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;h3 id="map-本身就是-reference-like">map 本身就是 reference-like&lt;/h3>
&lt;p>map 傳入函式後，函式可以修改同一份 map。這是很多共享狀態 bug 的來源。&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">Modify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">m&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">int&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">m&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">&amp;#34;count&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">10&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">values&lt;/span> &lt;span class="o">:=&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">int&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s">&amp;#34;count&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="nf">Modify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">values&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="nx">values&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">&amp;#34;count&amp;#34;&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="c1">// 10&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;h2 id="執行回傳資料時建立-copy-邊界">【執行】回傳資料時建立 copy 邊界&lt;/h2>
&lt;p>copy 邊界的核心規則是：不希望外部修改內部狀態時，不要直接回傳內部 map、slice 或 pointer。假設 &lt;code>UserRepository&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">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>&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="kd">type&lt;/span> &lt;span class="nx">UserRepository&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="nx">users&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">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>直接回傳 map 會把內部狀態暴露給呼叫者：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">UserRepository&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Users&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">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>UserRepository&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">users&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">repo&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Users&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nx">users&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">&amp;#34;1&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">User&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">ID&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s">&amp;#34;1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">Name&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s">&amp;#34;Changed&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="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">UserRepository&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Users&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">result&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">map&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">id&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">user&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">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">result&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>回傳 slice 時也要複製 slice：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">UserRepository&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">ListUsers&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="nx">User&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">result&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">([]&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">users&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">result&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">result&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">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">result&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>這樣呼叫者可以自由排序、append 或修改回傳資料，不會影響 repository 內部狀態。&lt;/p>
&lt;h2 id="深層複製與淺層複製">深層複製與淺層複製&lt;/h2>
&lt;p>深層複製的核心規則是：struct 裡面若含 slice、map 或 pointer，只複製 struct 本身仍會共享內部資料。以下 &lt;code>Profile&lt;/code> 包含 slice：&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">Profile&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>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">Tags&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">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;/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">copyProfile&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">profile&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>copyProfile.Tags&lt;/code> 和 &lt;code>profile.Tags&lt;/code> 仍然指向同一個底層 array。若要保護邊界，需要複製 slice：&lt;/p></description><content:encoded><![CDATA[<p>Go 的指標讓函式可以操作原本的資料，而不是資料複本。這很有效率，也很危險：當多個地方共享同一份資料時，你需要明確決定誰可以修改，誰只能讀取。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>理解值傳遞與指標傳遞的差異</li>
<li>判斷何時使用 pointer</li>
<li>理解 slice、map 本身已經帶有共享底層資料的特性</li>
<li>用 copy 保護資料邊界</li>
</ol>
<hr>
<h2 id="觀察go-預設是值傳遞">【觀察】Go 預設是值傳遞</h2>
<p>值傳遞的核心規則是：函式收到的是參數值的複本，修改複本不會改到呼叫端原值。以下範例中，<code>Rename</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">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">Name</span> <span class="kt">string</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">Rename</span><span class="p">(</span><span class="nx">u</span> <span class="nx">User</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">u</span><span class="p">.</span><span class="nx">Name</span> <span class="p">=</span> <span class="s">&#34;Bob&#34;</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="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">user</span> <span class="o">:=</span> <span class="nx">User</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">11</span><span class="cl">    <span class="nf">Rename</span><span class="p">(</span><span class="nx">user</span><span class="p">)</span>
</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="nx">user</span><span class="p">.</span><span class="nx">Name</span><span class="p">)</span> <span class="c1">// Alice</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>Rename</code> 修改的是複本，不是 <code>main</code> 裡的 <code>user</code>。</p>
<p>指標傳遞的核心規則是：函式收到原值位址，因此可以修改呼叫端原值。如果想修改原本的值，就要傳指標：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">Rename</span><span class="p">(</span><span class="nx">u</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">u</span><span class="p">.</span><span class="nx">Name</span> <span class="p">=</span> <span class="s">&#34;Bob&#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">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">user</span> <span class="o">:=</span> <span class="nx">User</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 class="nf">Rename</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">user</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">Name</span><span class="p">)</span> <span class="c1">// Bob</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>&amp;user</code> 取得位址，<code>*User</code> 表示指向 <code>User</code> 的指標。</p>
<h2 id="判讀pointer-表示共享修改權">【判讀】pointer 表示共享修改權</h2>
<p>pointer 的核心語意是共享修改權，不只是效能工具。它表示被呼叫者可能看到或修改原本那份資料。</p>
<p>適合使用 pointer 的情境：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>方法需要修改 receiver</td>
          <td>例如 <code>Counter.Inc()</code></td>
      </tr>
      <tr>
          <td>struct 很大，複製成本高</td>
          <td>避免每次呼叫都複製大量資料</td>
      </tr>
      <tr>
          <td>需要表示 optional object</td>
          <td><code>nil</code> 可表示不存在</td>
      </tr>
      <tr>
          <td>多個方法共享同一份狀態</td>
          <td>例如 repository、server、cache</td>
      </tr>
  </tbody>
</table>
<p>不適合濫用 pointer 的情境：</p>
<ul>
<li>小型不可變資料，例如 <code>time.Time</code> 常直接值傳遞</li>
<li>只是為了「看起來像物件導向」</li>
<li>不希望呼叫者能修改內部資料</li>
</ul>
<h2 id="策略slice-和-map-要特別小心">【策略】slice 和 map 要特別小心</h2>
<p>slice 和 map 的核心風險是：即使參數不是 pointer，也會共享底層資料。</p>
<h3 id="slice-共享底層陣列">slice 共享底層陣列</h3>
<p>slice 參數會複製 slice header，但 header 指向同一個底層 array；因此函式內修改元素，外面會看見。</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">Modify</span><span class="p">(</span><span class="nx">items</span> <span class="p">[]</span><span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">items</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="p">=</span> <span class="s">&#34;changed&#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">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">names</span> <span class="o">:=</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></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nf">Modify</span><span class="p">(</span><span class="nx">names</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="nx">names</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="c1">// changed</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="map-本身就是-reference-like">map 本身就是 reference-like</h3>
<p>map 傳入函式後，函式可以修改同一份 map。這是很多共享狀態 bug 的來源。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">Modify</span><span class="p">(</span><span class="nx">m</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">int</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">m</span><span class="p">[</span><span class="s">&#34;count&#34;</span><span class="p">]</span> <span class="p">=</span> <span class="mi">10</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">values</span> <span class="o">:=</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">int</span><span class="p">{</span><span class="s">&#34;count&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nf">Modify</span><span class="p">(</span><span class="nx">values</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="nx">values</span><span class="p">[</span><span class="s">&#34;count&#34;</span><span class="p">])</span> <span class="c1">// 10</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h2 id="執行回傳資料時建立-copy-邊界">【執行】回傳資料時建立 copy 邊界</h2>
<p>copy 邊界的核心規則是：不希望外部修改內部狀態時，不要直接回傳內部 map、slice 或 pointer。假設 <code>UserRepository</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">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></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="kd">type</span> <span class="nx">UserRepository</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nx">users</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>直接回傳 map 會把內部狀態暴露給呼叫者：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">Users</span><span class="p">()</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">return</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>呼叫者就可以繞過 <code>UserRepository</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">users</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">Users</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">users</span><span class="p">[</span><span class="s">&#34;1&#34;</span><span class="p">]</span> <span class="p">=</span> <span class="nx">User</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;1&#34;</span><span class="p">,</span> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;Changed&#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="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">Users</span><span class="p">()</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">result</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">User</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">for</span> <span class="nx">id</span><span class="p">,</span> <span class="nx">user</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="nx">result</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span> <span class="p">=</span> <span class="nx">user</span>
</span></span><span class="line"><span class="ln">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">result</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>回傳 slice 時也要複製 slice：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">UserRepository</span><span class="p">)</span> <span class="nf">ListUsers</span><span class="p">()</span> <span class="p">[]</span><span class="nx">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">result</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="nx">User</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">users</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">user</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">r</span><span class="p">.</span><span class="nx">users</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="nx">result</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">result</span><span class="p">,</span> <span class="nx">user</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">result</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這樣呼叫者可以自由排序、append 或修改回傳資料，不會影響 repository 內部狀態。</p>
<h2 id="深層複製與淺層複製">深層複製與淺層複製</h2>
<p>深層複製的核心規則是：struct 裡面若含 slice、map 或 pointer，只複製 struct 本身仍會共享內部資料。以下 <code>Profile</code> 包含 slice：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">type</span> <span class="nx">Profile</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></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">Tags</span> <span class="p">[]</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></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">copyProfile</span> <span class="o">:=</span> <span class="nx">profile</span></span></span></code></pre></div><p><code>copyProfile.Tags</code> 和 <code>profile.Tags</code> 仍然指向同一個底層 array。若要保護邊界，需要複製 slice：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">CloneProfile</span><span class="p">(</span><span class="nx">p</span> <span class="nx">Profile</span><span class="p">)</span> <span class="nx">Profile</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">p</span><span class="p">.</span><span class="nx">Tags</span> <span class="p">=</span> <span class="nb">append</span><span class="p">([]</span><span class="nb">string</span><span class="p">(</span><span class="kc">nil</span><span class="p">),</span> <span class="nx">p</span><span class="p">.</span><span class="nx">Tags</span><span class="o">...</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">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>這種 copy 邊界在共享狀態、快取、API response、測試資料中都很重要。</p>
]]></content:encoded></item><item><title>2.6 struct embedding 與組合式設計</title><link>https://tarrragon.github.io/blog/go/02-types-data/embedding-composition/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/02-types-data/embedding-composition/</guid><description>&lt;p>struct embedding 的核心用途是組合既有能力。它可以讓欄位與方法被提升到外層型別，但設計重點仍然是清楚表達責任，而不是模擬繼承階層。&lt;/p>
&lt;h2 id="預計補充內容">預計補充內容&lt;/h2>
&lt;p>這些組合邊界會在下列章節展開：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/00-philosophy/composition/" data-link-title="0.2 組合優先：小介面與明確依賴" data-link-desc="用小介面與 struct 組合取代大型繼承結構">Go 入門：組合優先：小介面與明確依賴&lt;/a>：先理解 Go 為什麼偏好組合，才能判斷 embedding 是在表達能力，還是在模仿繼承。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/02-types-data/interfaces/" data-link-title="2.3 interface：用行為定義依賴" data-link-desc="用小介面描述元件需要的能力">Go 入門：interface：用行為定義依賴&lt;/a>：這裡會把 embedding 和 interface 的責任分開，避免欄位提升變成隱性耦合。&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>：當組合開始影響 wiring 時，就要看依賴是在哪一層被建立的。&lt;/li>
&lt;/ul>
&lt;h2 id="與其他章節的關係">與其他章節的關係&lt;/h2>
&lt;p>本章承接 &lt;a href="https://tarrragon.github.io/blog/go/00-philosophy/composition/" data-link-title="0.2 組合優先：小介面與明確依賴" data-link-desc="用小介面與 struct 組合取代大型繼承結構">組合優先：小介面與明確依賴&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/go/02-types-data/interfaces/" data-link-title="2.3 interface：用行為定義依賴" data-link-desc="用小介面描述元件需要的能力">interface：用行為定義依賴&lt;/a>，後續會連到 &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 留在應用入口層">composition root 與依賴組裝&lt;/a>。&lt;/p>
&lt;h2 id="和-go-教材的關係">和 Go 教材的關係&lt;/h2>
&lt;p>這一章承接的是組合、interface 與依賴組裝；如果你要先回看語言教材，可以讀：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/00-philosophy/composition/" data-link-title="0.2 組合優先：小介面與明確依賴" data-link-desc="用小介面與 struct 組合取代大型繼承結構">Go：組合優先：小介面與明確依賴&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/02-types-data/interfaces/" data-link-title="2.3 interface：用行為定義依賴" data-link-desc="用小介面描述元件需要的能力">Go：interface：用行為定義依賴&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/07-refactoring/domain-packages/" data-link-title="7.5 以 domain 重新整理 package" data-link-desc="讓 account、job、event、workflow 這類領域邊界在目錄中可見">Go：以 domain 重新整理 package&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>struct embedding 的核心用途是組合既有能力。它可以讓欄位與方法被提升到外層型別，但設計重點仍然是清楚表達責任，而不是模擬繼承階層。</p>
<h2 id="預計補充內容">預計補充內容</h2>
<p>這些組合邊界會在下列章節展開：</p>
<ul>
<li><a href="/blog/go/00-philosophy/composition/" data-link-title="0.2 組合優先：小介面與明確依賴" data-link-desc="用小介面與 struct 組合取代大型繼承結構">Go 入門：組合優先：小介面與明確依賴</a>：先理解 Go 為什麼偏好組合，才能判斷 embedding 是在表達能力，還是在模仿繼承。</li>
<li><a href="/blog/go/02-types-data/interfaces/" data-link-title="2.3 interface：用行為定義依賴" data-link-desc="用小介面描述元件需要的能力">Go 入門：interface：用行為定義依賴</a>：這裡會把 embedding 和 interface 的責任分開，避免欄位提升變成隱性耦合。</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>：當組合開始影響 wiring 時，就要看依賴是在哪一層被建立的。</li>
</ul>
<h2 id="與其他章節的關係">與其他章節的關係</h2>
<p>本章承接 <a href="/blog/go/00-philosophy/composition/" data-link-title="0.2 組合優先：小介面與明確依賴" data-link-desc="用小介面與 struct 組合取代大型繼承結構">組合優先：小介面與明確依賴</a> 與 <a href="/blog/go/02-types-data/interfaces/" data-link-title="2.3 interface：用行為定義依賴" data-link-desc="用小介面描述元件需要的能力">interface：用行為定義依賴</a>，後續會連到 <a href="/blog/go/07-refactoring/composition-root/" data-link-title="7.7 composition root 與依賴組裝" data-link-desc="把具體 adapter、config 與 usecase wiring 留在應用入口層">composition root 與依賴組裝</a>。</p>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是組合、interface 與依賴組裝；如果你要先回看語言教材，可以讀：</p>
<ul>
<li><a href="/blog/go/00-philosophy/composition/" data-link-title="0.2 組合優先：小介面與明確依賴" data-link-desc="用小介面與 struct 組合取代大型繼承結構">Go：組合優先：小介面與明確依賴</a></li>
<li><a href="/blog/go/02-types-data/interfaces/" data-link-title="2.3 interface：用行為定義依賴" data-link-desc="用小介面描述元件需要的能力">Go：interface：用行為定義依賴</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/07-refactoring/domain-packages/" data-link-title="7.5 以 domain 重新整理 package" data-link-desc="讓 account、job、event、workflow 這類領域邊界在目錄中可見">Go：以 domain 重新整理 package</a></li>
</ul>
]]></content:encoded></item><item><title>2.7 generics 入門：型別參數與約束</title><link>https://tarrragon.github.io/blog/go/02-types-data/generics-basics/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/02-types-data/generics-basics/</guid><description>&lt;p>generics 的核心用途是讓重複的型別安全邏輯可以被抽出來。Go 的泛型適合資料結構、集合 helper、測試工具與少量演算法；一般 application flow 仍應優先使用具體型別與小介面。&lt;/p>
&lt;h2 id="預計補充內容">預計補充內容&lt;/h2>
&lt;p>這些型別系統邊界會在下列章節展開：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/02-types-data/interfaces/" data-link-title="2.3 interface：用行為定義依賴" data-link-desc="用小介面描述元件需要的能力">Go 入門：interface：用行為定義依賴&lt;/a>：先看 interface 與具體型別的邊界，才能判斷什麼情況值得引入 generics。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/05-error-testing/table-driven-test/" data-link-title="5.3 table-driven test" data-link-desc="用表格整理多組輸入、預期輸出與錯誤情境">Go 入門：table-driven test&lt;/a>：泛型 helper 常常是給測試工具用的，這裡會看到它怎麼支撐重複案例。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go-advanced/03-runtime-profiling/allocation/" data-link-title="3.4 資料結構與 allocation 壓力" data-link-desc="分析列表、歷史資料與 WebSocket payload 的配置成本">Go 進階：資料結構與 allocation 壓力&lt;/a>：當泛型影響配置與熱路徑時，才需要往 runtime 成本那層看。&lt;/li>
&lt;/ul>
&lt;h2 id="與-backend-教材的分工">與 Backend 教材的分工&lt;/h2>
&lt;p>本章只處理 Go 型別系統。資料庫 row mapping、serialization schema 或外部 protocol code generation 會放在 Backend 或實戰章節中討論。&lt;/p>
&lt;h2 id="和-go-教材的關係">和 Go 教材的關係&lt;/h2>
&lt;p>這一章承接的是集合操作、型別約束與泛型應用；如果你要先回看語言教材，可以讀：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/02-types-data/slices-maps/" data-link-title="2.2 slice 與 map" data-link-desc="掌握 Go 最常用的集合型別：slice 與 map">Go：slice 與 map&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/02-types-data/interfaces/" data-link-title="2.3 interface：用行為定義依賴" data-link-desc="用小介面描述元件需要的能力">Go：interface：用行為定義依賴&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/02-types-data/pointers-copy/" data-link-title="2.5 指標與資料複製邊界" data-link-desc="理解指標、slice 與共享狀態的防護策略">Go：指標與資料複製邊界&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/go/07-refactoring/state-boundary/" data-link-title="7.4 狀態管理的安全邊界" data-link-desc="用 lock、copy 與 API 限制保護共享狀態">Go：狀態管理的安全邊界&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>generics 的核心用途是讓重複的型別安全邏輯可以被抽出來。Go 的泛型適合資料結構、集合 helper、測試工具與少量演算法；一般 application flow 仍應優先使用具體型別與小介面。</p>
<h2 id="預計補充內容">預計補充內容</h2>
<p>這些型別系統邊界會在下列章節展開：</p>
<ul>
<li><a href="/blog/go/02-types-data/interfaces/" data-link-title="2.3 interface：用行為定義依賴" data-link-desc="用小介面描述元件需要的能力">Go 入門：interface：用行為定義依賴</a>：先看 interface 與具體型別的邊界，才能判斷什麼情況值得引入 generics。</li>
<li><a href="/blog/go/05-error-testing/table-driven-test/" data-link-title="5.3 table-driven test" data-link-desc="用表格整理多組輸入、預期輸出與錯誤情境">Go 入門：table-driven test</a>：泛型 helper 常常是給測試工具用的，這裡會看到它怎麼支撐重複案例。</li>
<li><a href="/blog/go-advanced/03-runtime-profiling/allocation/" data-link-title="3.4 資料結構與 allocation 壓力" data-link-desc="分析列表、歷史資料與 WebSocket payload 的配置成本">Go 進階：資料結構與 allocation 壓力</a>：當泛型影響配置與熱路徑時，才需要往 runtime 成本那層看。</li>
</ul>
<h2 id="與-backend-教材的分工">與 Backend 教材的分工</h2>
<p>本章只處理 Go 型別系統。資料庫 row mapping、serialization schema 或外部 protocol code generation 會放在 Backend 或實戰章節中討論。</p>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是集合操作、型別約束與泛型應用；如果你要先回看語言教材，可以讀：</p>
<ul>
<li><a href="/blog/go/02-types-data/slices-maps/" data-link-title="2.2 slice 與 map" data-link-desc="掌握 Go 最常用的集合型別：slice 與 map">Go：slice 與 map</a></li>
<li><a href="/blog/go/02-types-data/interfaces/" data-link-title="2.3 interface：用行為定義依賴" data-link-desc="用小介面描述元件需要的能力">Go：interface：用行為定義依賴</a></li>
<li><a href="/blog/go/02-types-data/pointers-copy/" data-link-title="2.5 指標與資料複製邊界" data-link-desc="理解指標、slice 與共享狀態的防護策略">Go：指標與資料複製邊界</a></li>
<li><a href="/blog/go/07-refactoring/state-boundary/" data-link-title="7.4 狀態管理的安全邊界" data-link-desc="用 lock、copy 與 API 限制保護共享狀態">Go：狀態管理的安全邊界</a></li>
</ul>
]]></content:encoded></item></channel></rss>