Interface 不是有開就好:從一個 PR 來看抽象化的重要性

前言 最近團隊正在開發一個新產品,其中一個核心功能需要 client 與 server 之間進行即時、雙向的溝通。經過一番技術評估,我們決定採用 WebSocket 來實現這個需求。 身為一個良好習慣的開發團隊,我們在開發初期就導入了依賴注入(Dependency Injection),希望透過界面(Interface)來解耦商業邏輯與具體的實作,這樣不僅能提高程式碼的可測試性,未來在更換底層實作時也能更加輕鬆。 一切聽起來都很美好,直到我在一次 Code Review 中,看到了一段熟悉的程式碼。 一個 PR 的故事 在我們的 Domain Layer,也就是處理核心商業邏輯的地方,我看到同事定義了下面這個 interface: // package/to/domain/service.go // WebSocketService defines the interface for websocket communication. type WebSocketService interface { // StartAndLinsten starts the service and listens for incoming messages. StartAndLinsten(ctx context.Context) error // Send sends a message to the client. Send(ctx context.Context, message any) error // ... other methods } 第一眼看過去,好像沒什麼大問題。有名稱、有方法、也確實是個 interface。然而,當我細看 WebSocketService 這個命名時,總覺得哪裡怪怪的。 於是我在 PR 上留下了這樣的 comment: 這個界面主要是抽象化 client 與 server 間的互動,不應該侷限於 WebSocket 這個 Protocol。假如我們未來要換成使用 socket.io 或是 gRPC stream,是不是連 domain 層的 interface 也要跟著改動? ...

2025-10-04 · 2 min · 334 words
監控你的執行檔:初探 watchexec

監控你的執行檔:初探 watchexec

前端開發有 liveserver,後端開發有 air,那 TUI 開發呢?本文記錄了我在開發 Bubbletea 應用時,從 air 轉向 watchexec 的心路歷程,以及如何使用這個通用工具來優雅地實現終端機應用的熱重載。

2025-09-06 · 2 min · 237 words

用 Golang Bubbletea 打造終端機應用:從 Hello World 到多頁面架構

探索如何使用 Golang 的 Bubbletea 函式庫,基於 Elm 架構,從零開始打造一個互動式終端機應用(TUI)。本文將從一個簡單的計數器範例,逐步引導你建構出一個類似 Web 應用的多頁面架構,並分享整個生命週期中的關鍵概念與注意事項。

2025-09-05 · 3 min · 630 words

初探 Tauri:為了解決朋友的繁瑣任務,寫了個桌面 App

一個非工程師朋友的日常繁瑣任務,成為我初探 Tauri 的契機。本文記錄如何使用 Rust 的強大後端結合 React 的靈活前端,打造一個輕量、跨平台的桌面應用,來解決真實世界的問題。

2025-08-27 · 2 min · 352 words
Prompt to Product:AI 時代開發者的範式轉移

Prompt to Product:AI 時代開發者的範式轉移

