<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[BulldogBytes]]></title><description><![CDATA[BulldogBytes]]></description><link>https://blog.ganhua.wang</link><generator>RSS for Node</generator><lastBuildDate>Mon, 11 May 2026 21:33:22 GMT</lastBuildDate><atom:link href="https://blog.ganhua.wang/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[工程師的 Claude Code 實戰指南：從零開始到高效開發]]></title><description><![CDATA[工程師的 Claude Code 實戰指南：從零開始到高效開發

本文整合 Anthropic 官方 Best Practices 與社群實戰 Tips，帶你由淺入深掌握 Claude Code。


什麼是 Claude Code？為什麼值得學？
如果你還在用「複製程式碼貼到 ChatGPT，再複製答案貼回去」的工作流程，Claude Code 會讓你大開眼界。
Claude Code 是 Anthropic 推出的命令列工具，它直接活在你的 terminal 裡，能夠讀懂你的整個 codeb...]]></description><link>https://blog.ganhua.wang/claude-code-1</link><guid isPermaLink="true">https://blog.ganhua.wang/claude-code-1</guid><category><![CDATA[claude-code]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Wed, 18 Feb 2026 14:42:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1771425737542/69581cea-5fee-4987-b3c7-34f07239bb7f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-claude-code">工程師的 Claude Code 實戰指南：從零開始到高效開發</h1>
<blockquote>
<p>本文整合 <a target="_blank" href="https://www.anthropic.com/engineering/claude-code-best-practices">Anthropic 官方 Best Practices</a> 與社群實戰 Tips，帶你由淺入深掌握 Claude Code。</p>
<h2 id="heading-"></h2>
</blockquote>
<h2 id="heading-claude-code-1">什麼是 Claude Code？為什麼值得學？</h2>
<p>如果你還在用「複製程式碼貼到 ChatGPT，再複製答案貼回去」的工作流程，Claude Code 會讓你大開眼界。</p>
<p>Claude Code 是 Anthropic 推出的命令列工具，它直接活在你的 terminal 裡，能夠讀懂你的整個 codebase、寫入檔案、執行指令、操作 git，甚至幫你開 PR。它不只是個「提示框」，而是一個能主動採取行動的 AI 代理（agentic coding assistant）。</p>
<p>用一句話形容：<strong>你告訴它要做什麼，它去搞定</strong>。</p>
<p>很多從 Cursor、GitHub Copilot 轉過來的工程師都說，用過 Claude Code 之後回不去了。原因不是它比較聰明，而是它的工作方式根本不同——它在你的環境裡工作，而不是你把東西帶去它的環境。</p>
<hr />
<h2 id="heading-56ys5lia5q2l77ya5a6j6kod6iih5z65pys5zwf5yuv">第一步：安裝與基本啟動</h2>
<p>安裝 Claude Code 只需要一行：</p>
<pre><code class="lang-bash">curl -fsSL https://claude.ai/install.sh | bash
</code></pre>
<p>安裝後，進入你的專案目錄，直接輸入 <code>claude</code> 就能啟動。</p>
<h3 id="heading-5zub56iu5zwf5yuv5qih5byp">四種啟動模式</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>指令</td><td>情境</td></tr>
</thead>
<tbody>
<tr>
<td><code>claude</code></td><td>標準啟動，開始全新對話（互動式）</td></tr>
<tr>
<td><code>claude -c</code></td><td>快速接回最近一次的對話</td></tr>
<tr>
<td><code>claude -r</code></td><td>顯示歷史對話列表，含摘要，選擇要接回哪一個</td></tr>
<tr>
<td><code>claude -p "..."</code></td><td>Headless Mode，非互動式，單次執行，用於自動化</td></tr>
</tbody>
</table>
</div><p>建議新手先從 <code>claude</code> 開始，熟悉基本操作後，<code>-c</code> 和 <code>-r</code> 會成為你每天的好朋友。</p>
<h3 id="heading-headless-modeclaude-p">Headless Mode（<code>claude -p</code>）</h3>
<p><code>claude -p</code> 是 Claude Code 從「個人工具」升級到「團隊基礎設施」的關鍵能力：</p>
<ul>
<li>非互動式，執行完就結束，不進入對話模式</li>
<li>適合用在 CI/CD、git hooks、自動化腳本</li>
<li>可搭配 <code>--output-format stream-json</code> 輸出結構化 JSON，方便下游程式處理</li>
<li>可搭配 <code>--allowedTools</code> 限制可用工具範圍</li>
</ul>
<pre><code class="lang-bash"><span class="hljs-comment"># 最簡單的用法</span>
claude -p <span class="hljs-string">"review 這個 PR 的安全性問題"</span>

<span class="hljs-comment"># pipe 資料進去</span>
cat error.log | claude -p <span class="hljs-string">"分析這些錯誤，找出最常見的根因"</span>

<span class="hljs-comment"># 輸出 JSON 給下游處理</span>
claude -p <span class="hljs-string">"列出所有 deprecated 的 API 呼叫"</span> --output-format stream-json | your_script.py

<span class="hljs-comment"># 限制工具範圍（更安全）</span>
claude -p <span class="hljs-string">"把 foo.py 從 React 改成 Vue"</span> --allowedTools Edit <span class="hljs-string">"Bash(git commit:*)"</span>
</code></pre>
<hr />
<h2 id="heading-56ys5lqm5q2l77ya5a245pyd5zyo5bcn6kmx5lit5pon5l2c">第二步：學會在對話中操作</h2>
<p>進入 Claude Code 後，它看起來像個聊天介面，但有一些特殊符號和快捷鍵讓你的效率倍增。</p>
<h3 id="heading-5lij5ycl6laf5pyj55so55qe56ym6jmf">三個超有用的符號</h3>
<p><strong><code>@</code> 指定檔案</strong>：輸入 <code>@</code> 後會顯示檔案列表，支援模糊搜尋，讓你精確告訴 Claude 要操作哪個檔案，不用擔心它讀錯地方。</p>
<p><strong><code>!</code> 直接執行 shell 指令</strong>：有時你只是想快速執行一個指令，不需要 AI 處理，直接用 <code>!</code> 前綴就能執行 shell 命令。例如 <code>!git status</code> 或 <code>!ls -la</code>。</p>
<p><strong><code>#</code> 加入記憶</strong>：當你輸入 <code>#</code> 開頭的訊息，系統會詢問你要存入哪個 CLAUDE.md，讓 Claude 長期記住這段背景知識。例如「# 我們的 API 版本是 v2，請不要使用 v1 的 endpoint」，之後的每次對話它都會記得。</p>
<h3 id="heading-5lin5yv5lin55l55qe5br5o236y21">不可不知的快捷鍵</h3>
<ul>
<li><strong><code>ESC</code></strong> — 中斷當前任務。Claude 正在瘋狂編輯檔案但方向不對？按 ESC 立刻停下，不會破壞 session，可以重新下指令</li>
<li><strong><code>ESC ESC</code>（按兩次）</strong> — 顯示過去發送的訊息列表，讓你選擇一個重新發送，類似「開分支」的概念，從不同的起點探索</li>
<li><strong><code>Shift+TAB</code></strong> — 切換工作模式</li>
</ul>
<h3 id="heading-5lij56iu5bel5l2c5qih5byp">三種工作模式</h3>
<p>使用 <code>Shift+TAB</code> 可以在三種模式間切換：</p>
<p><strong>自動接受模式（auto-accept edits）</strong>：Claude 提議的修改自動核准，適合信任度高、不想每次都確認的場景。速度最快。</p>
<p><strong>規劃模式（plan mode）</strong>：Claude 只做分析和規劃，不會實際動檔案。在開始寫程式之前，先用這個模式讓它產出設計方案給你審核，是架構討論的好幫手。</p>
<p><strong>危險模式（<code>--dangerously-skip-permissions</code>）</strong>：跳過所有權限確認，讓 Claude 全速工作。官方文件特別強調這個模式要在有限制的 Docker container 裡使用，不建議在本機直接用。</p>
<hr />
<h2 id="heading-claudemd">第三步：設定你的專案大腦 — CLAUDE.md</h2>
<p><code>CLAUDE.md</code> 是整個 Claude Code 生態系中最重要的概念。<strong>這個檔案是 Claude 每次啟動對話時必讀的說明書</strong>，放對內容，效果立竿見影。</p>
<p>第一次使用時，執行 <code>/init</code> 指令，Claude 會自動分析你的 codebase 結構並產生一份基礎的 CLAUDE.md，再手動精修即可。</p>
<h3 id="heading-claudemd-1">放什麼在 CLAUDE.md？</h3>
<ul>
<li>常用的 bash 指令（<code>npm run build</code>、<code>npm run test</code> 等）</li>
<li>核心檔案與工具函式的位置說明</li>
<li>程式碼風格規範（例如：使用 ES modules 而非 CommonJS）</li>
<li>測試方式與規則</li>
<li>Git 工作流程規範（例如：branch 命名方式、merge vs. rebase）</li>
<li>開發環境特殊設定</li>
<li>任何你希望 Claude 永遠記住的事項</li>
</ul>
<pre><code class="lang-markdown"><span class="hljs-section"># 常用指令</span>
<span class="hljs-bullet">-</span> npm run build: 建置專案
<span class="hljs-bullet">-</span> npm run test: 執行測試
<span class="hljs-bullet">-</span> npm run typecheck: 型別檢查

<span class="hljs-section"># 程式碼風格</span>
<span class="hljs-bullet">-</span> 使用 ES modules (import/export)，不用 CommonJS (require)
<span class="hljs-bullet">-</span> 盡量用解構語法引入 (import { foo } from 'bar')
<span class="hljs-bullet">-</span> 所有 API 呼叫都走 /src/api/ 資料夾的封裝

<span class="hljs-section"># 工作流程</span>
<span class="hljs-bullet">-</span> 每次改完記得跑 typecheck
<span class="hljs-bullet">-</span> 測試優先，盡量跑單一測試而非全套
<span class="hljs-bullet">-</span> branch 命名格式：feature/xxx 或 fix/xxx
</code></pre>
<h3 id="heading-claudemd-2">CLAUDE.md 的四個層級</h3>
<p>根據官方文件，CLAUDE.md 其實有四個層級，由高到低依序套用：</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>層級</td><td>位置</td><td>用途</td><td>誰能看到</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Enterprise policy</strong></td><td>macOS: <code>/Library/Application Support/ClaudeCode/CLAUDE.md</code></td><td>公司統一規範，由 IT/DevOps 部署</td><td>組織內所有使用者</td></tr>
<tr>
<td><strong>Project memory</strong></td><td><code>./CLAUDE.md</code> 或 <code>./.claude/CLAUDE.md</code></td><td>專案共用規範</td><td>團隊（透過 git）</td></tr>
<tr>
<td><strong>User memory</strong></td><td><code>~/.claude/CLAUDE.md</code></td><td>個人全域偏好</td><td>只有你（所有專案）</td></tr>
<tr>
<td><strong>Project memory (local)</strong></td><td><code>./CLAUDE.local.md</code></td><td>個人專案設定</td><td>只有你（當前專案）</td></tr>
</tbody>
</table>
</div><blockquote>
<p><code>CLAUDE.local.md</code> 會自動加入 <code>.gitignore</code>，適合放沙箱 URL、個人測試資料等不應上傳的設定。</p>
</blockquote>
<h3 id="heading-import"><code>@import</code> 語法</h3>
<p>CLAUDE.md 支援用 <code>@</code> 語法引入其他檔案，最多支援 5 層巢狀：</p>
<pre><code class="lang-markdown"><span class="hljs-section"># 引入其他說明文件</span>
請參考 @README 了解專案概覽，@package.json 查看可用指令。

<span class="hljs-section"># Git 工作流程</span>
@docs/git-instructions.md

<span class="hljs-section"># 個人偏好（引入自 home 目錄，不會進 git）</span>
@~/.claude/my-project-instructions.md
</code></pre>
<p>這讓 CLAUDE.md 保持簡潔，把細節拆到獨立文件裡分開維護。</p>
<h3 id="heading-monorepo">Monorepo 建議結構</h3>
<p>CLAUDE.md 支援<strong>多層級繼承</strong>，Claude 會自動把沿途讀到的所有 CLAUDE.md 合併進 context，這對 monorepo 非常適合：</p>
<pre><code>root/
├── CLAUDE.md                ← 全專案共用：monorepo 工具（nx/turborepo）、CI 指令、跨 package 規範
│                               可用 @<span class="hljs-keyword">import</span> 引入各 package 文件
├── docs/
│   └── git-instructions.md  ← 被 @<span class="hljs-keyword">import</span> 引用的獨立說明
├── packages/
│   ├── frontend/
│   │   └── CLAUDE.md        ← 前端專用：React 規範、CSS-<span class="hljs-keyword">in</span>-JS、UI 元件慣例
│   ├── backend/
│   │   └── CLAUDE.md        ← 後端專用：API 設計規範、DB migration 指令
│   └── shared/
│       └── CLAUDE.md        ← shared lib 專用：型別規範、不能有 side effect 等限制
└── CLAUDE.local.md          ← 個人設定，自動加入 .gitignore
</code></pre><p><strong>分層邏輯：</strong></p>
<ul>
<li><strong>根目錄</strong> 放「任何 package 都適用」的內容，例如 <code>pnpm install</code>、branch 命名規則、PR 流程</li>
<li><strong>各 package</strong> 只放「這個 package 才有意義」的差異化內容</li>
</ul>
<p>當你在 <code>packages/frontend/</code> 啟動 <code>claude</code>，它會同時讀到根目錄與 <code>packages/frontend/</code> 的 CLAUDE.md，不需要在每個 package 重複寫共用規範。</p>
<blockquote>
<p>小技巧：根目錄的 CLAUDE.md 加一行職責邊界說明，例如「<code>packages/shared</code> 只能被其他 package 引用，不能引用 frontend 或 backend 的程式碼」，跨 package 重構時 Claude 就不會犯錯。</p>
</blockquote>
<h3 id="heading-5yw25luw566h55cg6kiy5oa255qe5pa55byp">其他管理記憶的方式</h3>
<ul>
<li><strong><code>#</code> 快速新增</strong>：輸入 <code># 內容</code>，系統會詢問要存入哪個 CLAUDE.md</li>
<li><strong><code>/memory</code> 指令</strong>：在對話中執行，會用系統編輯器開啟記憶檔案，適合一次做大量整理</li>
</ul>
<hr />
<h2 id="heading-56ys5zub5q2l77ya5lik5lil5pah566h55cgiokalcdmnidpl5zpjbxnmotos4fmupa">第四步：上下文管理 — 最關鍵的資源</h2>
<p>很多人用 Claude Code 用著用著開始感覺它「變笨了」，原因幾乎都是一樣的：<strong>context window 滿了</strong>。</p>
<p>Claude 的整個對話歷史、讀過的每個檔案、執行過的每個指令輸出，全都塞在 context window 裡。一個複雜的除錯過程，可能幾萬個 token 就燒掉了。當 context 快滿，Claude 開始「遺忘」早期的指令，犯更多錯誤。</p>
<h3 id="heading-5ywp5ycl6zec6y215oyh5luk">兩個關鍵指令</h3>
<p><strong><code>/clear</code></strong>：清除所有當前對話的 context，重新開始。開始一個全新任務之前，養成習慣打 <code>/clear</code>，讓它帶著清爽的頭腦工作。</p>
<p><strong><code>/compact</code></strong>：壓縮 context，可以附上提示說明要保留哪些重點。注意，壓縮後有可能遺失部分細節、導致後續表現變差，更建議的做法是直接開新的 session。</p>
<p>官方建議：<strong>在長對話工作後、切換到新任務前，定期使用 <code>/clear</code></strong>。有些工程師甚至把「每次開始新任務就 /clear」當成強制習慣。</p>
<hr />
<h2 id="heading-think">第五步：善用 Think 模式深度推理</h2>
<p>Claude Code 支援幾個特殊關鍵字，讓它進入不同深度的思考模式：</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>層級</td><td>關鍵字</td></tr>
</thead>
<tbody>
<tr>
<td>基礎</td><td><code>think</code></td></tr>
<tr>
<td>中等</td><td><code>think more</code> / <code>think hard</code></td></tr>
<tr>
<td>深入</td><td><code>think longer</code> / <code>think harder</code></td></tr>
<tr>
<td>最大</td><td><code>ultrathink</code></td></tr>
</tbody>
</table>
</div><p>使用方式很直覺，直接在 prompt 裡說「請 think hard，設計這個資料庫 schema」或「ultrathink，分析這個 bug 的根本原因」即可。</p>
<p><strong>重要提醒</strong>：更深的思考消耗更多 token，建議根據問題複雜度選擇。簡單任務用 <code>think</code> 就夠，複雜架構設計或難以追蹤的 bug 才用 <code>ultrathink</code>。</p>
<p>中文指令「想一想」、「深度思考」也可以被識別，但優先用英文以確保穩定性。</p>
<hr />
<h2 id="heading-56ys5ywt5q2l77ya5o6m5oh6auy5pwi5bel5l2c5rwb">第六步：掌握高效工作流</h2>
<p>有了基礎工具知識後，來看幾個 Anthropic 官方推薦的實戰工作流。</p>
<h3 id="heading-commit">工作流一：探索 → 規劃 → 實作 → Commit</h3>
<p>這是最通用的工作流，適合大多數功能開發場景：</p>
<ol>
<li><strong>探索</strong>：告訴 Claude 讀相關的檔案、圖表或 URL，但<strong>明確說「還不要寫程式」</strong>。這個階段讓它建立完整的 context。</li>
<li><strong>規劃</strong>：請 Claude 制定實作計畫（可以用 <code>think hard</code> 讓它想得更仔細）。計畫確認後，請它寫成 Markdown 文件或 GitHub issue 存檔。</li>
<li><strong>實作</strong>：拿著確認好的計畫，請它動手寫程式。</li>
<li><strong>Commit</strong>：請 Claude 寫 commit message 並 commit，需要的話也可以請它開 PR。</li>
</ol>
<p>前兩步是關鍵。很多工程師跳過規劃直接叫它寫，結果方向跑偏，浪費更多時間。</p>
<h3 id="heading-tdd">工作流二：TDD 測試驅動開發</h3>
<p>這是 Anthropic 內部最愛的工作流，AI 時代的 TDD 威力加倍：</p>
<ol>
<li>請 Claude 根據預期的輸入輸出寫測試，<strong>明確說要做 TDD，不要先做 mock 實作</strong></li>
<li>請它跑測試，確認測試<strong>確實失敗</strong></li>
<li>對測試滿意後，請它 commit 測試</li>
<li>請 Claude 寫讓測試通過的實作，<strong>不允許修改測試</strong>，讓它自己跑測試、修改、再跑，直到全部通過</li>
<li>確認後 commit 實作</li>
</ol>
<p>提供清晰的「完成標準」是讓 Claude 表現最好的方式，而測試就是最好的完成標準。</p>
<h3 id="heading-ui">工作流三：截圖視覺迭代（UI 開發最愛）</h3>
<ol>
<li>給 Claude 一個設計稿圖片（拖放或貼上截圖）</li>
<li>請它實作 UI</li>
<li>透過 Puppeteer MCP 讓它截圖比對</li>
<li>請它根據差異迭代修正</li>
<li>滿意後 commit</li>
</ol>
<p>Claude 在有視覺目標時表現特別好。第一版可能不完美，但給它 2-3 輪迭代後通常相當接近設計稿。</p>
<hr />
<h2 id="heading-claude">第七步：進階技巧 — 讓 Claude 更強大</h2>
<h3 id="heading-mcp">擴充工具能力：MCP</h3>
<p>MCP（Model Context Protocol）是讓 Claude Code 連接外部服務的標準協議。常用的有：</p>
<ul>
<li><strong>Puppeteer</strong>：讓 Claude 能操控瀏覽器、截圖</li>
<li><strong>GitHub MCP</strong>：更深度整合 GitHub 操作</li>
<li><strong>資料庫 MCP</strong>：直接查詢你的資料庫</li>
</ul>
<p>在專案目錄加入 <code>.mcp.json</code> 設定檔，整個團隊都能共用這些工具。</p>
<h3 id="heading-slash">自訂 Slash 指令</h3>
<p>把重複的工作流程做成 slash 指令，存在 <code>.claude/commands/</code> 資料夾裡。例如建立 <code>fix-github-issue.md</code>：</p>
<pre><code class="lang-markdown">請分析並修復 GitHub issue：$ARGUMENTS