那些年我們追逐的開發效率 還記得第一次聽到「十倍工程師」這個詞的時候,心中總是充滿憧憬。想像著有一天也能寫出十倍的程式碼、處理十倍的任務、創造十倍的價值。每當看到某個同事在短時間內完成複雜的功能,總是會想:他們是不是真的擁有某種神秘的超能力? 隨著經驗的累積,漸漸發現這更像是一個美麗的神話。真正的效率並不在於敲擊鍵盤的速度,也不在於記住多少語法細節。那些看似神奇的十倍效率,往往來自於對問題本質的深刻理解,以及選擇正確工具的智慧。 直到 ChatGPT 橫空出世,Agentic AI 開始進入我們的開發流程,我開始思考:AI 能為我們帶來什麼?又有什麼是它永遠無法取代的? 從懷疑到接受的心路歷程 最初的抗拒與恐懼 坦白說,剛開始接觸這些 AI 工具時,我的內心是抗拒的。作為一個有著多年開發經驗的工程師,看著這些「會寫程式的機器」,心中五味雜陳。那種感覺就像是突然有人告訴你,你引以為傲的專業技能可能會被一個沒有情感的演算法取代。 第一次使用 AI code assistant 時,我提出一個簡單的需求:「幫我寫一個排序函數」。當它瞬間給出了完整且正確的程式碼時,我的第一反應不是驚喜,而是擔憂。這些工具真的實用嗎?會不會最終取代我們這些工程師?依賴 AI 是否會讓我們的技能退化? 這些疑慮很真實,也很合理。畢竟,我們這個行業向來對「銀彈」保持謹慎態度。太多次,我們看到某個新技術被吹捧為「革命性的」,最終卻發現它只是解決了某個特定問題,而創造了十個新問題。 重新定義效率的數學陷阱 然而,在實際使用 Zed AI、GitHub Copilot Agent 等工具幾個月後,我發現自己的思維框架需要調整。「十倍工程師」的概念確實存在一個隱藏的數學陷阱,而理解這個陷阱是擁抱 AI 輔助開發的第一步。 我開始仔細觀察 AI 在哪些環節能夠提供幫助。當我需要寫一個複雜的正則表達式時,AI 能在幾秒鐘內給出準確的答案,而我可能需要查資料和測試十幾分鐘。當我需要重構一個大型函數時,AI 能夠快速理解程式碼邏輯並提供多種重構方案。當我需要撰寫技術文件時,AI 能夠幫我整理思路,甚至提供不同角度的表達方式。 但同時,我也清楚地感受到 AI 的局限。當系統編譯時,AI 無法讓 CPU 運算得更快。當測試套件在 CI 環境中執行時,AI 無法讓網路延遲消失。當我需要與產品經理討論需求細節時,AI 無法代替我進行那些微妙的人際溝通。當面對複雜的架構選擇時,AI 可以提供選項,但最終的決策仍需要結合業務上下文和長期策略考量。 有一天,一位同事興奮地告訴我:「用了 AI 工具後,我覺得自己的開發速度提升了十倍!」我問他:「你的專案交付時間真的縮短了十倍嗎?」他想了想,搖搖頭說:「沒有,可能只快了兩三倍。」這就是數學陷阱的本質:AI 能夠大幅加速某些特定環節,但整個開發流程的速度受限於最慢的那個環節。 從工具使用者到架構思考者 這種認知的轉變讓我重新審視自己作為開發者的角色定位。AI 的出現並沒有降低我們的價值,反而讓我們從機械性的程式碼編寫中解放出來,有更多時間專注於真正重要的事情:理解業務需求的本質、設計優雅的系統架構、做出明智的技術選型決策。 我開始意識到,真正的「十倍效率」不是來自於打字速度的提升,而是來自於思維方式的轉變。當 AI 幫我們處理了大量重複性工作後,我們有機會成為更好的架構師、更好的問題解決者、更好的技術領導者。 機器可讀設計的哲學革命 經過這段時間的實踐,我得出了一個看似簡單但意義深遠的核心理念:「只有機器可讀的設計,才能被 LLM 加速」。 重新審視我們的設計習慣 這個理念讓我開始重新審視過去的工作習慣。回想一下我們是如何管理專案的:每次設計評審,我們會花費大量時間製作精美的 PowerPoint,其中包含各種流程圖、架構圖、時序圖。這些圖表在投影幕上看起來很專業,但會議結束後,它們就被儲存為 PNG 或 PDF 格式,靜靜地躺在某個共享資料夾中,再也沒有人會去更新或維護。 ...

2025-08-10 · 3 min · 470 words
如何利用 Golang AST 助攻 LLM 省 token 又高效

如何利用 Golang AST 助攻 LLM 省 token 又高效

前言 近來大型語言模型(LLM)的發展可謂一日千里,特別是在程式碼理解、生成與輔助開發方面,展現出了驚人的潛力。許多開發者開始嘗試將 LLM 融入到日常工作中,期望能提昇開發效率,甚至實現所謂的「vibe coding」——讓 LLM 理解程式碼的整體風格與意圖,並在此基礎上進行協作。 然而,當我們試圖讓 LLM 直接「閱讀」整個大型專案的程式碼庫時,往往會碰到一些現實的挑戰。上下文長度限制、高昂的 token 消耗以及潛在的雜訊干擾,都可能讓 LLM 的表現不盡如人意。這時候,我們就需要更聰明的方法來為 LLM「提煉」程式碼的精華。 在這篇文章中,我想分享一個在 Golang 專案中可能被忽略的利器:抽象語法樹(Abstract Syntax Tree, AST)。透過 Golang AST,我們可以更精準地提取程式碼的結構資訊,為 LLM 提供一份濃縮且高效的上下文,既能節省寶貴的 token,又能幫助 LLM 更好地把握「Code Vibe」。 LLM 直接消化大型 Code Base 的「痛」 想像一下,你正在開發一個頗具規模的 Golang 後端服務,裡面包含了數十個套件、數百個檔案。現在,你想讓 LLM 幫你新增一個功能,或者重構某個模組。如果直接把所有相關的程式碼一股腦地丟給 LLM,可能會遇到以下這些令人頭痛的問題: token 消耗「爆表」:LLM 的使用成本與輸入輸出的 token 數量直接相關。將大量原始碼作為輸入,無疑會產生巨額的 token 費用,對於個人開發者或小型團隊來說,這可能難以承受。 「腦容量」不足的上下文限制:即使是目前頂尖的 LLM,其能夠處理的上下文長度也是有限的。面對龐大的程式碼庫,LLM 可能無法一次「看」全所有必要的資訊,導致理解片面或生成結果不佳。 資訊過載與雜訊干擾:完整的程式碼中,充斥著各種細節——註解、空行、詳細的錯誤處理邏輯、暫時用不到的私有函式等等。這些資訊對於 LLM 理解程式碼的「vibe」或執行特定高層次任務(例如「模仿現有風格新增一個 API 端點」)來說,有時反而會成為雜訊,影響其判斷。 龜速的回應:通常情況下,輸入給 LLM 的資訊越多,它處理並生成回應所需的時間就越長。在追求高效開發的今天,漫長的等待顯然不是我們想要的。 面對這些挑戰,我們不禁要問:有沒有一種方法,可以只給 LLM「剛剛好」的資訊,讓它既能理解我們的意圖,又能高效地完成任務呢?Golang AST 或許就是答案的一部分。 Golang AST 如何「助攻」 在我們深入探討 AST 如何幫助 LLM 之前,先快速回顧一下什麼是 Golang AST。 ...

2025-06-16 · 3 min · 622 words

一次核心模組的重構經驗

前言 在軟體開發的漫漫長路中,我們時常會接手一些充滿「歷史印記」的專案。這些專案的核心模組,往往因為業務的快速迭代與時間的無情沖刷,逐漸演化成難以觸碰的「史前巨獸」。近期,我便有幸(或許該說是不幸地)參與了一次這樣核心模組的重構之旅,其核心是我們產品線廣泛使用的 Golang gRPC 認證攔截器 (Interceptor)。這段經歷充滿挑戰,但也收穫良多,希望能藉此分享一些心得。 歷史的塵埃:核心模組的演進悲歌 我接手的這個核心認證模組,在專案初期或許設計簡潔明瞭,但隨著產品線的不斷擴展和需求的堆疊,其複雜度已然失控。追溯其演進的脈絡,彷彿能看到一部小型技術債的形成史。 最初的起點:單純的 gRPC Interceptor 可以想見,專案伊始,對於 gRPC 服務的認證需求相對單純。一個通用的攔截器或許就能滿足所有需求,程式碼結構清晰可見: package main func SimpleAuthInterceptor(...) { log.Println("Performing basic authentication via SimpleAuthInterceptor") return handler(ctx, req) } func SimpleStreamAuthInterceptor(...) error { log.Println("Performing basic stream authentication via SimpleStreamAuthInterceptor") return handler(srv, ss) } func main() { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer( grpc.UnaryInterceptor(SimpleAuthInterceptor), grpc.StreamInterceptor(SimpleStreamAuthInterceptor), ) log.Println("gRPC server listening on :50051") if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } } 在這個階段,一切看起來是那麼的美好與純粹。 ...

2025-06-14 · 6 min · 1102 words
Golang Iterator 簡介與 samber/lo 比較

Golang Iterator 簡介與 samber/lo 比較

自從 Golang 1.18 版本引入泛型(Generics)後,Go 語言的生態系統迎來了許多令人興奮的變化。其中,Golang 1.23 版本對 Iterator(迭代器)的標準化,以及 iter 套件的加入,無疑是近期改動中相當重要的一環。本文將淺談 Golang Iterator 的基本概念,深入探討 Pure Iterator 與 Impure Iterator 之間的區別與設計考量,並與社群中流行的 samber/lo 工具庫進行比較。 什麼是 Iterator? Iterator Pattern(迭代器模式)是一種常見的設計模式,它提供了一種循序存取集合物件中各個元素的方法,而又無需暴露該物件的內部表示。簡單來說,Iterator 就像一個指針,可以依序指向集合中的下一個元素,直到遍歷完所有元素為止。 Golang 中的 Iterator 在 Golang 1.23 之前,我們通常透過 for-range 迴圈來迭代 array、slice、string、map、channel 等內建資料結構。然而,對於自訂的資料結構或複雜的序列生成邏輯,缺乏一個統一的迭代標準。 Golang 1.23 版本正式將 Iterator 標準化,並在標準庫中加入了 iter 套件。同時,slices 和 maps 套件也增加了一些回傳 Iterator 的工廠函數(Iterator Factories)。到了 Golang 1.24,更有如 strings.SplitSeq 等函數加入,進一步豐富了 Iterator 的應用場景。 // strings.SplitSeq 回傳一個迭代器,用於遍歷由 sep 分隔的 s 子字串。 // 此迭代器產生的字串與 Split(s, sep) 回傳的相同,但不會建構整個 slice。 // 它回傳一個單次使用的迭代器。 func SplitSeq(s, sep string) iter.Seq[string] 如果對 Golang 1.23+ 中 Iterator 的語法和語義還不熟悉,建議可以閱讀 Ian Lance Taylor 在 Go 官方部落格發表的介紹文章 ...