步驟：
<span class="hljs-bullet">1.</span> 用 gh issue view 取得 issue 詳細內容
<span class="hljs-bullet">2.</span> 理解問題描述
<span class="hljs-bullet">3.</span> 搜尋 codebase 找出相關檔案
<span class="hljs-bullet">4.</span> 實作必要的修改
<span class="hljs-bullet">5.</span> 撰寫並執行測試驗證修復
<span class="hljs-bullet">6.</span> 確認通過 lint 和 type check
<span class="hljs-bullet">7.</span> 建立描述性的 commit message
<span class="hljs-bullet">8.</span> Push 並開 PR
</code></pre>
<p>之後只要輸入 <code>/project:fix-github-issue 1234</code> 就能一鍵處理 issue #1234。個人常用指令則存到 <code>~/.claude/commands/</code> 讓所有專案都能用。</p>
<h3 id="heading-permissions">允許清單（Permissions）</h3>
<p>用 <code>/permissions</code> 指令把常用操作加入允許清單，例如 <code>Edit</code>（允許編輯檔案）和 <code>Bash(git commit:*)</code>（允許 git commit），不用每次都確認，工作流程更流暢。</p>
<hr />
<h2 id="heading-claude-1">第八步：多 Claude 並行 — 終極生產力</h2>
<p>當你熟悉了單 Claude 工作流後，可以嘗試更強大的多 Agent 模式。</p>
<h3 id="heading-claude-2">雙 Claude 互審</h3>
<p>用第一個 Claude 寫程式，另開一個 terminal 或用 <code>/clear</code> 重置，讓第二個 Claude 審查程式碼。不同的 context 往往能發現不同的問題，效果類似真實的 code review。</p>
<h3 id="heading-git-worktrees">Git Worktrees 平行作業</h3>
<p>這是 Anthropic 工程師內部的常用技巧：用 <code>git worktree add</code> 建立多個獨立的工作目錄，每個目錄開一個 Claude，同時處理不同任務：</p>
<pre><code class="lang-bash">git worktree add ../project-feature-a feature-a
git worktree add ../project-feature-b feature-b

<span class="hljs-built_in">cd</span> ../project-feature-a &amp;&amp; claude  <span class="hljs-comment"># Claude A 做 feature A</span>
<span class="hljs-built_in">cd</span> ../project-feature-b &amp;&amp; claude  <span class="hljs-comment"># Claude B 做 feature B</span>

<span class="hljs-comment"># 完成後清理</span>
git worktree remove ../project-feature-a
git worktree remove ../project-feature-b
</code></pre>
<p>兩個任務互不干擾，你只需要輪流去確認進度和核准操作。</p>
<hr />
<h2 id="heading-57wm5paw5oml55qe5bu66k2w5rif5zau">給新手的建議清單</h2>
<p><strong>操作習慣</strong></p>
<ul>
<li>每次開始新任務前先 <code>/clear</code>，保持 context 乾淨</li>
<li>養成用 <code>ESC</code> 中斷而非 <code>Ctrl+C</code>（後者會直接退出整個程式）</li>
<li>用 <code>@</code> 精確指定檔案，不要讓它猜</li>
<li>重要決策和設定用 <code>#</code> 記錄到 CLAUDE.md，或用 <code>/memory</code> 做整理</li>
</ul>
<p><strong>下指令的原則</strong></p>
<ul>
<li><strong>具體比模糊好</strong>。「幫我加測試」遠不如「為 foo.py 新增一個測試 case，覆蓋使用者未登入的邊界情況，不要用 mock」</li>
<li>先規劃再實作，省下來的時間遠比多打一次指令值得</li>
<li>根據問題複雜度選擇 think 層級，不要每次都 ultrathink</li>
</ul>
<p><strong>品質把關</strong></p>
<ul>
<li>Claude 寫的程式你負責 review，最終責任在你</li>
<li>有測試和 CI 的環境下信任它更多，沒有就要多留意</li>
<li>對話越長越要注意 context 品質，適時 <code>/clear</code> 比用 <code>/compact</code> 更可靠</li>
</ul>
<hr />
<h2 id="heading-57wq6kqe">結語</h2>
<p>Claude Code 的學習曲線不在於功能複雜，而在於思維方式的轉換：從「AI 幫我補全程式碼」轉向「AI 是我的協作工程師，我負責方向，它負責執行」。</p>
<p>一開始可能會覺得要打很多字、要設定很多東西。但一旦你的 CLAUDE.md 建立起來，slash 指令設定好，權限調整完，你會發現整個開發體驗有質的飛躍。</p>
<p>從今天開始，打開 terminal，進入你的專案，輸入 <code>claude</code>，然後問它：「你對這個 codebase 有什麼問題嗎？」——你們的旅程就此開始。</p>
<hr />
<p><em>參考資料：<a target="_blank" href="https://www.anthropic.com/engineering/claude-code-best-practices">Anthropic 官方 Claude Code Best Practices</a>、<a target="_blank" href="https://code.claude.com/docs/en/memory">Anthropic 官方 Memory 文件</a>、<a target="_blank" href="https://blog.cashwu.com/blog/2025/claude-code-tips">Cash Wu 的 Claude Code Tips</a></em></p>
]]></content:encoded></item><item><title><![CDATA[工程師的 Claude Code 實戰指南：從零開始到高效開發]]></title><description><![CDATA[工程師的 Claude Code 實戰指南：從零開始到高效開發

本文整合 Anthropic 官方 Best Practices 與社群實戰 Tips，帶你由淺入深掌握 Claude Code。


什麼是 Claude Code？為什麼值得學？
如果你還在用「複製程式碼貼到 ChatGPT，再複製答案貼回去」的工作流程，Claude Code 會讓你大開眼界。
Claude Code 是 Anthropic 推出的命令列工具，它直接活在你的 terminal 裡，能夠讀懂你的整個 codeb...]]></description><link>https://blog.ganhua.wang/claude-code</link><guid isPermaLink="true">https://blog.ganhua.wang/claude-code</guid><category><![CDATA[claude-code]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Wed, 18 Feb 2026 14:41:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1771425630435/85ac2cbf-a681-49d9-8f8d-580c082eba73.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-claude-code">工程師的 Claude Code 實戰指南：從零開始到高效開發</h1>
<blockquote>
<p>本文整合 <a target="_blank" href="https://www.anthropic.com/engineering/claude-code-best-practices">Anthropic 官方 Best Practices</a> 與社群實戰 Tips，帶你由淺入深掌握 Claude Code。</p>
<h2 id="heading-"></h2>
</blockquote>
<h2 id="heading-claude-code-1">什麼是 Claude Code？為什麼值得學？</h2>
<p>如果你還在用「複製程式碼貼到 ChatGPT，再複製答案貼回去」的工作流程，Claude Code 會讓你大開眼界。</p>
<p>Claude Code 是 Anthropic 推出的命令列工具，它直接活在你的 terminal 裡，能夠讀懂你的整個 codebase、寫入檔案、執行指令、操作 git，甚至幫你開 PR。它不只是個「提示框」，而是一個能主動採取行動的 AI 代理（agentic coding assistant）。</p>
<p>用一句話形容：<strong>你告訴它要做什麼，它去搞定</strong>。</p>
<p>很多從 Cursor、GitHub Copilot 轉過來的工程師都說，用過 Claude Code 之後回不去了。原因不是它比較聰明，而是它的工作方式根本不同——它在你的環境裡工作，而不是你把東西帶去它的環境。</p>
<hr />
<h2 id="heading-56ys5lia5q2l77ya5a6j6kod6iih5z65pys5zwf5yuv">第一步：安裝與基本啟動</h2>
<p>安裝 Claude Code 只需要一行：</p>
<pre><code class="lang-bash">curl -fsSL https://claude.ai/install.sh | bash
</code></pre>
<p>安裝後，進入你的專案目錄，直接輸入 <code>claude</code> 就能啟動。</p>
<h3 id="heading-5zub56iu5zwf5yuv5qih5byp">四種啟動模式</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>指令</td><td>情境</td></tr>
</thead>
<tbody>
<tr>
<td><code>claude</code></td><td>標準啟動，開始全新對話（互動式）</td></tr>
<tr>
<td><code>claude -c</code></td><td>快速接回最近一次的對話</td></tr>
<tr>
<td><code>claude -r</code></td><td>顯示歷史對話列表，含摘要，選擇要接回哪一個</td></tr>
<tr>
<td><code>claude -p "..."</code></td><td>Headless Mode，非互動式，單次執行，用於自動化</td></tr>
</tbody>
</table>
</div><p>建議新手先從 <code>claude</code> 開始，熟悉基本操作後，<code>-c</code> 和 <code>-r</code> 會成為你每天的好朋友。</p>
<h3 id="heading-headless-modeclaude-p">Headless Mode（<code>claude -p</code>）</h3>
<p><code>claude -p</code> 是 Claude Code 從「個人工具」升級到「團隊基礎設施」的關鍵能力：</p>
<ul>
<li>非互動式，執行完就結束，不進入對話模式</li>
<li>適合用在 CI/CD、git hooks、自動化腳本</li>
<li>可搭配 <code>--output-format stream-json</code> 輸出結構化 JSON，方便下游程式處理</li>
<li>可搭配 <code>--allowedTools</code> 限制可用工具範圍</li>
</ul>
<pre><code class="lang-bash"><span class="hljs-comment"># 最簡單的用法</span>
claude -p <span class="hljs-string">"review 這個 PR 的安全性問題"</span>

<span class="hljs-comment"># pipe 資料進去</span>
cat error.log | claude -p <span class="hljs-string">"分析這些錯誤，找出最常見的根因"</span>

<span class="hljs-comment"># 輸出 JSON 給下游處理</span>
claude -p <span class="hljs-string">"列出所有 deprecated 的 API 呼叫"</span> --output-format stream-json | your_script.py

<span class="hljs-comment"># 限制工具範圍（更安全）</span>
claude -p <span class="hljs-string">"把 foo.py 從 React 改成 Vue"</span> --allowedTools Edit <span class="hljs-string">"Bash(git commit:*)"</span>
</code></pre>
<hr />
<h2 id="heading-56ys5lqm5q2l77ya5a245pyd5zyo5bcn6kmx5lit5pon5l2c">第二步：學會在對話中操作</h2>
<p>進入 Claude Code 後，它看起來像個聊天介面，但有一些特殊符號和快捷鍵讓你的效率倍增。</p>
<h3 id="heading-5lij5ycl6laf5pyj55so55qe56ym6jmf">三個超有用的符號</h3>
<p><strong><code>@</code> 指定檔案</strong>：輸入 <code>@</code> 後會顯示檔案列表，支援模糊搜尋，讓你精確告訴 Claude 要操作哪個檔案，不用擔心它讀錯地方。</p>
<p><strong><code>!</code> 直接執行 shell 指令</strong>：有時你只是想快速執行一個指令，不需要 AI 處理，直接用 <code>!</code> 前綴就能執行 shell 命令。例如 <code>!git status</code> 或 <code>!ls -la</code>。</p>
<p><strong><code>#</code> 加入記憶</strong>：當你輸入 <code>#</code> 開頭的訊息，系統會詢問你要存入哪個 CLAUDE.md，讓 Claude 長期記住這段背景知識。例如「# 我們的 API 版本是 v2，請不要使用 v1 的 endpoint」，之後的每次對話它都會記得。</p>
<h3 id="heading-5lin5yv5lin55l55qe5br5o236y21">不可不知的快捷鍵</h3>
<ul>
<li><strong><code>ESC</code></strong> — 中斷當前任務。Claude 正在瘋狂編輯檔案但方向不對？按 ESC 立刻停下，不會破壞 session，可以重新下指令</li>
<li><strong><code>ESC ESC</code>（按兩次）</strong> — 顯示過去發送的訊息列表，讓你選擇一個重新發送，類似「開分支」的概念，從不同的起點探索</li>
<li><strong><code>Shift+TAB</code></strong> — 切換工作模式</li>
</ul>
<h3 id="heading-5lij56iu5bel5l2c5qih5byp">三種工作模式</h3>
<p>使用 <code>Shift+TAB</code> 可以在三種模式間切換：</p>
<p><strong>自動接受模式（auto-accept edits）</strong>：Claude 提議的修改自動核准，適合信任度高、不想每次都確認的場景。速度最快。</p>
<p><strong>規劃模式（plan mode）</strong>：Claude 只做分析和規劃，不會實際動檔案。在開始寫程式之前，先用這個模式讓它產出設計方案給你審核，是架構討論的好幫手。</p>
<p><strong>危險模式（<code>--dangerously-skip-permissions</code>）</strong>：跳過所有權限確認，讓 Claude 全速工作。官方文件特別強調這個模式要在有限制的 Docker container 裡使用，不建議在本機直接用。</p>
<hr />
<h2 id="heading-claudemd">第三步：設定你的專案大腦 — CLAUDE.md</h2>
<p><code>CLAUDE.md</code> 是整個 Claude Code 生態系中最重要的概念。<strong>這個檔案是 Claude 每次啟動對話時必讀的說明書</strong>，放對內容，效果立竿見影。</p>
<p>第一次使用時，執行 <code>/init</code> 指令，Claude 會自動分析你的 codebase 結構並產生一份基礎的 CLAUDE.md，再手動精修即可。</p>
<h3 id="heading-claudemd-1">放什麼在 CLAUDE.md？</h3>
<ul>
<li>常用的 bash 指令（<code>npm run build</code>、<code>npm run test</code> 等）</li>
<li>核心檔案與工具函式的位置說明</li>
<li>程式碼風格規範（例如：使用 ES modules 而非 CommonJS）</li>
<li>測試方式與規則</li>
<li>Git 工作流程規範（例如：branch 命名方式、merge vs. rebase）</li>
<li>開發環境特殊設定</li>
<li>任何你希望 Claude 永遠記住的事項</li>
</ul>
<pre><code class="lang-markdown"><span class="hljs-section"># 常用指令</span>
<span class="hljs-bullet">-</span> npm run build: 建置專案
<span class="hljs-bullet">-</span> npm run test: 執行測試
<span class="hljs-bullet">-</span> npm run typecheck: 型別檢查

<span class="hljs-section"># 程式碼風格</span>
<span class="hljs-bullet">-</span> 使用 ES modules (import/export)，不用 CommonJS (require)
<span class="hljs-bullet">-</span> 盡量用解構語法引入 (import { foo } from 'bar')
<span class="hljs-bullet">-</span> 所有 API 呼叫都走 /src/api/ 資料夾的封裝

<span class="hljs-section"># 工作流程</span>
<span class="hljs-bullet">-</span> 每次改完記得跑 typecheck
<span class="hljs-bullet">-</span> 測試優先，盡量跑單一測試而非全套
<span class="hljs-bullet">-</span> branch 命名格式：feature/xxx 或 fix/xxx
</code></pre>
<h3 id="heading-claudemd-2">CLAUDE.md 的四個層級</h3>
<p>根據官方文件，CLAUDE.md 其實有四個層級，由高到低依序套用：</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>層級</td><td>位置</td><td>用途</td><td>誰能看到</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Enterprise policy</strong></td><td>macOS: <code>/Library/Application Support/ClaudeCode/CLAUDE.md</code></td><td>公司統一規範，由 IT/DevOps 部署</td><td>組織內所有使用者</td></tr>
<tr>
<td><strong>Project memory</strong></td><td><code>./CLAUDE.md</code> 或 <code>./.claude/CLAUDE.md</code></td><td>專案共用規範</td><td>團隊（透過 git）</td></tr>
<tr>
<td><strong>User memory</strong></td><td><code>~/.claude/CLAUDE.md</code></td><td>個人全域偏好</td><td>只有你（所有專案）</td></tr>
<tr>
<td><strong>Project memory (local)</strong></td><td><code>./CLAUDE.local.md</code></td><td>個人專案設定</td><td>只有你（當前專案）</td></tr>
</tbody>
</table>
</div><blockquote>
<p><code>CLAUDE.local.md</code> 會自動加入 <code>.gitignore</code>，適合放沙箱 URL、個人測試資料等不應上傳的設定。</p>
</blockquote>
<h3 id="heading-import"><code>@import</code> 語法</h3>
<p>CLAUDE.md 支援用 <code>@</code> 語法引入其他檔案，最多支援 5 層巢狀：</p>
<pre><code class="lang-markdown"><span class="hljs-section"># 引入其他說明文件</span>
請參考 @README 了解專案概覽，@package.json 查看可用指令。

<span class="hljs-section"># Git 工作流程</span>
@docs/git-instructions.md

<span class="hljs-section"># 個人偏好（引入自 home 目錄，不會進 git）</span>
@~/.claude/my-project-instructions.md
</code></pre>
<p>這讓 CLAUDE.md 保持簡潔，把細節拆到獨立文件裡分開維護。</p>
<h3 id="heading-monorepo">Monorepo 建議結構</h3>
<p>CLAUDE.md 支援<strong>多層級繼承</strong>，Claude 會自動把沿途讀到的所有 CLAUDE.md 合併進 context，這對 monorepo 非常適合：</p>
<pre><code>root/
├── CLAUDE.md                ← 全專案共用：monorepo 工具（nx/turborepo）、CI 指令、跨 package 規範
│                               可用 @<span class="hljs-keyword">import</span> 引入各 package 文件
├── docs/
│   └── git-instructions.md  ← 被 @<span class="hljs-keyword">import</span> 引用的獨立說明
├── packages/
│   ├── frontend/
│   │   └── CLAUDE.md        ← 前端專用：React 規範、CSS-<span class="hljs-keyword">in</span>-JS、UI 元件慣例
│   ├── backend/
│   │   └── CLAUDE.md        ← 後端專用：API 設計規範、DB migration 指令
│   └── shared/
│       └── CLAUDE.md        ← shared lib 專用：型別規範、不能有 side effect 等限制
└── CLAUDE.local.md          ← 個人設定，自動加入 .gitignore
</code></pre><p><strong>分層邏輯：</strong></p>
<ul>
<li><strong>根目錄</strong> 放「任何 package 都適用」的內容，例如 <code>pnpm install</code>、branch 命名規則、PR 流程</li>
<li><strong>各 package</strong> 只放「這個 package 才有意義」的差異化內容</li>
</ul>
<p>當你在 <code>packages/frontend/</code> 啟動 <code>claude</code>，它會同時讀到根目錄與 <code>packages/frontend/</code> 的 CLAUDE.md，不需要在每個 package 重複寫共用規範。</p>
<blockquote>
<p>小技巧：根目錄的 CLAUDE.md 加一行職責邊界說明，例如「<code>packages/shared</code> 只能被其他 package 引用，不能引用 frontend 或 backend 的程式碼」，跨 package 重構時 Claude 就不會犯錯。</p>
</blockquote>
<h3 id="heading-5yw25luw566h55cg6kiy5oa255qe5pa55byp">其他管理記憶的方式</h3>
<ul>
<li><strong><code>#</code> 快速新增</strong>：輸入 <code># 內容</code>，系統會詢問要存入哪個 CLAUDE.md</li>
<li><strong><code>/memory</code> 指令</strong>：在對話中執行，會用系統編輯器開啟記憶檔案，適合一次做大量整理</li>
</ul>
<hr />
<h2 id="heading-56ys5zub5q2l77ya5lik5lil5pah566h55cgiokalcdmnidpl5zpjbxnmotos4fmupa">第四步：上下文管理 — 最關鍵的資源</h2>
<p>很多人用 Claude Code 用著用著開始感覺它「變笨了」，原因幾乎都是一樣的：<strong>context window 滿了</strong>。</p>
<p>Claude 的整個對話歷史、讀過的每個檔案、執行過的每個指令輸出，全都塞在 context window 裡。一個複雜的除錯過程，可能幾萬個 token 就燒掉了。當 context 快滿，Claude 開始「遺忘」早期的指令，犯更多錯誤。</p>
<h3 id="heading-5ywp5ycl6zec6y215oyh5luk">兩個關鍵指令</h3>
<p><strong><code>/clear</code></strong>：清除所有當前對話的 context，重新開始。開始一個全新任務之前，養成習慣打 <code>/clear</code>，讓它帶著清爽的頭腦工作。</p>
<p><strong><code>/compact</code></strong>：壓縮 context，可以附上提示說明要保留哪些重點。注意，壓縮後有可能遺失部分細節、導致後續表現變差，更建議的做法是直接開新的 session。</p>
<p>官方建議：<strong>在長對話工作後、切換到新任務前，定期使用 <code>/clear</code></strong>。有些工程師甚至把「每次開始新任務就 /clear」當成強制習慣。</p>
<hr />
<h2 id="heading-think">第五步：善用 Think 模式深度推理</h2>
<p>Claude Code 支援幾個特殊關鍵字，讓它進入不同深度的思考模式：</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>層級</td><td>關鍵字</td></tr>
</thead>
<tbody>
<tr>
<td>基礎</td><td><code>think</code></td></tr>
<tr>
<td>中等</td><td><code>think more</code> / <code>think hard</code></td></tr>
<tr>
<td>深入</td><td><code>think longer</code> / <code>think harder</code></td></tr>
<tr>
<td>最大</td><td><code>ultrathink</code></td></tr>
</tbody>
</table>
</div><p>使用方式很直覺，直接在 prompt 裡說「請 think hard，設計這個資料庫 schema」或「ultrathink，分析這個 bug 的根本原因」即可。</p>
<p><strong>重要提醒</strong>：更深的思考消耗更多 token，建議根據問題複雜度選擇。簡單任務用 <code>think</code> 就夠，複雜架構設計或難以追蹤的 bug 才用 <code>ultrathink</code>。</p>
<p>中文指令「想一想」、「深度思考」也可以被識別，但優先用英文以確保穩定性。</p>
<hr />
<h2 id="heading-56ys5ywt5q2l77ya5o6m5oh6auy5pwi5bel5l2c5rwb">第六步：掌握高效工作流</h2>
<p>有了基礎工具知識後，來看幾個 Anthropic 官方推薦的實戰工作流。</p>
<h3 id="heading-commit">工作流一：探索 → 規劃 → 實作 → Commit</h3>
<p>這是最通用的工作流，適合大多數功能開發場景：</p>
<ol>
<li><strong>探索</strong>：告訴 Claude 讀相關的檔案、圖表或 URL，但<strong>明確說「還不要寫程式」</strong>。這個階段讓它建立完整的 context。</li>
<li><strong>規劃</strong>：請 Claude 制定實作計畫（可以用 <code>think hard</code> 讓它想得更仔細）。計畫確認後，請它寫成 Markdown 文件或 GitHub issue 存檔。</li>
<li><strong>實作</strong>：拿著確認好的計畫，請它動手寫程式。</li>
<li><strong>Commit</strong>：請 Claude 寫 commit message 並 commit，需要的話也可以請它開 PR。</li>
</ol>
<p>前兩步是關鍵。很多工程師跳過規劃直接叫它寫，結果方向跑偏，浪費更多時間。</p>
<h3 id="heading-tdd">工作流二：TDD 測試驅動開發</h3>
<p>這是 Anthropic 內部最愛的工作流，AI 時代的 TDD 威力加倍：</p>
<ol>
<li>請 Claude 根據預期的輸入輸出寫測試，<strong>明確說要做 TDD，不要先做 mock 實作</strong></li>
<li>請它跑測試，確認測試<strong>確實失敗</strong></li>
<li>對測試滿意後，請它 commit 測試</li>
<li>請 Claude 寫讓測試通過的實作，<strong>不允許修改測試</strong>，讓它自己跑測試、修改、再跑，直到全部通過</li>
<li>確認後 commit 實作</li>
</ol>
<p>提供清晰的「完成標準」是讓 Claude 表現最好的方式，而測試就是最好的完成標準。</p>
<h3 id="heading-ui">工作流三：截圖視覺迭代（UI 開發最愛）</h3>
<ol>
<li>給 Claude 一個設計稿圖片（拖放或貼上截圖）</li>
<li>請它實作 UI</li>
<li>透過 Puppeteer MCP 讓它截圖比對</li>
<li>請它根據差異迭代修正</li>
<li>滿意後 commit</li>
</ol>
<p>Claude 在有視覺目標時表現特別好。第一版可能不完美，但給它 2-3 輪迭代後通常相當接近設計稿。</p>
<hr />
<h2 id="heading-claude">第七步：進階技巧 — 讓 Claude 更強大</h2>
<h3 id="heading-mcp">擴充工具能力：MCP</h3>
<p>MCP（Model Context Protocol）是讓 Claude Code 連接外部服務的標準協議。常用的有：</p>
<ul>
<li><strong>Puppeteer</strong>：讓 Claude 能操控瀏覽器、截圖</li>
<li><strong>GitHub MCP</strong>：更深度整合 GitHub 操作</li>
<li><strong>資料庫 MCP</strong>：直接查詢你的資料庫</li>
</ul>
<p>在專案目錄加入 <code>.mcp.json</code> 設定檔，整個團隊都能共用這些工具。</p>
<h3 id="heading-slash">自訂 Slash 指令</h3>
<p>把重複的工作流程做成 slash 指令，存在 <code>.claude/commands/</code> 資料夾裡。例如建立 <code>fix-github-issue.md</code>：</p>
<pre><code class="lang-markdown">請分析並修復 GitHub issue：$ARGUMENTS

步驟：
<span class="hljs-bullet">1.</span> 用 gh issue view 取得 issue 詳細內容
<span class="hljs-bullet">2.</span> 理解問題描述
<span class="hljs-bullet">3.</span> 搜尋 codebase 找出相關檔案
<span class="hljs-bullet">4.</span> 實作必要的修改
<span class="hljs-bullet">5.</span> 撰寫並執行測試驗證修復
<span class="hljs-bullet">6.</span> 確認通過 lint 和 type check
<span class="hljs-bullet">7.</span> 建立描述性的 commit message
<span class="hljs-bullet">8.</span> Push 並開 PR
</code></pre>
<p>之後只要輸入 <code>/project:fix-github-issue 1234</code> 就能一鍵處理 issue #1234。個人常用指令則存到 <code>~/.claude/commands/</code> 讓所有專案都能用。</p>
<h3 id="heading-permissions">允許清單（Permissions）</h3>
<p>用 <code>/permissions</code> 指令把常用操作加入允許清單，例如 <code>Edit</code>（允許編輯檔案）和 <code>Bash(git commit:*)</code>（允許 git commit），不用每次都確認，工作流程更流暢。</p>
<hr />
<h2 id="heading-claude-1">第八步：多 Claude 並行 — 終極生產力</h2>
<p>當你熟悉了單 Claude 工作流後，可以嘗試更強大的多 Agent 模式。</p>
<h3 id="heading-claude-2">雙 Claude 互審</h3>
<p>用第一個 Claude 寫程式，另開一個 terminal 或用 <code>/clear</code> 重置，讓第二個 Claude 審查程式碼。不同的 context 往往能發現不同的問題，效果類似真實的 code review。</p>
<h3 id="heading-git-worktrees">Git Worktrees 平行作業</h3>
<p>這是 Anthropic 工程師內部的常用技巧：用 <code>git worktree add</code> 建立多個獨立的工作目錄，每個目錄開一個 Claude，同時處理不同任務：</p>
<pre><code class="lang-bash">git worktree add ../project-feature-a feature-a
git worktree add ../project-feature-b feature-b

<span class="hljs-built_in">cd</span> ../project-feature-a &amp;&amp; claude  <span class="hljs-comment"># Claude A 做 feature A</span>
<span class="hljs-built_in">cd</span> ../project-feature-b &amp;&amp; claude  <span class="hljs-comment"># Claude B 做 feature B</span>