2025-05-31 · 6 min · 1162 words
告別 Docker Desktop 束縛!macOS 容器實戰:colima + k8s + containerd 踩坑遷移全記錄

告別 Docker Desktop 束縛!macOS 容器實戰:colima + k8s + containerd 踩坑遷移全記錄

前言 身為一個開發者,特別是在 macOS 環境下,Docker Desktop 幾乎是容器化開發的標配。然而,自從 Docker Desktop 開始針對大型企業調整其授權模式後,許多開發者開始尋找替代方案。 市面上確實出現了一些選擇,例如閉源但功能強大的 OrbStack。但對於熱愛開源的我來說,目光自然投向了社群。這時,colima 這個開源專案進入了我的視野。它不僅提供了一個在 macOS 上運行 Linux 容器的輕量級方式,還內建了 Kubernetes 支援,引起了我極大的興趣。 這篇文章,想記錄一下我從 Docker Desktop 轉換到 colima,並且在 colima 的 Kubernetes 環境中,從原本依賴 Docker Engine 逐步遷移到使用 containerd 作為容器執行時(Container Runtime)的心路歷程與踩坑經驗。 lima 與 colima 簡介 在深入 colima 之前,得先提一下 lima (Linux virtual machines on macOS)。lima 是一個旨在於 macOS 上輕鬆運行 Linux 虛擬機的開源專案。它底層利用了 macOS 的虛擬化框架(如 QEMU 或更高效的 vz),提供了一個相對輕量的 Linux VM 環境。 而 colima 則可以看作是建立在 lima 之上的「使用者友善層」。它簡化了 lima 的配置,並專注於提供容器執行時環境。colima 可以讓你輕鬆地啟動一個配置好 Docker 或 containerd 的 Linux VM,並且可以選擇性地啟用 Kubernetes (K3s) 支援。簡單來說,colima 幫你處理了建立 VM、安裝 Runtime 等繁瑣步驟,讓你專注在容器本身。 ...

2025-04-19 · 6 min · 1096 words
建構多平台的 container image

建構多平台的 container image

前言 最近在搞 side project 時,常常需要在不同的 CPU 架構上跑我的應用程式。例如,開發用的 MacBook 是 ARM 架構 (Apple Silicon),開發用的 Desktop 是 x86 架構(Ryzen 5900X),而部署的伺服器可能是 x86 (AMD64),有時候甚至想在 Raspberry Pi (ARM) 上跑些小東西。每次都要為不同平台分別建構 image 實在有點麻煩,而且 Registry 上一堆 xxxapp-amd64, xxxapp-arm64 的 tag 看了也很礙眼。經過一番研究與嘗試,是時候接觸 Docker Buildx 了。 為什麼需要多平台映像檔 在 wintel 的商業策略下,以及大家對高性能伺服器的普遍認知,主要用 x86/amd64,但現在 ARM 架構越來越普及,從 Apple Silicon 的 Mac、AWS Graviton 處理器、各種 IoT 設備到你的 Raspberry Pi,ARM 無所不在。如果你的 container image 只支援 amd64,那它就無法在這些 ARM 設備上原生運行 (需要模擬,效能差)。為了Build Once, Run Anywhere,多平台映像檔 (Multi-platform images) 就是 meta。 OCI 多平台映像檔架構簡述 其實不複雜。傳統的單一平台 image,它的 manifest 指向一組設定檔和一堆 layer。而多平台 image 則是透過一個 manifest list (索引) 指向多個特定平台的 manifest。每個特定平台的 manifest 才各自指向該平台的設定檔和 layer。 ...

2025-03-30 · 12 min · 2404 words