<span class="hljs-comment"># 完成後清理</span>
git worktree remove ../project-feature-a
git worktree remove ../project-feature-b
</code></pre>
<p>兩個任務互不干擾，你只需要輪流去確認進度和核准操作。</p>
<hr />
<h2 id="heading-57wm5paw5oml55qe5bu66k2w5rif5zau">給新手的建議清單</h2>
<p><strong>操作習慣</strong></p>
<ul>
<li>每次開始新任務前先 <code>/clear</code>，保持 context 乾淨</li>
<li>養成用 <code>ESC</code> 中斷而非 <code>Ctrl+C</code>（後者會直接退出整個程式）</li>
<li>用 <code>@</code> 精確指定檔案，不要讓它猜</li>
<li>重要決策和設定用 <code>#</code> 記錄到 CLAUDE.md，或用 <code>/memory</code> 做整理</li>
</ul>
<p><strong>下指令的原則</strong></p>
<ul>
<li><strong>具體比模糊好</strong>。「幫我加測試」遠不如「為 foo.py 新增一個測試 case，覆蓋使用者未登入的邊界情況，不要用 mock」</li>
<li>先規劃再實作，省下來的時間遠比多打一次指令值得</li>
<li>根據問題複雜度選擇 think 層級，不要每次都 ultrathink</li>
</ul>
<p><strong>品質把關</strong></p>
<ul>
<li>Claude 寫的程式你負責 review，最終責任在你</li>
<li>有測試和 CI 的環境下信任它更多，沒有就要多留意</li>
<li>對話越長越要注意 context 品質，適時 <code>/clear</code> 比用 <code>/compact</code> 更可靠</li>
</ul>
<hr />
<h2 id="heading-57wq6kqe">結語</h2>
<p>Claude Code 的學習曲線不在於功能複雜，而在於思維方式的轉換：從「AI 幫我補全程式碼」轉向「AI 是我的協作工程師，我負責方向，它負責執行」。</p>
<p>一開始可能會覺得要打很多字、要設定很多東西。但一旦你的 CLAUDE.md 建立起來，slash 指令設定好，權限調整完，你會發現整個開發體驗有質的飛躍。</p>
<p>從今天開始，打開 terminal，進入你的專案，輸入 <code>claude</code>，然後問它：「你對這個 codebase 有什麼問題嗎？」——你們的旅程就此開始。</p>
]]></content:encoded></item><item><title><![CDATA[Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦]]></title><description><![CDATA[在現代 AI 輔助開發中，我們不僅需要 AI 寫程式，更需要它懂規則、記性好，並且能自動處理那些繁瑣的雜事。透過 Claude Code Hooks 機制，我們可以介入 AI 的思考與執行迴圈，實現真正的「人機協作自動化」。

一、 動機與痛點：為什麼你需要介入 AI 的生命週期？
在預設狀態下，Claude Code 雖然強大，但它是「被動」且「無狀態」的，這導致了開發者常遇到以下痛點：

記憶重置 (Session Amnesia)：

痛點：每次重啟終端機，AI 就像失憶一樣。

解法：你...]]></description><link>https://blog.ganhua.wang/claude-code-event-driven-hooks</link><guid isPermaLink="true">https://blog.ganhua.wang/claude-code-event-driven-hooks</guid><category><![CDATA[claude.ai]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Sat, 24 Jan 2026 13:38:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769261866761/54abeedf-f539-43e7-9d60-a6257cf32119.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>在現代 AI 輔助開發中，我們不僅需要 AI 寫程式，更需要它懂規則、記性好，並且能自動處理那些繁瑣的雜事。透過 <strong>Claude Code Hooks</strong> 機制，我們可以介入 AI 的思考與執行迴圈，實現真正的「人機協作自動化」。</p>
<hr />
<h2 id="heading-ai">一、 動機與痛點：為什麼你需要介入 AI 的生命週期？</h2>
<p>在預設狀態下，Claude Code 雖然強大，但它是「被動」且「無狀態」的，這導致了開發者常遇到以下痛點：</p>
<ol>
<li><p><strong>記憶重置 (Session Amnesia)</strong>：</p>
<ul>
<li><p><em>痛點</em>：每次重啟終端機，AI 就像失憶一樣。</p>
</li>
<li><p><em>解法</em>：你需要一個機制，在 <code>SessionStart</code> 時自動把「上一集的劇情（Session Log）」灌輸給它。</p>
</li>
</ul>
</li>
<li><p><strong>程式碼品質不一 (Inconsistent Quality)</strong>：</p>
<ul>
<li><p><em>痛點</em>：AI 寫出的 Go 程式碼可能忘了 <code>gofmt</code>，或者留下了 <code>fmt.Println</code> 除錯訊息。</p>
</li>
<li><p><em>解法</em>：你需要一個「糾察隊」，在 <code>PostToolUse</code>（工具用完後）自動執行格式化與檢查。</p>
</li>
</ul>
</li>
<li><p><strong>危險操作 (Safety Risks)</strong>：</p>
<ul>
<li><p><em>痛點</em>：AI 有時會過度自信，想直接 <code>git push</code> 到主分支。</p>
</li>
<li><p><em>解法</em>：你需要在 <code>PreToolUse</code>（工具執行前）設下攔截點，強制顯示警告。</p>
</li>
</ul>
</li>
<li><p><strong>上下文丟失 (Context Drift)</strong>：</p>
<ul>
<li><p><em>痛點</em>：對話太長時，重要資訊被壓縮丟棄。</p>
</li>
<li><p><em>解法</em>：利用 <code>PreCompact</code> 在壓縮發生前，將關鍵狀態寫入硬碟。</p>
</li>
</ul>
</li>
</ol>
<hr />
<h2 id="heading-the-lifecycle">二、 核心機制：生命週期圖解 (The Lifecycle)</h2>
<p>要掌握 Hooks，必須理解這張生命週期圖。這不僅是流程，更是我們可以「插入程式碼」的機會點：</p>
<p><img src="https://mintcdn.com/claude-code/z2YM37Ycg6eMbID3/images/hooks-lifecycle.png?fit=max&amp;auto=format&amp;n=z2YM37Ycg6eMbID3&amp;q=85&amp;s=5c25fedbc3db6f8882af50c3cc478c32" alt="Hook lifecycle diagram showing the sequence of hooks from SessionStart through the agentic loop to SessionEnd" /></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Hook 事件</td><td>觸發時機</td><td>應用場景</td></tr>
</thead>
<tbody>
<tr>
<td><code>SessionStart</code></td><td>Claude Code 啟動時</td><td>載入上次進度、顯示專案狀態</td></tr>
<tr>
<td><code>SessionEnd</code></td><td>會話結束時</td><td>保存工作進度、建立 session 記錄</td></tr>
<tr>
<td><code>PreToolUse</code></td><td>執行工具前</td><td>安全檢查、阻擋危險操作</td></tr>
<tr>
<td><code>PostToolUse</code></td><td>執行工具後</td><td>程式碼格式化、品質檢查</td></tr>
<tr>
<td><code>Stop</code></td><td>AI 完成回應時</td><td>檢查未提交的 debug 程式碼</td></tr>
<tr>
<td><code>PreCompact</code></td><td>Context 壓縮前</td><td>保存重要狀態、記錄壓縮事件</td></tr>
</tbody>
</table>
</div><p>我們可以將其劃分為三個戰略區域：</p>
<h3 id="heading-1-session-management">1. 啟動與結束區 (Session Management)</h3>
<ul>
<li><p><code>SessionStart</code>：這是「載入記憶」的時刻。你的腳本 <code>session-start.js</code> 在這裡執行，負責掃描 <code>.claude/sessions/</code> 下的 <code>.tmp</code> 檔案，告訴 AI 上次工作到哪裡。</p>
</li>
<li><p><code>SessionEnd</code>：這是「存檔」的時刻。<code>session-end.js</code> 會將當前的狀態快照保存下來，供下次使用。</p>
</li>
</ul>
<h3 id="heading-2-the-agentic-loop">2. 代理循環區 (The Agentic Loop) - <em>自動化的核心</em></h3>
<p>這是圖中橙色虛線框起來的部分，也是 AI 實際工作的地方。</p>
<ul>
<li><p><code>PreToolUse</code> (攔截層)：在 AI 真正執行 <code>Bash</code> 或 <code>Edit</code> 之前。這是防止錯誤的最佳時機（例如阻擋 <code>git push</code>）。</p>
</li>
<li><p><code>PostToolUse</code> (修正層)：在 AI 修改完檔案後。你的腳本可以在這裡自動執行 <code>gofmt</code>，或是檢查有沒有遺留的 <code>fmt.Print</code>。</p>
</li>
</ul>
<h3 id="heading-3-maintenance">3. 維護區 (Maintenance)</h3>
<ul>
<li><code>PreCompact</code>：當 Token 即將爆滿時，系統會觸發壓縮。利用 <code>pre-compact.js</code> 記錄這一事件，防止重要資訊在壓縮中「無聲消失」。</li>
</ul>
<hr />
<h2 id="heading-5lij44cbiomfjee9rue1koaniiihiqnuazlq">三、 配置結構與語法</h2>
<p>Hooks 配置在 <code>.claude/settings.local.json</code> 中：</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"hooks"</span>: {
    <span class="hljs-attr">"HookEvent"</span>: [
      {
        <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"條件表達式"</span>,
        <span class="hljs-attr">"hooks"</span>: [
          {
            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
            <span class="hljs-attr">"command"</span>: <span class="hljs-string">"要執行的指令"</span>
          }
        ]
      }
    ]
  }
}
</code></pre>
<h3 id="heading-matcher">Matcher 語法</h3>
<ul>
<li><p><code>"*"</code> - 匹配所有情況</p>
</li>
<li><p><code>tool == "Bash"</code> - 匹配特定工具</p>
</li>
<li><p><code>tool == "Edit" &amp;&amp; tool_input.file_path matches "\\.go$"</code> - 組合條件</p>
</li>
<li><p><code>!(tool_input.file_path matches "README\\.md")</code> - 排除條件</p>
</li>
</ul>
<hr />
<h2 id="heading-5zub44cbiowvpuaisomfjee9ruinoaeko8mus9ooeahoifsacrowbmus6hus7gom6vo8nw">四、 實戰配置解析：你的腳本做了什麼？</h2>
<p>結合 Go 專案範例，這套配置實現了以下具體功能：</p>
<h3 id="heading-1-sessionstart">1. 智慧型記憶掛載 (<code>SessionStart</code>)</h3>
<p>你的配置不再只是依賴單一的 <code>CLAUDE.md</code>，而是引入了時間序列的 Session Log。</p>
<ul>
<li><p><strong>行為</strong>：腳本會檢查 <code>go.mod</code> 確認這是 Go 專案，並自動尋找最近修改過的 <code>sessions/*.tmp</code> 檔案。</p>
</li>
<li><p><strong>優勢</strong>：AI 一啟動就知道專案類型（Go Module）以及上次具體的工作內容，實現「無縫熱啟動」。</p>
</li>
</ul>
<p><strong>配置範例：</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"SessionStart"</span>: [
    {
      <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"*"</span>,
      <span class="hljs-attr">"hooks"</span>: [
        {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
          <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node /path/to/project/.claude/scripts/hooks/session-start.js"</span>
        }
      ]
    }
  ]
}
</code></pre>
<p><strong>session-start.js：</strong></p>
<pre><code class="lang-javascript"><span class="hljs-meta">#!/usr/bin/env node</span>
<span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);
<span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> sessionsDir = path.join(process.env.HOME, <span class="hljs-string">'.claude'</span>, <span class="hljs-string">'sessions'</span>);

  <span class="hljs-comment">// 檢查是否有最近的 session 記錄</span>
  <span class="hljs-keyword">if</span> (fs.existsSync(sessionsDir)) {
    <span class="hljs-keyword">const</span> files = fs.readdirSync(sessionsDir)
      .filter(<span class="hljs-function"><span class="hljs-params">f</span> =&gt;</span> f.endsWith(<span class="hljs-string">'.tmp'</span>))
      .sort().reverse();

    <span class="hljs-keyword">if</span> (files.length &gt; <span class="hljs-number">0</span>) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`[SessionStart] Found <span class="hljs-subst">${files.length}</span> recent session(s)`</span>);
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`[SessionStart] Latest: <span class="hljs-subst">${files[<span class="hljs-number">0</span>]}</span>`</span>);
    }
  }

  <span class="hljs-comment">// Go 專案檢測</span>
  <span class="hljs-keyword">if</span> (fs.existsSync(<span class="hljs-string">'go.mod'</span>)) {
    <span class="hljs-keyword">const</span> content = fs.readFileSync(<span class="hljs-string">'go.mod'</span>, <span class="hljs-string">'utf8'</span>);
    <span class="hljs-keyword">const</span> match = content.match(<span class="hljs-regexp">/^module\s+(.+)$/m</span>);
    <span class="hljs-keyword">if</span> (match) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`[SessionStart] Go project: <span class="hljs-subst">${match[<span class="hljs-number">1</span>]}</span>`</span>);
    }
  }

  process.exit(<span class="hljs-number">0</span>);
}

main();
</code></pre>
<h3 id="heading-2-posttooluse">2. 強制性程式碼規範 (<code>PostToolUse</code>)</h3>
<p>這是這套配置最精彩的部分—— <strong>自動修正 (Auto-Correction)</strong>。</p>
<ul>
<li><p><strong>行為</strong>：當監測到 <code>Edit</code> 工具修改了 <code>.go</code> 檔案後，Hooks 會自動觸發：</p>
<pre><code class="lang-bash">  gofmt -w <span class="hljs-string">"file_path"</span>
</code></pre>
<p>  同時，若發現檔案內含有 <code>fmt.Print</code>，會透過 <code>console.error</code> 警告開發者。</p>
</li>
<li><p><strong>優勢</strong>：即使 AI 生成的程式碼格式混亂，寫入硬碟的那一刻也會被強制修正為標準格式。這大幅減少了 code review 的負擔。</p>
</li>
</ul>
<p><strong>配置範例：</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"PostToolUse"</span>: [
    {
      <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"tool == \"Edit\" &amp;&amp; tool_input.file_path matches \"\\\\.go$\""</span>,
      <span class="hljs-attr">"hooks"</span>: [
        {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
          <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node -e \"const{execSync}=require('child_process');const fs=require('fs');let d='';process.stdin.on('data',c=&gt;d+=c);process.stdin.on('end',()=&gt;{const i=JSON.parse(d);const p=i.tool_input?.file_path;if(p&amp;&amp;fs.existsSync(p)){try{execSync('gofmt -w \\\"'+p+'\\\"',{stdio:['pipe','pipe','pipe']})}catch(e){console.error('[Hook] gofmt failed: '+e.message)}}console.log(d)})\""</span>
        }
      ]
    },
    {
      <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"tool == \"Edit\" &amp;&amp; tool_input.file_path matches \"\\\\.go$\""</span>,
      <span class="hljs-attr">"hooks"</span>: [
        {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
          <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node -e \"const fs=require('fs');let d='';process.stdin.on('data',c=&gt;d+=c);process.stdin.on('end',()=&gt;{const i=JSON.parse(d);const p=i.tool_input?.file_path;if(p&amp;&amp;fs.existsSync(p)){const c=fs.readFileSync(p,'utf8');if(/fmt\\\\.Print(ln|f)?\\\\(/.test(c)){console.error('[Hook] WARNING: fmt.Print found in '+p);console.error('[Hook] Consider using proper logging instead')}}console.log(d)})\""</span>
        }
      ]
    },
    {
      <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"tool == \"Bash\""</span>,
      <span class="hljs-attr">"hooks"</span>: [
        {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
          <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node -e \"let d='';process.stdin.on('data',c=&gt;d+=c);process.stdin.on('end',()=&gt;{const i=JSON.parse(d);const cmd=i.tool_input?.command||'';if(/gh pr create/.test(cmd)){const out=i.tool_output?.output||'';const m=out.match(/https:\\\\/\\\\/github.com\\\\/[^/]+\\\\/[^/]+\\\\/pull\\\\/\\\\d+/);if(m){console.error('[Hook] PR created: '+m[0])}}console.log(d)})\""</span>
        }
      ]
    }
  ]
}
</code></pre>
<h3 id="heading-3-pretooluse">3. 危險操作防護網 (<code>PreToolUse</code>)</h3>
<ul>
<li><p><strong>行為</strong>：當 AI 試圖執行 <code>git push</code> 時，Hooks 會攔截並輸出：</p>
<blockquote>
<p><code>[Hook] Review changes before push... Consider: git diff HEAD~1</code></p>
</blockquote>
</li>
<li><p><strong>優勢</strong>：增加了一道「冷靜期」，防止 AI 在未經人工確認的情況下將錯誤程式碼推送到遠端倉庫。</p>
</li>
</ul>
<p><strong>配置範例：</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"PreToolUse"</span>: [
    {
      <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"tool == \"Bash\" &amp;&amp; tool_input.command matches \"git push\""</span>,
      <span class="hljs-attr">"hooks"</span>: [
        {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
          <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node -e \"console.error('[Hook] Review changes before push...');console.error('[Hook] Consider: git diff HEAD~1, git log --oneline -5')\""</span>
        }
      ]
    },
    {
      <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"tool == \"Write\" &amp;&amp; tool_input.file_path matches \"\\\\.(md|txt)$\" &amp;&amp; !(tool_input.file_path matches \"README\\\\.md|CLAUDE\\\\.md\")"</span>,
      <span class="hljs-attr">"hooks"</span>: [
        {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
          <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node -e \"console.error('[Hook] WARNING: Creating documentation file');console.error('[Hook] Consider using CONTEXT.md for session notes')\""</span>
        }
      ]
    }
  ]
}
</code></pre>
<h3 id="heading-4-stop">4. 提交前最後檢查 (<code>Stop</code>)</h3>
<p><strong>配置範例：</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"Stop"</span>: [
    {
      <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"*"</span>,
      <span class="hljs-attr">"hooks"</span>: [
        {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
          <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node -e \"const{execSync}=require('child_process');const fs=require('fs');let d='';process.stdin.on('data',c=&gt;d+=c);process.stdin.on('end',()=&gt;{try{execSync('git rev-parse --git-dir',{stdio:'pipe'})}catch{console.log(d);process.exit(0)}try{const files=execSync('git diff --name-only HEAD',{encoding:'utf8',stdio:['pipe','pipe','pipe']}).split('\\\\n').filter(f=&gt;/\\\\.go$/.test(f)&amp;&amp;fs.existsSync(f));let hasDebug=false;for(const f of files){const content=fs.readFileSync(f,'utf8');if(/fmt\\\\.Print(ln|f)?\\\\(/.test(content)){console.error('[Hook] WARNING: fmt.Print found in '+f);hasDebug=true}}if(hasDebug)console.error('[Hook] Remove debug prints before committing')}catch(e){}console.log(d)})\""</span>
        }
      ]
    }
  ]
}
</code></pre>
<h3 id="heading-5-session-sessionend">5. Session 結束存檔 (<code>SessionEnd</code>)</h3>
<p><strong>配置範例：</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"SessionEnd"</span>: [
    {
      <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"*"</span>,
      <span class="hljs-attr">"hooks"</span>: [
        {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
          <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node /path/to/project/.claude/scripts/hooks/session-end.js"</span>
        }
      ]
    }
  ]
}
</code></pre>
<p><strong>session-end.js：</strong></p>
<pre><code class="lang-javascript"><span class="hljs-meta">#!/usr/bin/env node</span>
<span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);
<span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> sessionsDir = path.join(process.env.HOME, <span class="hljs-string">'.claude'</span>, <span class="hljs-string">'sessions'</span>);
  <span class="hljs-keyword">const</span> today = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString().split(<span class="hljs-string">'T'</span>)[<span class="hljs-number">0</span>];
  <span class="hljs-keyword">const</span> sessionFile = path.join(sessionsDir, <span class="hljs-string">`<span class="hljs-subst">${today}</span>-session.tmp`</span>);

  <span class="hljs-keyword">if</span> (!fs.existsSync(sessionsDir)) {
    fs.mkdirSync(sessionsDir, { <span class="hljs-attr">recursive</span>: <span class="hljs-literal">true</span> });
  }

  <span class="hljs-keyword">const</span> time = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toTimeString().slice(<span class="hljs-number">0</span>, <span class="hljs-number">5</span>);

  <span class="hljs-keyword">if</span> (fs.existsSync(sessionFile)) {
    <span class="hljs-keyword">let</span> content = fs.readFileSync(sessionFile, <span class="hljs-string">'utf8'</span>);
    content = content.replace(<span class="hljs-regexp">/\*\*Last Updated:\*\*.*/</span>, <span class="hljs-string">`**Last Updated:** <span class="hljs-subst">${time}</span>`</span>);
    fs.writeFileSync(sessionFile, content);
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`[SessionEnd] Updated session: <span class="hljs-subst">${today}</span>-session.tmp`</span>);
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">const</span> template = <span class="hljs-string">`# Session: <span class="hljs-subst">${today}</span>
**Started:** <span class="hljs-subst">${time}</span>
**Last Updated:** <span class="hljs-subst">${time}</span>

## Completed
- [ ]

## In Progress
- [ ]

## Notes for Next Session
-
`</span>;
    fs.writeFileSync(sessionFile, template);
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`[SessionEnd] Created session: <span class="hljs-subst">${today}</span>-session.tmp`</span>);
  }

  process.exit(<span class="hljs-number">0</span>);
}

main();
</code></pre>
<h3 id="heading-6-precompact">6. 壓縮前狀態保存 (<code>PreCompact</code>)</h3>
<p><strong>配置範例：</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"PreCompact"</span>: [
    {
      <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"*"</span>,
      <span class="hljs-attr">"hooks"</span>: [
        {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
          <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node /path/to/project/.claude/scripts/hooks/pre-compact.js"</span>
        }
      ]
    }
  ]
}
</code></pre>
<p><strong>pre-compact.js：</strong></p>
<pre><code class="lang-javascript"><span class="hljs-meta">#!/usr/bin/env node</span>
<span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);
<span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> sessionsDir = path.join(process.env.HOME, <span class="hljs-string">'.claude'</span>, <span class="hljs-string">'sessions'</span>);
  <span class="hljs-keyword">const</span> logFile = path.join(sessionsDir, <span class="hljs-string">'compaction-log.txt'</span>);

  <span class="hljs-keyword">if</span> (!fs.existsSync(sessionsDir)) {
    fs.mkdirSync(sessionsDir, { <span class="hljs-attr">recursive</span>: <span class="hljs-literal">true</span> });
  }

  <span class="hljs-keyword">const</span> timestamp = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString().replace(<span class="hljs-string">'T'</span>, <span class="hljs-string">' '</span>).slice(<span class="hljs-number">0</span>, <span class="hljs-number">19</span>);
  fs.appendFileSync(logFile, <span class="hljs-string">`[<span class="hljs-subst">${timestamp}</span>] Context compaction triggered\n`</span>);

  <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'[PreCompact] State preserved before compaction'</span>);
  process.exit(<span class="hljs-number">0</span>);
}

main();
</code></pre>
<hr />
<h2 id="heading-5lqu44cbioebrummhoe1koaniw">五、 目錄結構</h2>
<p>建議的專案 hooks 結構：</p>
<pre><code class="lang-sql">.claude/
├── settings.local.json    <span class="hljs-comment"># Hooks 配置</span>
├── scripts/
│   ├── hooks/
│   │   ├── session-start.js
│   │   ├── session-end.js
│   │   └── pre-compact.js
│   └── lib/
│       └── utils.js       <span class="hljs-comment"># 共用工具函式</span>
└── skills/                <span class="hljs-comment"># Claude Code skills</span>
</code></pre>
<hr />
<h2 id="heading-claude-code">六、 為什麼 Claude Code 仰賴這些設定？</h2>
<p>Claude Code 本質上是一個 <strong>「事件驅動的執行環境 (Event-Driven Execution Environment)」</strong>。</p>
<ol>
<li><p><strong>彌補 LLM 的缺陷</strong>：LLM 擅長生成，但不擅長「守紀律」和「記狀態」。Hooks 透過確定性的程式碼（Node.js/Shell）來彌補機率性的 AI 模型。</p>
</li>
<li><p><strong>標準輸入/輸出的管道設計</strong>： 注意到腳本中使用了 <code>process.stdin</code> 和 <code>process.stdout</code> 嗎？</p>
<pre><code class="lang-javascript"> process.stdin.on(<span class="hljs-string">'data'</span>, <span class="hljs-function"><span class="hljs-params">c</span> =&gt;</span> d += c); <span class="hljs-comment">// 接收 Claude 的數據</span>
 <span class="hljs-built_in">console</span>.log(d); <span class="hljs-comment">// 必須把數據傳下去，否則流程會斷掉</span>
</code></pre>
<p> 這設計讓 Hooks 成為類似 Linux Pipe 的過濾器，可以在不打斷 AI 思路的前提下，對資料進行「偷看」、「修改」或「阻擋」。</p>
</li>
</ol>
<hr />
<h2 id="heading-5lid44cbioazqoaejs6imghq">七、 注意事項</h2>
<ol>
<li><p><strong>Hook 必須輸出 stdin 資料</strong>：對於需要處理輸入的 hooks，必須在最後輸出 <code>console.log(d)</code> 將原始資料傳遞下去</p>
</li>
<li><p><strong>使用 stderr 顯示訊息</strong>：<code>console.error()</code> 用於顯示給使用者的訊息，<code>console.log()</code> 用於傳遞資料</p>
</li>
<li><p><strong>避免阻塞</strong>：Hook 腳本應快速執行，避免耗時操作</p>
</li>
<li><p><strong>錯誤處理</strong>：即使發生錯誤也應 <code>process.exit(0)</code>，避免阻斷 Claude Code 流程</p>
</li>
<li><p><strong>路徑處理</strong>：使用絕對路徑或相對於專案根目錄的路徑</p>
</li>
</ol>
<hr />
<h2 id="heading-5ywr44cbiomfjee9ruwjow4tushueahoihjoecuuiuiumdqq">八、 配置後帶來的行為變革</h2>
<p>一旦部署這套 <code>.claude/settings.local.json</code>，你的開發體驗將發生如下質變：</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>開發情境</strong></td><td><strong>觸發設定 (Hook)</strong></td><td><strong>系統自動化行為</strong></td><td><strong>開發者獲得的好處</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>開啟專案</strong></td><td><code>SessionStart</code></td><td>自動讀取 <code>sessions/2026-01-24.tmp</code> 並分析 <code>go.mod</code>。</td><td><strong>秒進狀態</strong>：不用再打字解釋「這是 Go 專案，上次做到哪」。</td></tr>
<tr>
<td><strong>AI 寫完 Code</strong></td><td><code>PostToolUse</code></td><td>背景靜默執行 <code>gofmt</code>。</td><td><strong>格式完美</strong>：檔案永遠符合 Go Standard，不會有縮排錯誤。</td></tr>
<tr>
<td><strong>AI 忘記刪 Log</strong></td><td><code>Stop</code>/<code>PostToolUse</code></td><td>掃描並紅字警告：<code>WARNING: fmt.Print found</code>。</td><td><strong>保持潔淨</strong>：防止 Debug 代碼污染生產環境。</td></tr>
<tr>
<td><strong>準備提交 PR</strong></td><td><code>PreToolUse</code></td><td>攔截 <code>git push</code> 並建議先 <code>git diff</code>。</td><td><strong>安全防護</strong>：避免意外將實驗性代碼推上線。</td></tr>
<tr>
<td><strong>關閉終端</strong></td><td><code>SessionEnd</code></td><td>將當前進度寫入 <code>*-session.tmp</code>。</td><td><strong>進度固化</strong>：確保今天的上下文能準確傳遞給明天的自己。</td></tr>
</tbody>
</table>
</div><hr />
<h2 id="heading-57i957wq">總結</h2>
<p>這套配置將 <strong>記憶持久化概念</strong> 進一步細化為針對 <strong>Go 語言特性的自動化工作流</strong>。它不僅解決了「失憶」問題，更透過 <code>gofmt</code> 和安全檢查，讓 AI 成為了一個「守紀律」的初級工程師，而不僅僅是一個聊天機器人。</p>
<hr />
<h2 id="heading-5yd6icd6loh5rqq">參考資源</h2>
<ul>
<li><a target="_blank" href="https://code.claude.com/docs/en/hooks">Claude Code Hooks 官方文件</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[剖析 OTel Collector Delta To Cumulative Processor]]></title><description><![CDATA[這篇筆記主要記錄我在研究 OpenTelemetry Collector Contrib 中 deltatocumulative Processor 的心得。除了基本的配置，我們直接從 Source Code 層級來看看它是怎麼運作的，特別是它在狀態管理上的設計，以及我們在生產環境踩過的那些「坑」。
1. 為什麼需要這個組件？
簡單來說，deltatocumulativeprocessor 的工作就是把 Delta (增量) 指標轉成 Cumulative (累積) 指標。
聽起來很簡單？但這是...]]></description><link>https://blog.ganhua.wang/otel-collector-delta-to-cumulative-processor</link><guid isPermaLink="true">https://blog.ganhua.wang/otel-collector-delta-to-cumulative-processor</guid><category><![CDATA[OpenTelemetry]]></category><category><![CDATA[observability]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Wed, 21 Jan 2026 14:48:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769006075411/32aad55c-e29f-4233-a87e-5049348de652.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>這篇筆記主要記錄我在研究 OpenTelemetry Collector Contrib 中 deltatocumulative Processor 的心得。除了基本的配置，我們直接從 Source Code 層級來看看它是怎麼運作的，特別是它在狀態管理上的設計，以及我們在生產環境踩過的那些「坑」。</p>
<h2 id="heading-1">1. 為什麼需要這個組件？</h2>
<p>簡單來說，<code>deltatocumulativeprocessor</code> 的工作就是把 <code>Delta (增量)</code> 指標轉成 <code>Cumulative (累積)</code> 指標。</p>
<p>聽起來很簡單？但這是一個 <code>Stateful (有狀態)</code> 的操作。這意味著 Processor 必須在記憶體裡「記住」所有 Time Series 當前的數值。一旦流量大起來，這裡就是記憶體洩漏或是數據遺失的高風險區。</p>
<h2 id="heading-2">2. 核心架構：它是如何撐住高併發的？</h2>
<p>為了不讓這個 Processor 成為效能瓶頸，此 Processor 的設計核心在於「高效的狀態管理」與「嚴格的時間序驗證」。為了在高併發下維持準確性，它採用了細粒度的鎖定策略與強型別的狀態存儲。</p>
<h3 id="heading-21-key-data-structures">2.1 關鍵資料結構 (Key Data Structures)</h3>
<p>核心邏輯位於 <code>processor/deltatocumulativeprocessor</code>，主要由以下結構支撐：</p>
<h4 id="heading-a-identitystream">A. 唯一識別 <code>identity.Stream</code></h4>
<p>Processor 怎麼知道哪些數據屬於同一個 Time Series？它依賴 <code>identity.Stream</code>。這不光是看 Metric Name，它會把 Name、Unit、Type 甚至所有的 Label (Attribute Hash) 組合起來當作唯一的 Key。所以，只要 Label 變了，對它來說就是一個全新的 Stream。</p>
<ul>
<li><p><strong>Metric Signature</strong>: Name, Unit, Type, Monotonicity, Temporality.</p>
</li>
<li><p><strong>Attributes Hash</strong>: 所有 DataPoint 屬性 (Labels) 的雜湊值。 這確保了即使是同一個 Metric Name，不同的 Label 組合也會被視為獨立的 Stream。</p>
</li>
</ul>
<h4 id="heading-b-state">B. 狀態儲存 <code>state</code></h4>
<p>在儲存方面，它用了 <code>maps.Parallel</code> (底層是 <code>xsync.MapOf</code>)。為了避開 Golang interface 轉換的開銷並確保型別安全，它很「搞剛」地把 Number (Sum/Gauge)、Histogram 和 ExponentialHistogram 拆成三個獨立的 Map 來存。：</p>
<ul>
<li><p><code>nums</code>: 存儲 <code>NumberDataPoint</code> (Sum/Gauge)</p>
</li>
<li><p><code>hist</code>: 存儲 <code>HistogramDataPoint</code></p>
</li>
<li><p><code>expo</code>: 存儲 <code>ExponentialHistogramDataPoint</code></p>
</li>
</ul>
<h4 id="heading-c-mutext">C. 鎖的策略、併發控制 <code>mutex[T]</code></h4>
<p>這是效能的關鍵點。Processor 沒有使用全域鎖 (Global Lock)。 如果每次處理數據都要鎖住整個 Map，那吞吐量肯定上不去。它為每一個獨立的 Stream 分配了一個專屬的 <code>mutex</code>。這意味著，除非多個請求同時更新「同一個 Metric 的同一個 Label 組合」，否則大家的更新操作是完全平行、互不卡頓的。</p>
<h3 id="heading-22-consumemetrics">2.2 數據流經的旅程 (<code>ConsumeMetrics</code>)</h3>
<p>當一筆 Metrics 進入 Processor 時，數據流經以下嚴格步驟：</p>
<ol>
<li><p><strong>過濾 (Filter)</strong>:</p>
<ul>
<li>檢查 <code>AggregationTemporality</code>。只有 <strong>Delta</strong> 類型的指標會被處理；Cumulative 指標直接透傳 (Pass-through)。</li>
</ul>
</li>
<li><p><strong>識別與查找 (Identify &amp; Lookup)</strong>:</p>
<ul>
<li><p>計算 DataPoint 的 <code>identity.Stream</code>。</p>
</li>
<li><p>嘗試從 <code>state</code> Map 中撈取現有累積值。</p>
</li>
<li><p><strong>注意</strong>：如果是新 Stream 且總數超過 <code>max_streams</code>，它會直接標記 <code>error="limit"</code> 然後丟棄。這在除錯時很容易被忽略。</p>
</li>
</ul>
</li>
<li><p><strong>聚合運算 (</strong><code>delta.Aggregate</code>):</p>
<ul>
<li><p>在 Stream 級別的鎖保護下執行。</p>
</li>
<li><p><strong>邏輯</strong>: <code>New_Cumulative = Old_Cumulative + New_Delta</code>。</p>
</li>
</ul>
</li>
<li><p><strong>時間序驗證 (Validation)</strong>: 在聚合前，必須通過兩項關鍵檢查 (位於 <code>internal/delta/delta.go</code>)：</p>
<ul>
<li><p><strong>亂序檢測 (</strong><code>ErrOutOfOrder</code>):</p>
<ul>
<li><p>條件: <code>New.Time &lt;= Stored.Time</code></p>
</li>
<li><p>結果: 丟棄數據。這通常發生在發送端重試或網路亂序時。</p>
</li>
</ul>
</li>
<li><p><strong>重啟檢測 (</strong><code>ErrOlderStart</code>):</p>
<ul>
<li><p>條件: <code>New.Start &lt; Stored.Start</code></p>
</li>
<li><p>結果: 丟棄數據。這代表來源進程可能已重啟，發送了屬於「上一代」的數據，或是時間戳生成有誤。</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>寫回與標記</strong>:</p>
<ul>
<li><p>將計算出的 Cumulative 值寫回原始 DataPoint。</p>
</li>
<li><p>將 Temporality 修改為 <code>Cumulative</code>。</p>
</li>
<li><p>更新 <code>stale</code> map 中的最後活躍時間 (Last Seen)。</p>
</li>
</ul>
</li>
</ol>
<h3 id="heading-23-gc">2.3 垃圾回收機制 (GC)</h3>
<p>為了避免記憶體爆炸（例如 Pod 重啟頻繁導致 Stream 無限增長），這裡有一個背景 Goroutine，<code>每分鐘</code>會巡一次。只要發現某個 Stream 超過 max_stale 沒更新，就會把它從記憶體中清掉。</p>
<ul>
<li><strong>邏輯</strong>: 遍歷 <code>stale</code> Map。如果 <code>now - last_seen &gt; max_stale</code>，則從記憶體中刪除該 Stream 的所有狀態。</li>
</ul>
<hr />
<h2 id="heading-3-configuration">3. 設定檔該怎麼調？ (Configuration)</h2>
<p>只有兩個參數，但都很致命：</p>
<pre><code class="lang-yaml"><span class="hljs-attr">processors:</span>
    <span class="hljs-attr">deltatocumulative:</span>
       <span class="hljs-comment"># Stream 多久沒動靜就清掉？預設 5m。</span>
        <span class="hljs-comment"># 坑點：設太短會導致狀態頻繁重置（數據斷層）；設太長記憶體會爆</span>
        <span class="hljs-attr">max_stale:</span> <span class="hljs-string">5m</span>

        <span class="hljs-comment"># 允許追蹤的最大 Stream 數量 (預設為 Max Int)</span>
        <span class="hljs-comment"># 影響: 這是保護 Collector 不被 High Cardinality 數據撐爆的最後防線。</span>
        <span class="hljs-comment"># 這是防線。一旦爆了，超出的數據會被「無情丟棄」。</span>
        <span class="hljs-attr">max_streams:</span> <span class="hljs-number">9223372036854775807</span>
</code></pre>
<hr />
<h2 id="heading-4-observability">4. 監控指標詳解 (Observability)</h2>
<p>Processor 透過 <code>internal/telemetry</code> 暴露了自我監控指標 (Self-monitoring Metrics)，這是排查數據丟失問題的首要依據。當你懷疑數據掉了，請先看這些自我監控指標 (internal/telemetry)：</p>
<h3 id="heading-41">4.1 核心指標列表</h3>
<ul>
<li><code>deltatocumulative_datapoints</code>：這是最重要的 Counter。<ul>
<li>看 <code>error="limit"</code>：是不是 <code>max_streams</code> 設太小了？</li>
<li>看 <code>error="delta.ErrOutOfOrder"</code>：發送端是不是時間戳亂跳？</li>
</ul>
</li>
<li><code>deltatocumulative_streams_tracked</code>：目前記憶體裡到底存了多少 Stream。</li>
</ul>
<div class="hn-table">
<table>
<thead>
<tr>
<td>指標名稱 (Metric Name)</td><td>類型</td><td>說明</td><td>關鍵標籤 (Labels)</td></tr>
</thead>
<tbody>
<tr>
<td><code>deltatocumulative_datapoints</code></td><td>Counter</td><td>處理的數據點總數。請密切關注 <code>error</code> 標籤。</td><td><strong>error</strong>:</td></tr>
<tr>
<td>- <code>(missing)</code>: 處理成功</td><td></td><td></td><td></td></tr>
<tr>
<td>- <code>limit</code>: 觸發 <code>max_streams</code> 上限而丟棄</td><td></td><td></td><td></td></tr>
<tr>
<td>- <code>delta.ErrOutOfOrder</code>: 因時間戳亂序而丟棄</td><td></td><td></td><td></td></tr>
<tr>
<td>- <code>delta.ErrOlderStart</code>: 因起始時間異常而丟棄</td><td></td><td></td><td></td></tr>
<tr>
<td><code>deltatocumulative_streams_tracked</code></td><td>Gauge</td><td>當前記憶體中活躍追蹤的 Stream 總數。</td><td>無</td></tr>
<tr>
<td><code>deltatocumulative_streams_limit</code></td><td>Gauge</td><td>配置的 <code>max_streams</code> 值。</td><td>無</td></tr>
<tr>
<td><code>deltatocumulative_streams_max_stale</code></td><td>Gauge</td><td>配置的 <code>max_stale</code> 值 (秒)。</td><td>無</td></tr>
</tbody>
</table>
</div><hr />
<ol start="5">
<li><h2 id="heading-57ea5lik6ay85pwf5lql77ya5bi46kal5zwp6agm5ymw5p6q">線上鬼故事：常見問題剖析</h2>
</li>
</ol>
<h3 id="heading-streamstracked">案例一：狀態丟失與 <code>streams_tracked</code> 的鋸齒狀波動</h3>
<p><strong>現象：</strong> 監控圖表顯示 <code>streams_tracked</code> 呈現週期性的鋸齒狀下跌，或者劇烈震盪。同時下游看到的數值可能突然歸零或重置。</p>
<p><strong>原因：</strong> 這通常與 GC 機制 (<code>stale</code> check) 有關。</p>
<ol>
<li><p><strong>間歇性流量</strong>: 如果某個指標每 6 分鐘才送一次，而 <code>max_stale</code> 設為 5 分鐘。Processor 會在第 5 分鐘刪除狀態。第 6 分鐘數據進來時，被視為全新的 Stream，累積值從 0 (或當前 Delta) 開始計算，導致<strong>狀態丟失</strong>。</p>
</li>
<li><p><strong>GC 運作</strong>: 背景 Goroutine 每分鐘一次的清理動作，會導致 <code>streams_tracked</code> 出現階梯式下降。</p>
</li>
</ol>
<p><strong>解法：</strong></p>
<ul>
<li>確保 <code>max_stale</code> 顯著大於 metrics 的 scrape interval 或 push interval (建議至少 2-3 倍)。</li>
</ul>
<h3 id="heading-pipeline">案例二：消失的數據與 Pipeline 順序之謎</h3>
<p><strong>現象：</strong><code>Batch</code> Processor 報告發送了 2.6k 點，但 <code>exporter</code> 報告只發送了 2.0k 點。中間的 0.6k 憑空消失，且沒有任何 Error Log。</p>
<p><strong>源碼級原因：</strong> 這是 <code>deltatocumulative</code> 的 <code>max_streams</code> 限制與 Pipeline 順序共同作用的結果。</p>
<pre><code class="lang-go"><span class="hljs-comment">// processor.go 片段</span>
<span class="hljs-keyword">if</span> maps.Exceeded(last, loaded) {
    attrs.Set(telemetry.Error(<span class="hljs-string">"limit"</span>))
    <span class="hljs-keyword">return</span> drop <span class="hljs-comment">// 靜默丟棄，只會標記 error label</span>
}
</code></pre>
<p>如果 Pipeline 配置為 <code>[batch, deltatocumulative]</code>：</p>
<ol>
<li><p><strong>Batch</strong>: 收到數據，計數器 <code>batch_send_size</code> +2.6k。</p>
</li>
<li><p><strong>DeltaToCumulative</strong>: 發現 Stream 總數超標，靜默丟棄 0.6k 數據點 (僅增加 <code>deltatocumulative_datapoints{error="limit"}</code>)。</p>
</li>
<li><p><strong>Exporter</strong>: 收到剩餘的 2.0k，計數器 <code>sent_metric_points</code> +2.0k。</p>
</li>
</ol>
<p><strong>解法：</strong></p>
<ol>
<li><p><strong>調整順序</strong>: 改為 <code>[deltatocumulative, batch]</code>。讓過濾發生在打包之前。</p>
</li>
<li><p><strong>監控 Drop</strong>: 設置告警監控 <code>sum(rate(otelcol_deltatocumulative_datapoints{error="limit"}[2m])) &gt; 0</code>。</p>
</li>
</ol>
<h3 id="heading-out-of-order">案例三：亂序數據 (Out of Order)</h3>
<p><strong>現象：</strong> 數據偶爾丟失，<code>deltatocumulative_datapoints</code> 出現 <code>error="out_of_order"</code>。</p>
<p><strong>原因 (</strong><code>internal/delta/delta.go</code>)：</p>
<pre><code class="lang-go"><span class="hljs-keyword">case</span> dp.Timestamp() &lt;= state.Timestamp():
    <span class="hljs-keyword">return</span> ErrOutOfOrder{...}
</code></pre>
<p>Processor 為了保證累積值的單調遞增，對時間戳要求非常嚴格 (New.Time &gt; Stored.Time)。如果發送端 (如 Prometheus Remote Write 或某些 SDK) 因重試邏輯發送了重複或舊的時間戳，Processor 會為了保護累積值的單調性而拒絕該數據。</p>
<p><strong>解法：</strong> 檢查發送端的 Retry 策略或時鐘同步狀態。</p>
<hr />
<h2 id="heading-6-poc">6. 本地重現 (PoC)</h2>
<p>為了驗證那個「Pipeline 順序導致數據消失」的鬼故事，我用 Docker Compose 搞了個實驗。 (詳細配置略，核心概念是用 60 個 telemetrygen 實例去衝撞 max_streams: 50 的限制)</p>
<h3 id="heading-61">6.1 環境架構</h3>
<pre><code class="lang-sql">┌─────────────────┐     ┌─────────────────────────────────────┐     ┌────────────┐
│  telemetrygen   │────▶│         OTel Collector              │────▶│ Prometheus │
│  (60 instances) │     │  ┌─────────────────────────────┐    │     │  :9090     │
│  app-01 ~ 60    │     │  │ Pipeline:                   │    │     └────────────┘
└─────────────────┘     │  │ receiver → cumulativetodelta│    │
                        │  │          → batch            │    │
                        │  │          → deltatocumulative│    │
                        │  │          → exporter         │    │
                        │  │                             │    │
                        │  │ max_streams: 50 (&lt; 60)      │    │
                        │  └─────────────────────────────┘    │
                        └─────────────────────────────────────┘
</code></pre>
<p><strong>設計理念：</strong></p>
<ul>
<li><p>60 個 telemetrygen 實例，每個發送不同的 <code>service.name</code> (app-01 ~ app-60)</p>
</li>
<li><p><code>max_streams</code> 設為 50，刻意製造 Stream 超限</p>
</li>
<li><p>Pipeline 順序為 <code>[cumulativetodelta, batch, deltatocumulative]</code>，重現「先 batch 後過濾」的問題</p>
</li>
</ul>
<h3 id="heading-62">6.2 檔案結構</h3>
<pre><code class="lang-sql">poc-lab/
├── docker-compose.yaml   <span class="hljs-comment"># Docker Compose 配置</span>
├── otel-config.yaml      <span class="hljs-comment"># OTel Collector 配置</span>
└── prometheus.yaml       <span class="hljs-comment"># Prometheus scrape 配置</span>
</code></pre>
<h3 id="heading-63">6.3 配置檔案</h3>
<h4 id="heading-otel-configyaml"><code>otel-config.yaml</code></h4>
<pre><code class="lang-yaml"><span class="hljs-attr">receivers:</span>
  <span class="hljs-attr">otlp:</span>
    <span class="hljs-attr">protocols:</span>
      <span class="hljs-attr">grpc:</span>
        <span class="hljs-attr">endpoint:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">:4317</span>

<span class="hljs-attr">processors:</span>
  <span class="hljs-comment"># 將 telemetrygen 產生的 Cumulative 轉成 Delta</span>
  <span class="hljs-attr">cumulativetodelta:</span>

  <span class="hljs-attr">batch:</span>
    <span class="hljs-attr">send_batch_size:</span> <span class="hljs-number">100</span>
    <span class="hljs-attr">timeout:</span> <span class="hljs-string">1s</span>

  <span class="hljs-attr">deltatocumulative:</span>
    <span class="hljs-attr">max_stale:</span> <span class="hljs-string">1m</span>
    <span class="hljs-comment"># 【關鍵設定】設定極低的上限，強迫發生 Drop</span>
    <span class="hljs-attr">max_streams:</span> <span class="hljs-number">50</span>

<span class="hljs-attr">exporters:</span>
  <span class="hljs-attr">prometheus:</span>
    <span class="hljs-attr">endpoint:</span> <span class="hljs-string">"0.0.0.0:8889"</span>
    <span class="hljs-attr">namespace:</span> <span class="hljs-string">"poc_app"</span>

  <span class="hljs-attr">debug:</span>
    <span class="hljs-attr">verbosity:</span> <span class="hljs-string">normal</span>

<span class="hljs-attr">service:</span>
  <span class="hljs-attr">telemetry:</span>
    <span class="hljs-attr">metrics:</span>
      <span class="hljs-attr">readers:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">pull:</span>
            <span class="hljs-attr">exporter:</span>
              <span class="hljs-attr">prometheus:</span>
                <span class="hljs-attr">host:</span> <span class="hljs-string">"0.0.0.0"</span>
                <span class="hljs-attr">port:</span> <span class="hljs-number">8888</span>

  <span class="hljs-attr">pipelines:</span>
    <span class="hljs-attr">metrics:</span>
      <span class="hljs-attr">receivers:</span> [<span class="hljs-string">otlp</span>]
      <span class="hljs-comment"># 【關鍵錯誤順序】重現問題</span>
      <span class="hljs-attr">processors:</span> [<span class="hljs-string">cumulativetodelta</span>, <span class="hljs-string">batch</span>, <span class="hljs-string">deltatocumulative</span>]
      <span class="hljs-attr">exporters:</span> [<span class="hljs-string">prometheus</span>, <span class="hljs-string">debug</span>]
</code></pre>
<h4 id="heading-prometheusyaml"><code>prometheus.yaml</code></h4>
<pre><code class="lang-yaml"><span class="hljs-attr">global:</span>
  <span class="hljs-attr">scrape_interval:</span> <span class="hljs-string">5s</span>
  <span class="hljs-attr">evaluation_interval:</span> <span class="hljs-string">5s</span>

<span class="hljs-attr">scrape_configs:</span>
  <span class="hljs-comment"># Job 1: 監控 Collector 本身</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">job_name:</span> <span class="hljs-string">'otel-collector-internal'</span>
    <span class="hljs-attr">static_configs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">targets:</span> [<span class="hljs-string">'otel-collector:8888'</span>]

  <span class="hljs-comment"># Job 2: 監控實際輸出的數據</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">job_name:</span> <span class="hljs-string">'app-metrics'</span>
    <span class="hljs-attr">static_configs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">targets:</span> [<span class="hljs-string">'otel-collector:8889'</span>]
</code></pre>
<h4 id="heading-docker-composeyaml"><code>docker-compose.yaml</code> (精簡版)</h4>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">otel-collector:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">otel/opentelemetry-collector-contrib:0.142.0</span>
    <span class="hljs-attr">command:</span> [<span class="hljs-string">"--config=/etc/otel-collector-config.yaml"</span>]
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./otel-config.yaml:/etc/otel-collector-config.yaml:ro</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"4317:4317"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8888:8888"</span>   <span class="hljs-comment"># Collector Internal Metrics</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8889:8889"</span>   <span class="hljs-comment"># Prometheus Exporter</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">otel-network</span>

  <span class="hljs-attr">prometheus:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">prom/prometheus:latest</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./prometheus.yaml:/etc/prometheus/prometheus.yml:ro</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"9090:9090"</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">otel-collector</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">otel-network</span>

  <span class="hljs-comment"># 使用 YAML anchor 定義 60 個 telemetrygen 實例</span>
  <span class="hljs-attr">telemetrygen-01:</span> <span class="hljs-string">&amp;telemetrygen-base</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen:latest</span>
    <span class="hljs-attr">command:</span> [<span class="hljs-string">"metrics"</span>, <span class="hljs-string">"--otlp-insecure"</span>, <span class="hljs-string">"--otlp-endpoint=otel-collector:4317"</span>,
              <span class="hljs-string">"--rate=5"</span>, <span class="hljs-string">"--duration=1000h"</span>, <span class="hljs-string">"--metric-type=Sum"</span>, <span class="hljs-string">"--service=app-01"</span>]
    <span class="hljs-attr">depends_on:</span> [<span class="hljs-string">otel-collector</span>]
    <span class="hljs-attr">networks:</span> [<span class="hljs-string">otel-network</span>]

  <span class="hljs-attr">telemetrygen-02:</span> { <span class="hljs-string">&lt;&lt;:</span> <span class="hljs-string">*telemetrygen-base</span>, <span class="hljs-attr">command:</span> [<span class="hljs-string">...</span>, <span class="hljs-string">"--service=app-02"</span>] }
  <span class="hljs-comment"># ... (app-03 ~ app-60)</span>

<span class="hljs-attr">networks:</span>
  <span class="hljs-attr">otel-network:</span>
    <span class="hljs-attr">driver:</span> <span class="hljs-string">bridge</span>
</code></pre>
<h3 id="heading-64">6.4 運行與驗證</h3>
<h4 id="heading-1-1">步驟 1: 啟動環境</h4>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> poc-lab
docker compose up -d
</code></pre>
<h4 id="heading-2-30-60">步驟 2: 等待數據累積 (約 30-60 秒)</h4>
<pre><code class="lang-bash">docker compose ps  <span class="hljs-comment"># 確認所有容器運行中</span>
</code></pre>
<h4 id="heading-3">步驟 3: 驗證查詢</h4>
<p>打開 Prometheus UI (http://localhost:9090) 或使用 curl：</p>
<p><strong>查詢 1: Streams 追蹤數量 (應該達到上限 50)</strong></p>
<pre><code class="lang-sql">otelcol_deltatocumulative_streams_tracked
</code></pre>
<p><strong>查詢 2: 數據點處理結果 (按 error 分類)</strong></p>
<pre><code class="lang-sql">otelcol_deltatocumulative_datapoints_total
</code></pre>
<p><strong>查詢 3: 比較 Batch vs Exporter</strong></p>
<pre><code class="lang-sql"><span class="hljs-comment"># Batch 處理量</span>
otelcol_processor_batch_batch_send_size_sum

<span class="hljs-comment"># Exporter 發送量</span>
otelcol_exporter_sent_metric_points_total
</code></pre>
<h3 id="heading-65">6.5 驗證結果 - 問題重現 (錯誤順序)</h3>
<p>使用錯誤的 Pipeline 順序 <code>[cumulativetodelta, batch, deltatocumulative]</code> 運行後：</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>指標</td><td>數值</td><td>說明</td></tr>
</thead>
<tbody>
<tr>
<td><code>streams_tracked</code></td><td><strong>50</strong></td><td>達到 max_streams 上限</td></tr>
<tr>
<td><code>streams_limit</code></td><td>50</td><td>配置值</td></tr>
<tr>
<td><code>receiver_accepted</code></td><td>13,000</td><td>Receiver 接收的總數據點</td></tr>
<tr>
<td><code>batch_send_size_sum</code></td><td><strong>13,000</strong></td><td>Batch 處理的數據點</td></tr>
<tr>
<td><code>exporter_sent</code></td><td><strong>11,000</strong></td><td>Exporter 實際發送的數據點</td></tr>
</tbody>
</table>
</div><p><strong>數據點處理詳情：</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>error 標籤</td><td>數值</td><td>說明</td></tr>
</thead>
<tbody>
<tr>
<td><code>error="none"</code></td><td>16,000</td><td>成功處理</td></tr>
<tr>
<td><code>error="limit"</code></td><td><strong>3,000</strong></td><td>因達到 Stream 上限而丟棄</td></tr>
</tbody>
</table>
</div><p><strong>關鍵發現：</strong></p>
<ul>
<li><p>發送端: 60 個 telemetrygen 實例</p>
</li>
<li><p>限制: max_streams = 50</p>
</li>
<li><p>被完全丟棄的實例: 10 個 (60 - 50)</p>
</li>
<li><p><strong>Batch (13,000) ≠ Exporter (11,000)</strong>：差異 2,000 個數據點「憑空消失」</p>
</li>
<li><p>丟棄僅標記 <code>error="limit"</code>，無 Error Log，難以察覺</p>
</li>
</ul>
<h3 id="heading-66">6.6 修正驗證 - 正確順序</h3>
<p>修改 <code>otel-config.yaml</code> 中的 processors 順序：</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># 修正前 (問題配置)</span>
<span class="hljs-attr">processors:</span> [<span class="hljs-string">cumulativetodelta</span>, <span class="hljs-string">batch</span>, <span class="hljs-string">deltatocumulative</span>]

<span class="hljs-comment"># 修正後 (正確配置)</span>
<span class="hljs-attr">processors:</span> [<span class="hljs-string">cumulativetodelta</span>, <span class="hljs-string">deltatocumulative</span>, <span class="hljs-string">batch</span>]
</code></pre>
<p><strong>修正後驗證結果：</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>指標</td><td>數值</td><td>說明</td></tr>
</thead>
<tbody>
<tr>
<td><code>streams_tracked</code></td><td><strong>50</strong></td><td>仍達到 max_streams 上限</td></tr>
<tr>
<td><code>receiver_accepted</code></td><td>17,500</td><td>Receiver 接收的總數據點</td></tr>
<tr>
<td><code>batch_send_size_sum</code></td><td><strong>14,950</strong></td><td>Batch 處理的數據點</td></tr>
<tr>
<td><code>exporter_sent</code></td><td><strong>14,950</strong></td><td>Exporter 實際發送的數據點</td></tr>
</tbody>
</table>
</div><p><strong>數據點處理詳情：</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>error 標籤</td><td>數值</td><td>說明</td></tr>
</thead>
<tbody>
<tr>
<td><code>error="none"</code></td><td>14,950</td><td>成功處理</td></tr>
<tr>
<td><code>error="limit"</code></td><td>2,490</td><td>因達到 Stream 上限而丟棄</td></tr>
</tbody>
</table>
</div><h3 id="heading-67">6.7 修正前後對比</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>指標</td><td>修正前</td><td>修正後</td><td>結論</td></tr>
</thead>
<tbody>
<tr>
<td>Batch 處理量</td><td>13,000</td><td>14,950</td><td>-</td></tr>
<tr>
<td>Exporter 發送量</td><td>11,000</td><td>14,950</td><td>-</td></tr>
<tr>
<td><strong>Batch = Exporter?</strong></td><td><strong>否 (差 2,000)</strong></td><td><strong>是 (完全一致)</strong></td><td><strong>問題解決</strong></td></tr>
<tr>
<td>error="limit"</td><td>3,000</td><td>2,490</td><td>仍有丟棄 (預期行為)</td></tr>
</tbody>
</table>
</div><p><strong>實驗結論：</strong></p>
<pre><code class="lang-sql">修正前: Batch (13,000) ≠ Exporter (11,000)  ← 數據「消失」，難以排查
修正後: Batch (14,950) = Exporter (14,950)  ← 指標一致，問題可追溯
</code></pre>
<ol>
<li><p><strong>Pipeline 順序修正有效</strong> - Batch 和 Exporter 的數值現在完全一致</p>
</li>
<li><p><strong>丟棄仍然發生</strong> (error="limit")，但這是<strong>預期行為</strong>，因為來源數 (60) &gt; max_streams (50)</p>
</li>
<li><p><strong>監控指標正確反映實際狀態</strong> - 運維人員可直接從 <code>error="limit"</code> 指標看到丟棄量</p>
</li>
</ol>
<h3 id="heading-68">6.8 清理環境</h3>
<pre><code class="lang-bash">docker compose down
</code></pre>
<hr />
<h2 id="heading-7-php">7. 番外篇：為什麼 PHP 開發者最需要它？</h2>
<h2 id="heading-71-php-the-share-nothing-architecture">7.1 為什麼是 PHP？ (The "Share-Nothing" Architecture)</h2>
<p>你可能會問，為什麼我們需要这么麻煩的轉 Delta？</p>
<p>對於 Java、Go 這種 Long-running process 來說，維護一個全域計數器（Cumulative）是輕而易舉的事。它們在記憶體中有一個全域的計數器，可以一直累加數值：</p>
<ul>
<li><p>00:01: 累計 10 次</p>
</li>
<li><p>00:02: 累計 15 次 (累加了 5 次)</p>
</li>
<li><p>00:03: 累計 20 次 (又累加了 5 次)</p>
</li>
</ul>
<p>這就是 Cumulative (累積) 模式，也是 Prometheus 等後端系統最喜歡的格式。</p>
<p>但是，PHP 通常運行在 PHP-FPM 或 CGI 模式下。其生命週期是「一個請求一個進程」 (Per-request process)：</p>
<ol>
<li>收到請求 -&gt; 啟動 (或重用) PHP worker。</li>
<li>執行腳本 -&gt; 處理指標 (Metrics)。</li>
<li>請求結束 -&gt; 記憶體釋放/重置。</li>
</ol>
<p>PHP (在 FPM 模式下) 是 Share-Nothing 架構。一個請求進來，Process 啟動，處理完，記憶體釋放。 PHP 進程沒辦法簡單地告訴下一個進程：「嘿，我剛剛處理了 1 個，現在總數是 100 喔。」(除非你用 Redis or SharedMemory等外部儲存)。</p>
<p>  因此，PHP 最自然的做法是只回報「這一次請求發生了什麼」：</p>
<ul>
<li>請求 A: 我處理了 1 個 DB 查詢 (Delta) -&gt; 結束</li>
<li><p>請求 B: 我處理了 1 個 DB 查詢 (Delta) -&gt; 結束</p>
<p>所以 PHP 最自然的行為是：「這次請求我處理了 1 個 DB Query (Delta)」，這就是 <code>Delta (增量)</code> 模式。
至於加總的工作？就交給 OTel Collector 的 deltatocumulative processor 來扛吧。這就是它存在的最大意義。</p>
</li>
</ul>
<h2 id="heading-72-delta-to-cumulative-processor">7.2 Delta to Cumulative Processor 的角色</h2>
<p>  當您的後端資料庫（如 Prometheus）只接受 Cumulative 資料，但您的應用程式（如 PHP 或 Serverless
  Functions）只能提供 Delta 資料時，就會發生格式不相容。</p>
<p>  這時候就需要 deltatocumulative processor 擔任「狀態管理者」 (Stateful Intermediary) 的角色：</p>
<ol>
<li>接收 (Receive): 它接收來自 PHP 的無數個小 Delta (例如：+1, +1, +1)。</li>
<li>記憶 (Remember): 它在 Collector 的記憶體中維護一個對應的 Stream，並幫忙做加法運算 (State Management)。</li>
<li>轉換 (Convert): 它算出累積值 (例如：目前總共是 3)，並將其轉換為 Cumulative 格式。</li>
<li>輸出 (Export): 發送給 Prometheus。</li>
</ol>
<p>雖然 Serverless (如 AWS Lambda) 或 CLI 工具也有類似需求，但 PHP的廣泛使用以及其標準的運行模式，使其成為這個 Processor 最常見的使用案例。</p>
<h2 id="heading-8">8. 結論與最佳實踐</h2>
<ol>
<li><p>Pipeline 順序至關重要：請務必把 deltatocumulative 放在 batch 之前。</p>
</li>
<li><p>監控不能少：一定要針對 error="limit" 和 error="out_of_order" 設告警。</p>
</li>
<li><p><strong>容量規劃</strong>: 根據預期的 Stream 數量合理設置 <code>max_streams</code></p>
</li>
<li><p><strong>Stale 設定</strong>: <code>max_stale</code> 應大於最大的 push/scrape interval (建議 2-3 倍)</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Project Layout for Go]]></title><description><![CDATA[剛入門任何一門程式語言開發的人, 應該大多都是參考各路大神們的專案或者公司的專案在學習模仿。 一開始印入眼簾的應該就是 Project布局的各種長相， Project布局關心的是我們怎樣組織 Go project。 這裡針對的是資料夾跟檔案的布局，官方Blog Organizing a Go module這篇文章有給我們建議跟說明。讓我們一起讀這篇吧！
在閱讀這篇之前能先稍微理解Go Module以及Go install的用法。

官方Blog Organizing a Go module這篇文...]]></description><link>https://blog.ganhua.wang/project-layout-for-go</link><guid isPermaLink="true">https://blog.ganhua.wang/project-layout-for-go</guid><category><![CDATA[Go Language]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Mon, 09 Oct 2023 06:48:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1696834024389/2b3fcccc-3845-40c3-8968-b5fee9fc9236.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>剛入門任何一門程式語言開發的人, 應該大多都是參考各路大神們的專案或者公司的專案在學習模仿。 一開始印入眼簾的應該就是 <strong>Project布局</strong>的各種長相， Project布局關心的是我們怎樣<strong>組織</strong> Go project。 這裡針對的是資料夾跟檔案的布局，<a target="_blank" href="https://go.dev/doc/modules/layout#multiple-packages">官方Blog Organizing a Go module</a>這篇文章有給我們建議跟說明。讓我們一起讀這篇吧！</p>
<p>在閱讀這篇之前能先稍微理解<a target="_blank" href="https://go.dev/blog/using-go-modules">Go Module</a>以及<a target="_blank" href="https://go.dev/ref/mod#go-install">Go install</a>的用法。</p>
<hr />
<p><a target="_blank" href="https://go.dev/doc/modules/layout#multiple-packages">官方Blog Organizing a Go module</a>這篇文章內把Go專案的內容大致分成Package和Command或者兩個的組合。 <code>Package</code> 就是我們熟知的library, <code>Command</code> 則是executable能編譯出來執行的go檔案。</p>
<p>演化路程如下圖</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696833947497/e810dde0-db53-404c-9985-8e3ec5cf277d.gif" alt class="image--center mx-auto" /></p>
<p>如果是純library專案給人家import到別人的go project內使用的, 那package這條路線就是我們能參考。 像這個<a target="_blank" href="https://github.com/ahmetb/go-linq">go-linq</a>，該專案都是library。<a target="_blank" href="https://github.com/redis/go-redis">go-redis</a>也是這類型的專案。</p>
<p>如果是需要安裝到主機上變成執行檔的，那command這條路線能參考。 像<a target="_blank" href="https://github.com/protocolbuffers/protobuf-go">protoc-gen-go</a>。</p>
<h2 id="heading-basic-package">Basic package</h2>
<p>下面的目錄結構長相是go module和package code都在專案的根目錄中。</p>
<pre><code class="lang-bash">project-root-directory/
  go.mod
  modname.go
  modname_test.go
</code></pre>
<p>上面的 <code>modname.go</code> 是package code，<code>modname_test.go</code> 則是與modname.go相關的測試程式。</p>
<p>如果把這包程式碼上傳到Github repo上，例如 <code>github.com/someuser/modname</code>，那麼 <code>go.mod</code> 裡指定該module的路徑也會是 <code>github.com/someuser/modname</code>，這點之後有機會在介紹go module的細節。</p>
<pre><code class="lang-arduino">module github.com/someuser/modname
</code></pre>
<p>然後因為package code在根目錄，所以沒意外這package code一開始的package name會是 <code>modname</code></p>
<pre><code class="lang-bash">package modname
</code></pre>
<p>這樣別人就能透過 <code>go get</code> <a target="_blank" href="http://github.com/someuser/modname%E4%BE%86%E5%8F%96%E5%BE%97%E9%80%99%E5%85%AC%E9%96%8B%E7%9A%84package%E3%80%82"><code>github.com/someuser/modname</code> 來取得這公開的package。</a></p>
<p>也可以該package的程式碼能切分開來在多個檔案中，只要這些檔案都在同一層的目錄中。</p>
<pre><code class="lang-bash">project-root-directory/
  go.mod
  modname.go
  modname_test.go
  auth.go
  auth_test.go
  hash.go
  hash_test.go
</code></pre>
<p>這些檔案除了在同一層目錄中，同時它們的package name也都是modname。</p>
<h2 id="heading-basic-command">Basic command</h2>
<p>如果是要可以執行的程式，那最基本的專案結構會長的像</p>
<pre><code class="lang-bash">project-root-directory/
  go.mod
  auth.go
  auth_test.go
  client.go
  main.go
</code></pre>
<p>其中 <code>main.go</code> 會包含 <code>func main</code>，這應該沒什麼問題，雖然檔名也能叫 <code>modname.go</code>，只是main.go比較是慣例命名。 這目錄中所有的檔案其pakcage name也會是 <code>main</code>，畢竟go同一層目錄的package name必須一樣否則編譯會出錯。</p>
<p><a target="_blank" href="http://xn--github-9o7ia342d47drxg322a2m0bo02g6tf.com/someuser/modname">同上該專案被上傳到 <code>github.com/someuser/modname</code></a></p>
<pre><code class="lang-arduino">module github.com/someuser/modname
</code></pre>
<p>那麼我們就能夠透過 <code>go install</code> 來下載並安裝</p>
<pre><code class="lang-ruby">go install github.com/someuser/modname@latest
</code></pre>
<p>只是通常這種可執行的專案沒那麼單純，我們會有一些程式碼不希望發布上git後被別人的專案給go get來使用那些package。這時就出現 <code>interanl</code> 資料夾了。</p>
<h3 id="heading-internal-package">Internal package</h3>
<p>Go在1.4時加入了<a target="_blank" href="https://go.dev/doc/go1.4#internalpackages">internal package</a>的機制。 internal package除了不能讓外面的用戶直接import之外，代表著對其他module來說，該internal package對它們來說是<code>不可見</code>的，所以能做一層隔離，interanl package內程式的修改對外部是沒影響的</p>
<p>同時也只能被<code>同一個階層以下</code>的所有package來存取，所以絕大部分internal package都被放在根目錄，就是希望讓根目錄中所有package都能去導入 internal package。</p>
<pre><code class="lang-bash">project-root-directory/
  go.mod
  main.go
  package1/
    internal/
      interal.go
    package1.go
  package2/
    internal/
      interal.go
    package2.go
  interanl/
    version/
      version.go
    internal.go
</code></pre>
<p>這樣的定義下，package1的interanl只有package1這層以下的能導入，package2的interanl也是，連main都無法導入到這兩個interanl package。 但是main可以導入根目錄下的internal package。 package1能夠導入根目錄下的interanl package。</p>
<p><img src="https://hackmd.io/_uploads/By1UxMbWT.png" alt /></p>
<p>如果硬要import，則編譯時會出現編譯錯誤 <code>use of internal package xxxx/internal not allowed</code></p>
<p>至於如果package1剛好要導入root internal package, 都要給導入internal package一個別名, 才能正常使用；除非引用的是internal package的下一層package。 如導入上面目錄結構的 <code>internal/version</code>, 就沒問題。 如果是剛好想要導入internal package的internal.go的部份就要給上alias別名。</p>
<h2 id="heading-package-or-command-with-supporting-packages">Package or command with supporting packages</h2>
<p>有了internal package的認識後，就能將interanl package理解成supporting package，就是我們內部會引用到，但不希望被外部專案給直接引用的package。這時候Go官方就會建議我們放到 <code>internal</code> 目錄中。</p>
<pre><code class="lang-bash">project-root-directory/
  internal/
    auth/
      auth.go
      auth_test.go
    <span class="hljs-built_in">hash</span>/
      hash.go
      hash_test.go
  go.mod
  modname.go
  modname_test.go
</code></pre>
<p>在這樣目的下，我們就能設計modname.go，能導入auth package和hash package。 modname.go能這樣導入</p>
<pre><code class="lang-bash">import <span class="hljs-string">"github.com/someuser/modname/internal/auth"</span>
</code></pre>
<p>但外部只能導入modname package來使用。</p>
<h2 id="heading-multiple-packages">Multiple packages</h2>
<p>一個專案可以有多個能被導入的package，且每個package都會有自己的目錄結構來進行組織。</p>
<pre><code class="lang-bash">project-root-directory/
  go.mod
  modname.go
  modname_test.go
  auth/
    auth.go
    auth_test.go
    token/
      token.go
      token_test.go
  <span class="hljs-built_in">hash</span>/
    hash.go
  internal/
    trace/
      trace.go
</code></pre>
<p>這樣子的結構意味著，別人可以導入</p>
<pre><code class="lang-bash">github.com/someuser/modname
github.com/someuser/modname/auth
github.com/someuser/modname/<span class="hljs-built_in">hash</span>
</code></pre>
<p>也能導入auth package的下一層token package</p>
<pre><code class="lang-bash">github.com/someuser/modname/auth/token
</code></pre>
<p>外部專案就是不能導入</p>
<pre><code class="lang-bash">github.com/someuser/modname/internal/trace
</code></pre>
<p>在這專案結構設計上，剛好interanl package放在根目錄中，就是為了能讓專案內的package給導入使用。 例如專案內的package就能導入trace package使用</p>
<pre><code class="lang-bash">github.com/someuser/modname/internal/trace
</code></pre>
<h2 id="heading-multiple-commands">Multiple commands</h2>
<p>一個專案能包含多個可執行的程序。像是例子中的prog1/main.go和prog2/main.go</p>
<pre><code class="lang-bash">project-root-directory/
  go.mod
  internal/
    ... shared internal packages
  prog1/
    main.go
  prog2/
    main.go
</code></pre>
<p>所以使用者可以透過go install來直接安裝使用</p>
<pre><code class="lang-ruby">$ go install github.com/someuser/modname/prog1@latest
$ go install github.com/someuser/modname/prog2@latest
</code></pre>
<p>但是為了好區別可執行command的目錄名稱，通常會建議放入一個名為 <code>cmd</code> 的目錄內，這樣子會很好區分出哪些是可以被導入的package哪些則是可以被執行的command。這些都是建議，並非必要。</p>
<h2 id="heading-packages-and-commands-in-the-same-repository">Packages and commands in the same repository</h2>
<p>如果一個專案同時提供可以被導入的package和能夠被安裝執行的command，通常會像這樣的結構。</p>
<pre><code class="lang-bash">project-root-directory/
  go.mod
  modname.go
  modname_test.go
  auth/
    auth.go
    auth_test.go
  internal/
    ... internal packages
  cmd/
    prog1/
      main.go
    prog2/
      main.go
</code></pre>
<p>這樣子的結構意味著，別人可以導入</p>
<pre><code class="lang-bash">github.com/someuser/modname
github.com/someuser/modname/auth
</code></pre>
<p>能透過go install來安裝cmd/prog1和cmd/prog2</p>
<pre><code class="lang-ruby">$ go install github.com/someuser/modname/cmd/prog1@latest
$ go install github.com/someuser/modname/cmd/prog2@latest
</code></pre>
<p>以<a target="_blank" href="https://github.com/prometheus/prometheus">Prometheus</a>這專案為例子</p>
<pre><code class="lang-bash">github.com/prometheus/prometheus/
  cmd/
    prometheus/
      main.go
    promtool/
      main.go
  internal/
  model/
  plugins/
  rules/
  scrape/
  scripts/
  storage/
    interface.go
</code></pre>
<p>Prometheus提供了prometheus能安裝啟動，還提供了promtool這工具，能讓我們安裝來對prometheus進行檢查還有檢查各種prometheus設定文件等作用。</p>
<p>又因為prometheus本身只提供local storage功能, 所以在stoage package內有定義了interace來給其他storage service來導入開發用。 像是Grafana Lab旗下的<a target="_blank" href="https://github.com/grafana/mimir/tree/80ee3c17ee134541904fcefc7e9b066b6288348d">Mimir</a>，內部就導入很多Prometheus專案內的package。</p>
<p>但你不會看到Mimir去導入prometheus/internal底下的package，因為這些只能被prometheus專案內部所導入。</p>
<h2 id="heading-57i957wq">總結</h2>
<p>官方這篇文章主要針對專案根目錄，internal目錄做說明，cmd目錄則是建議。 如果我們的專案需要提供一堆package供別專案導入時，有人也是會建議集中放到 <code>pkg</code> 目錄中，讓根目錄乾淨點，但也只是建議。</p>
<p>對我來說目錄結構反應的也是設計時的一個邊界，怎樣探索出邊界，不是按照這篇<a target="_blank" href="https://github.com/golang-standards/project-layout">Project布局</a>來被強迫設計就這樣塞入，該repo的<a target="_blank" href="https://github.com/golang-standards/project-layout/issues/117">issue</a>也在討論這份也不是標準Go專案布局。</p>
<p>更多的我想還是回歸場景與需求的設計，像是DDD中提到的subdomain與bounded context, 就是基於業務層面的設計布局，在基於這樣業務布局進行專案結構的布局設計，這樣能讓專案設計貼近於業務設計，減低認知與轉化負擔。</p>
<p>令一個層面是關於release，如果該專案內有些package對於其他專案項目有用處，為了方便管理發佈與版本控制，官方是建議拆分成獨立的專案進行管理。只有跟我們這包系統有緊密生命週期相關的package和command才集中在一個專案內，方便開發和部署。</p>
<h1 id="heading-5yd6icd6loh5paz">參考資料</h1>
<p><a target="_blank" href="https://go.dev/doc/modules/layout">Go doc Organizing a Go module</a></p>
<p><a target="_blank" href="https://go.dev/doc/go1.4#internalpackages">Go doc go1.4 Internal packages</a></p>
<p><a target="_blank" href="https://tonybai.com/2023/10/05/the-official-guide-of-organizing-go-project/">Go 项目目录该怎么组织？官方终于出指南了！</a></p>
]]></content:encoded></item><item><title><![CDATA[Common mistakes with for loops in Go]]></title><description><![CDATA[先來一段程式
func main() {
    done := make(chan bool)

    values := []string{"a", "b", "c"}
    for _, v := range values {
        /*
        在每次loop中，此程式碼會啟動一個Goroutine來執行匿名函數。
        該函數將當前的值v輸出出來，然後將true發送到done通道。
        */
        go func() {
     ...]]></description><link>https://blog.ganhua.wang/common-mistakes-with-for-loops-in-go</link><guid isPermaLink="true">https://blog.ganhua.wang/common-mistakes-with-for-loops-in-go</guid><category><![CDATA[Go Language]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Wed, 04 Oct 2023 17:39:47 GMT</pubDate><content:encoded><![CDATA[<p>先來一段程式</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    done := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">bool</span>)

    values := []<span class="hljs-keyword">string</span>{<span class="hljs-string">"a"</span>, <span class="hljs-string">"b"</span>, <span class="hljs-string">"c"</span>}
    <span class="hljs-keyword">for</span> _, v := <span class="hljs-keyword">range</span> values {
        <span class="hljs-comment">/*
        在每次loop中，此程式碼會啟動一個Goroutine來執行匿名函數。
        該函數將當前的值v輸出出來，然後將true發送到done通道。
        */</span>
        <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
            fmt.Println(v)
            done &lt;- <span class="hljs-literal">true</span>
        }()
    }

    <span class="hljs-comment">// wait for all goroutines to complete before exiting</span>
    <span class="hljs-keyword">for</span> _ = <span class="hljs-keyword">range</span> values {
        &lt;-done
    }
}
</code></pre>
<p>直覺上應該會輸出<code>a,b,c</code>。 但因為Gorouting可能在loop的下一次迭代之後才執行，所以v的值可能已經改變。因此，所有Goroutines可能都會輸出相同的值<code>c</code>，而不是按順序輸出每個值。</p>
<p>再者是因為Go的for loop那個v, 其實是共用變數, 所以記憶體位置也是一樣的, 讓我們驗證看看。</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    done := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">bool</span>)

    values := []<span class="hljs-keyword">string</span>{<span class="hljs-string">"a"</span>, <span class="hljs-string">"b"</span>, <span class="hljs-string">"c"</span>}
    <span class="hljs-keyword">for</span> _, v := <span class="hljs-keyword">range</span> values {
        <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
            fmt.Printf(<span class="hljs-string">"value=%s, addr=%p\n"</span>, v, &amp;v)
            done &lt;- <span class="hljs-literal">true</span>
        }()
    }

    <span class="hljs-comment">// wait for all goroutines to complete before exiting</span>
    <span class="hljs-keyword">for</span> _ = <span class="hljs-keyword">range</span> values {
        &lt;-done
    }
}

<span class="hljs-comment">/*
value=c, addr=0xc000014070
value=c, addr=0xc000014070
value=c, addr=0xc000014070
*/</span>
</code></pre>
<p>解法有幾種, 第一種, 進到迭代時, 就把值給複製一份。</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    done := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">bool</span>)

    values := []<span class="hljs-keyword">string</span>{<span class="hljs-string">"a"</span>, <span class="hljs-string">"b"</span>, <span class="hljs-string">"c"</span>}
    <span class="hljs-keyword">for</span> _, v := <span class="hljs-keyword">range</span> values {
        v1 := v
        <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
            fmt.Println(v1)
            done &lt;- <span class="hljs-literal">true</span>
        }()
    }

    <span class="hljs-comment">// wait for all goroutines to complete before exiting</span>
    <span class="hljs-keyword">for</span> _ = <span class="hljs-keyword">range</span> values {
        &lt;-done
    }
}
</code></pre>
<p>第二種, 進到closure時, 就把值給傳入closure做一份參數的複製。</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    done := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">bool</span>)

    values := []<span class="hljs-keyword">string</span>{<span class="hljs-string">"a"</span>, <span class="hljs-string">"b"</span>, <span class="hljs-string">"c"</span>}
    <span class="hljs-keyword">for</span> _, v := <span class="hljs-keyword">range</span> values {
        <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(v <span class="hljs-keyword">string</span>)</span></span> {
            fmt.Println(v)
            done &lt;- <span class="hljs-literal">true</span>
        }(v)
    }

    <span class="hljs-comment">// wait for all goroutines to complete before exiting</span>
    <span class="hljs-keyword">for</span> _ = <span class="hljs-keyword">range</span> values {
        &lt;-done
    }
}
</code></pre>
<p>面試蠻常問的題目XD 但其實Go有提供tool來做靜態程式碼掃描, <a target="_blank" href="https://pkg.go.dev/cmd/vet"><code>go vet</code></a></p>
<p>go tool vet提供這麼多面向的check, 其中<code>loopclosure check references to loop variables from within nested functions</code>就是能掃描出上述的議題。</p>
<pre><code class="lang-plaintext">To list the available checks, run "go tool vet help":
* asmdecl      report mismatches between assembly files and Go declarations
* assign       check for useless assignments
* atomic       check for common mistakes using the sync/atomic package
* bools        check for common mistakes involving boolean operators
* buildtag     check that +build tags are well-formed and correctly located
* cgocall      detect some violations of the cgo pointer passing rules
* composites   check for unkeyed composite literals
* copylocks    check for locks erroneously passed by value
* httpresponse check for mistakes using HTTP responses
* loopclosure  check references to loop variables from within nested functions
* lostcancel   check cancel func returned by context.WithCancel is called
* nilfunc      check for useless comparisons between functions and nil
* printf       check consistency of Printf format strings and arguments
* shift        check for shifts that equal or exceed the width of the integer
* slog         check for incorrect arguments to log/slog functions
* stdmethods   check signature of methods of well-known interfaces
* structtag    check that struct field tags conform to reflect.StructTag.Get
* tests        check for common mistaken usages of tests and examples
* unmarshal    report passing non-pointer or non-interface values to unmarshal
* unreachable  check for unreachable code
* unsafeptr    check for invalid conversions of uintptr to unsafe.Pointer
* unusedresult check for unused results of calls to some functions
</code></pre>
<p>能透過<code>go tool bet help</code>能看看使用方法。 讓我來透過<code>go tool vet</code>來掃描看看</p>
<pre><code class="lang-bash">go vet main.go
或者
go vet -loopclosure main.go

/*
<span class="hljs-comment"># command-line-arguments</span>
./main.go:11:38: loop variable v captured by func literal
./main.go:11:42: loop variable v captured by func literal
*/
</code></pre>
<p>看到<code>loop variable v captured by func literal</code>就是告訴你第11行的v這個loop共享變數,正在被closure function給使用。 當我們改成上面的幾個寫法後，再執行<code>go vet</code>就不會看到上述警告了。</p>
<p>好，上面的寫法還算好察覺到。 接著來個很不容易察覺到的寫法，也會踩到這地雷。 <a target="_blank" href="https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable">參考</a></p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> <span class="hljs-string">"fmt"</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">var</span> out []*<span class="hljs-keyword">int</span>
    <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">3</span>; i++ {
        out = <span class="hljs-built_in">append</span>(out, &amp;i)
    }
    fmt.Println(<span class="hljs-string">"Values:"</span>, *out[<span class="hljs-number">0</span>], *out[<span class="hljs-number">1</span>], *out[<span class="hljs-number">2</span>])
    fmt.Println(<span class="hljs-string">"Addresses:"</span>, out[<span class="hljs-number">0</span>], out[<span class="hljs-number">1</span>], out[<span class="hljs-number">2</span>])
}

<span class="hljs-comment">/*
Values: 3 3 3
Addresses: 0xc0000120e8 0xc0000120e8 0xc0000120e
*/</span>
</code></pre>
<p>咦, 這次沒closure function了還會這樣? 試試看go vet! <code>go vet main.go</code></p>
<p>啥都沒跑出來, 咦? 明明結果不如預期，go vet卻覺得沒問題! (拉G go vet呸?)</p>
<p>其實這裡for loop每次迭代的<code>i</code>,也都是指向同一個共享變數; 然後我們還做死, 取址&amp;i, 新增進去out這ptr slice中。 i在最後一次迭代時真正指向的值是3, 只是地址都是同一個, 所以最後輸出才都會是3</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696441951810/18db8409-92d1-46fa-b9c2-38faabfecbd9.png" alt class="image--center mx-auto" /></p>
<p>來看看<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1619047">publicly documented issue at Lets Encrypt</a> 內討論的一段程式碼</p>
<pre><code class="lang-go"><span class="hljs-comment">// authz2ModelMapToPB converts a mapping of domain name to authz2Models into a</span>
<span class="hljs-comment">// protobuf authorizations map</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">authz2ModelMapToPB</span><span class="hljs-params">(m <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]authz2Model)</span> <span class="hljs-params">(*sapb.Authorizations, error)</span></span> {
    resp := &amp;sapb.Authorizations{}
    <span class="hljs-keyword">for</span> k, v := <span class="hljs-keyword">range</span> m {
        <span class="hljs-comment">// Make a copy of k because it will be reassigned with each loop.</span>
        kCopy := k
        authzPB, err := modelToAuthzPB(&amp;v)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
        }
        resp.Authz = <span class="hljs-built_in">append</span>(resp.Authz, &amp;sapb.Authorizations_MapElement{
            Domain: &amp;kCopy,
            Authz: authzPB,
        })
    }
    <span class="hljs-keyword">return</span> resp, <span class="hljs-literal">nil</span>
}
</code></pre>
<p>ln7做了一行k的copy, 因為如果他不在這裡做copy, 此時的k其實也是共享變數, 在ln13的地方用&amp;k的話會發生上面不如預期的錯誤。</p>
<p>Go1.22終於要面對這常見的陷阱，<a target="_blank" href="https://go.dev/blog/loopvar-preview?fbclid=IwAR1SuAoeSKTBYQ4wK3SjnWWxs99NgdzFztAqZ6QnztTTB7vuGsEInz7rbq4">Fixing For Loops in Go 1.22</a></p>
<p>但現在<code>1.22</code>還沒正式release阿? 不怕文章內有說<code>1.21</code>有提供這新功能的<code>Preview</code> 只要加上<code>GOEXPERIMENT=loopvar</code></p>
<pre><code class="lang-bash">GOEXPERIMENT=loopvar  go run main.go

/*
Values: 0 1 2
Addresses: 0xc0000120e8 0xc000012110 0xc000012118
*/
</code></pre>
<p>完美!</p>
<p>總結一下, 在Go1.21之前的版本, 只要for loop有對迭代變數取址, 或者for+closure時, code review能相互提醒。 在CI或透過husky這git hook利用<code>go vet</code>掃出這些低級錯誤先。 但Go 1.22我覺得是很值得升級的版本，因為這地雷真的太常踩到了。</p>
<p>參考資料:</p>
<p><a target="_blank" href="https://go.dev/blog/loopvar-preview?fbclid=IwAR1SuAoeSKTBYQ4wK3SjnWWxs99NgdzFztAqZ6QnztTTB7vuGsEInz7rbq4">Fixing For Loops in Go 1.22</a></p>
<p><a target="_blank" href="https://go.dev/doc/faq#closures_and_goroutines">What happens with closures running as goroutines?</a></p>
<p><a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1619047">Let's Encrypt: CAA Rechecking bug</a></p>
<p><a target="_blank" href="https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable">CommonMistakes</a></p>
]]></content:encoded></item><item><title><![CDATA[Go是System Programming Language還是OOP?]]></title><description><![CDATA[同步分享於MicroFIRE
什麼是System Programming Language
System Programming Language 主要用於底層系統開發，例如操作系統、系統工具、驅動程式等。這類語言具有以下特點：

緊密地與硬體交互：系統程式語言可以直接訪問硬體資源，例如記憶體、CPU 和外部設備，使其更適合底層系統開發。

更低的抽象層次：相比其他高階程式語言，系統程式語言設計時並沒有將底層細節過度隱層，因此開發者需要對底層硬體和系統有較深入的了解; 例如可以直接管理記憶體(m...]]></description><link>https://blog.ganhua.wang/gosystem-programming-languageoop</link><guid isPermaLink="true">https://blog.ganhua.wang/gosystem-programming-languageoop</guid><category><![CDATA[Go Language]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Sat, 08 Apr 2023 14:23:12 GMT</pubDate><content:encoded><![CDATA[<p><a target="_blank" href="https://microfire.hashnode.dev/gosystem-programming-languageoop">同步分享於MicroFIRE</a></p>
<h2 id="heading-system-programming-language">什麼是System Programming Language</h2>
<p>System Programming Language 主要用於<strong>底層系統開發</strong>，例如操作系統、系統工具、驅動程式等。這類語言具有以下特點：</p>
<ul>
<li><p>緊密地與硬體交互：系統程式語言可以直接訪問硬體資源，例如記憶體、CPU 和外部設備，使其更適合底層系統開發。</p>
</li>
<li><p>更低的抽象層次：相比其他高階程式語言，系統程式語言設計時並沒有將底層細節過度隱層，因此開發者需要對底層硬體和系統有較深入的了解; 例如可以直接管理記憶體(malloc, free), 進行unsafe的指標操作等等的。</p>
</li>
<li><p>高性能和高效率：由於系統程式語言通常具有較低的抽象層次，因此它們可以更有效地利用系統資源，達到更高的性能。</p>
</li>
<li><p>程式碼可移植性：系統編程語言通常具有較高的可移植性，可以在不同平臺和操作系統上運行。</p>
</li>
<li><p>程式設計風格：系統程式語言通常使用程序式（Procedural）或命令式（Imperative）程式風格，強調程序的流程和步驟。</p>
</li>
</ul>
<p>常見的系統程式語言包括：C、C++、Rust 和 Go 等。這些語言在底層系統開發領域具有廣泛的應用，並且在性能和效率方面具有優勢。</p>
<h2 id="heading-object-oriented-programming">什麼是Object-Oriented Programming</h2>
<p>物件導向語言（Object-Oriented Programming）是一種程式設計風格，強調使用物件（或稱Class）來組織和執行程式。在物件導向語言中，開發者將資料(屬性)和可操作的方法封裝到物件中，這些物件間可以相互交互，以達到特定的任務。面向對象編程的主要特點包括：</p>
<ul>
<li><p>封裝性：物件將資料(屬性)和方法封裝在一起，可以控制對資料的訪問和修改(隱藏實踐細節)，從而實現更好的安全性和可維護性。</p>
</li>
<li><p>繼承性：物件可以通過繼承擴展其屬性和方法，減少程式碼的重複，提高程式碼的可重用性。</p>
</li>
<li><p>多型性：同一個方法可以在不同物件上實現出不同的實際行為(Polymorphism)。</p>
</li>
<li><p>抽象性：物件導向語言支持抽象類別和介面(一種本質/流程分類的呈現)，可以提高程式的模組化和可重用性。</p>
</li>
</ul>
<p>常見的物件導向語言包括 Java、C++、Python、C#、Ruby 等。這些語言通常提供了豐富的面向物件特性和方便使用的library，使開發者可以更方便地實現物件導向分析與設計(OOAD)。</p>
<h2 id="heading-go">Go</h2>
<h3 id="heading-is-go-an-oop">Is Go an OOP?</h3>
<p>在Go doc中<a target="_blank" href="https://go.dev/doc/faq#Is_Go_an_object-oriented_language">官方給出的回應</a>是</p>
<blockquote>
<p>Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general. There are also ways to embed types in other types to provide something analogous—but not identical—to subclassing. Moreover, methods in Go are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, “unboxed” integers. They are not restricted to structs (classes).</p>
<p>Also, the lack of a type hierarchy makes “objects” in Go feel much more lightweight than in languages such as C++ or Java.</p>
</blockquote>
<p>Go不完全滿足OOP喔!</p>
<p>沒類別繼承, 但我們可以透過embedding type(嵌入)的方法來做出有點點像"子"類別的東西.</p>
<p>但能透過Composition來讓interface來組合令一個interface.</p>
<p>但這都不是繼承!</p>
<p>多型性Polymorphism, Go在1.18以前完全不支援, 直到1.18開始才支援了基礎類型的Generic type. (<a target="_blank" href="https://go.dev/doc/faq#generics">FAQ連結</a>); 為什麼不支援, 就是為了高性能(參考系統語言)</p>
<h3 id="heading-go-1">Go是系統程式語言?</h3>
<p><strong>是!</strong></p>
<p>在FAQ中有一題問的是<a target="_blank" href="https://go.dev/doc/faq#ancestors">Go的祖先是誰?</a></p>
<blockquote>
<p>Go is mostly in the C family (basic syntax), with significant input from the Pascal/Modula/Oberon family (declarations, packages), plus some ideas from languages inspired by Tony Hoare's CSP, such as Newsqueak and Limbo (concurrency). However, it is a new language across the board. In every respect the language was designed by thinking about what programmers do and how to make programming, at least the kind of programming we do, more effective, which means more fun.</p>
</blockquote>
<p>接著來看看Go最初要<a target="_blank" href="https://go.dev/talks/2012/splash.article#TOC_4.">解決Google在用C/C++構建分散式系統時的痛點</a></p>
<blockquote>
<ul>
<li><p>slow builds</p>
</li>
<li><p>uncontrolled dependencies</p>
</li>
<li><p>each programmer using a different subset of the language</p>
</li>
<li><p>poor program understanding (code hard to read, poorly documented, and so on)</p>
</li>
<li><p>duplication of effort</p>
</li>
<li><p>cost of updates</p>
</li>
<li><p>version skew</p>
</li>
<li><p>difficulty of writing automatic tools</p>
</li>
<li><p>cross-language builds</p>
</li>
</ul>
</blockquote>
<p>沒一點是為了要快速開發業務系統用的.</p>
<p>都是為了實踐<strong>現代化大型雲端原生系統</strong>而設計的語言.</p>
<h2 id="heading-reference">Reference</h2>
<p><a target="_blank" href="https://go.dev/talks/2012/splash.article">Go at Google: Language Design in the Service of Software</a></p>
<p><a target="_blank" href="https://go.dev/doc/faq">Frequently Asked Questions (FAQ)</a></p>
]]></content:encoded></item><item><title><![CDATA[淺談Observability]]></title><description><![CDATA[Observability的歷史
(又在講古了XD) Observability這詞, 最早在控制理論出現, 這是一門關注如何控制動態系統的工程學科, 其中的Rudolf E. Kálmán在1960的論文中提出的.

Observability to describe mathematical control systems. In control theory, observability is defined as a measure of how well internal states...]]></description><link>https://blog.ganhua.wang/observability</link><guid isPermaLink="true">https://blog.ganhua.wang/observability</guid><category><![CDATA[observability]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Mon, 27 Mar 2023 01:52:38 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-observability">Observability的歷史</h2>
<p>(又在講古了XD) <strong>Observability</strong>這詞, 最早在<a target="_blank" href="https://en.wikipedia.org/wiki/Control_theory">控制理論</a>出現, 這是一門關注如何控制動態系統的工程學科, 其中的<a target="_blank" href="https://www.sciencedirect.com/science/article/pii/S1474667017700948">Rudolf E. Kálmán在1960的論文</a>中提出的.</p>
<blockquote>
<p>Observability to describe mathematical control systems. In control theory, observability is defined as a measure of how well internal states of a system can be inferred from knowledge of its external outputs.</p>
</blockquote>
<p>恩...大概就根據一個系統的外部輸出的資訊來推斷內部狀態的能力,甚至可以透過輸出的資訊搭配知識來做出推斷假設與證明. 進而優化再持續性的觀測, 是不能稱為系統具備可觀測性的, 因為這樣子組織會高度依賴少部份人的能力而帶來管理風險.</p>
<p><img src="https://ithelp.ithome.com.tw/upload/images/20220904/20104930NtEuKNpTjR.png" alt="https://ithelp.ithome.com.tw/upload/images/20220904/20104930NtEuKNpTjR.png" /></p>
<p>這也呼應了DevOps的8字環.</p>
<p><img src="https://www.tibco.com/sites/tibco/files/media_entity/2021-04/Dev-ops-01.svg" alt /></p>
<p>Kálmán後來有把這論文的理論, 用在軟體系統上, 並且給出了更詳細的要求, 只有滿足了才可稱為具備Observability.</p>
<ol>
<li><p>了解應用程式的內部運作</p>
</li>
<li><p>了解你的應用程式可以進入任何的系統狀態, 甚至你以前都沒見過也無法預測的新狀態</p>
</li>
<li><p>僅透過觀察與存取外部工具來了解軟體的內部運作與系統狀態</p>
</li>
<li><p>了解內部狀態, 不需要提供任何新的自定義程式來處理解析它.</p>
</li>
</ol>
<p><a target="_blank" href="https://www.oreilly.com/library/view/observability-engineering/9781492076438/">Observability Engineering</a>這本書的第一章, 有提出很多小問題來詢問自己是否夠了解自己的系統. 我隨便舉兩三題 1.是否曾經遇過線上問題, 自己都能解釋異常狀況, 沒碰到過排查的死胡同, 最後只能靠通靈跟重開機. 2.如果有用戶抱怨系統很慢, 但你的監控面板顯示99th, 99.9th甚至99.99th的請求都很快, 能找到隱藏的超時嘛?</p>
<p>之類的很多問題XD</p>
<p><strong>1988年</strong>, SNMPv1定義在<a target="_blank" href="https://www.rfc-editor.org/rfc/rfc1157">RFC1157</a>, 其中提到了大家很熟悉的Monitoring. 其中有<strong>Metric度量指標</strong>,它是一個數字, 跟隨著一些<strong>tag</strong>, 我們就能用這些tag對這些metrics的數字做一些分類和搜尋. 通常這些metric指標都是一次性產生、廉價的存儲空間. 但它們又通常會用Time-Series bucket(時間序列集合)來聚合存放. 現在有很多複雜的應用或組織運作都建立在Metric指標上, 像是Time-Series databases(TSDBs)、一些統計分析、圖形儀表板、運維團隊、On-Call輪轉等等的, 很多方式能展示跟告知團隊. 但這些總有上限, 這上限在這些metric指標都是自己對系統已知的資訊. 平常跑得很正常的系統, 突然壞了, metric指標也不出來時, 大部分的運維團隊就會用low-level的指令來查找問題, 像是strace, tcpdump等等數百條命令來嘗試讓主機來回答問題.</p>
<p>且現在的系統架構往往是</p>
<ol>
<li><p>一個應用系統有多組服務來支撐, 換言之系統很複雜</p>
</li>
<li><p>有很多種的持久化服務(像是DB, storage systems)</p>
</li>
<li><p>Infrastructure是很動態地, 像容量就任意的伸縮</p>
</li>
<li><p>許多服務是我們有依賴, 但不歸屬我們做管理控制</p>
</li>
<li><p>分散式系統是難以被完整預測的</p>
<p> <img src="https://i.imgur.com/R7Ckjve.png" alt /></p>
</li>
</ol>
<p>還有很多原因, 將導致團隊在治理跟排查上遭遇很大的困難. 若是沒有一個高維度(Dimensionality)和高基數(Cardinality)資料, 貫穿在眾多線索中, 將會非常難在現在這樣複雜的系統架構裡快速地找到問題, 甚至證明假設.</p>
<h3 id="heading-cardinality">基數Cardinality</h3>
<p>在關聯式資料庫裡, <strong>Cardinality</strong>指的是資料的唯一性. Low-cardinality表示一列有很多重複的值. 性別/姓名都算是低基數. High-cardinality意味著該列包含很大比例的唯一值. 所以像UniqueID, UUID這樣的值在該列則能稱為高基數.</p>
<p><img src="https://i.imgur.com/gI2ZWeT.png" alt /></p>
<p>Cardinality對於Observability是非常重要的, 因為高基數的資料在Debug或了解系統的資料識別上是最有用有效的. 像是UserIDs, ShoppingCartIDs, RequestIDs其他等等的高基數ID. 又或是我們之後要介紹的TraceIDs, SpanIDs, 或是容器ID, 主機名稱都是查詢唯一ID的好方法. 高基數資料的好處除了方便識別外, 也能轉化成低基數的資料, 像是分類分堆, 資料前綴/後綴等等的; 但低基數的資料卻不能反過來轉化成高基數的資料.</p>
<p>但是!! 大部分基於Metrics的工具或服務(像Prometheus), 基本都是處理低基數的資料. 通常低基數的資料都是要是前先決定好的. 但通常很多人事先不太清楚哪些指標做度量的, 或哪些標籤是需要做識別.</p>
<h3 id="heading-dimensionality">維度Dimensionality</h3>
<p>基數講的是資料的唯一性. 維度講的是該資料所擁有key(屬性)的數量. 在Observability System中, Telemetry(遙測)數據是由任意寬度(Wide)的結構化事件所生成的. 這些事件能被稱為Wide, 是因為它們可以包含上百甚至上千個Key-value pair(或者叫維度Dimensionality).</p>
<p><img src="https://i.imgur.com/ienjRfT.png" alt /></p>
<p>圖上的P是特徵, n則是metric資料. 盡可能描述多點特徵. 統計學對於高維度資料有嚴格的定義[^1].</p>
<p>像之後要介紹的Prometheus, 其metric資料裡面能含有任意數量的k-v pairs; 結構化Log其內容也是有很多k-v pair來描述其上下文的資訊, 便於觀察了解系統.</p>
<h3 id="heading-context">上下文Context</h3>
<p>所有的Signal(之後會解釋), 都有相同的內容. 其實就是在空間上建立資料之間的關聯.</p>
<p>舉例, Prometheus[^2]首頁就有這段描述</p>
<p><img src="https://i.imgur.com/HzRTuPy.png" alt="https://prometheus.io/" /></p>
<p>Prometheus就是實現能提供一個高維度資料的模型, 其中也包含了一組任意數量的k-v pair.</p>
<p>在現在這年代, 硬碟容量是很方便被擴充的, 且讀寫速度也遠快於以前. 沒必要省成這樣. 多冗餘些資訊在資料內反而方便之後關聯查詢用. 數據的維度越高, 就越有可能找到系統行為中隱藏或難以捉摸的模式(還是交給數據分析專家幫忙分析好了).</p>
<p>假設我們定義了6個高基數維度的事件, time, app, host, user, endpoint還有status. 我們就可以建立查詢方法來分析這些維度的組合和顯示相關性. 像是可以搜尋所有status是502, time發生在過去半小時內的, 且host是ithome的數據. 或是找所有status是403, 由user是Nathan對著ironman14th這endpoint發生的錯誤, 等等的.</p>
<h2 id="heading-5luk5pel5bcp57i957wq">今日小總結</h2>
<p>Observability相關的工具或服務, 其實不只是上面提到的metrics, 或者是限制遙測資料的基數跟維度. 反而是鼓勵開發團隊為每個可能發生的事件收集豐富的遙測資訊. 將請求的完整上下文也存儲起來, 在某些時候使用或者分析用.</p>
<p>聊到現在, 感覺上Observability就只是Monitoring換了一層包裝的講法, 但不管如何, Observability都是為了給系統帶來更好的Visibility(可見性), 易於Debugging調試, 是一個穩定的系統在維護與發展上的基石.</p>
<p>明天也會繼續講更多Observability. 謝謝各位與隊友們.</p>
<h2 id="heading-5yd6icd6loh5paz">參考資料</h2>
<p>[^1]: <a target="_blank" href="https://www.statology.org/high-dimensional-data/">What is High Dimensional Data?</a> [^2]: <a target="_blank" href="https://prometheus.io/">Prometheus</a> <a target="_blank" href="https://github.com/cncf/tag-observability/blob/main/whitepaper.md">Observability Whitepaper</a></p>
]]></content:encoded></item></channel></rss>