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
如何利用 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 · 8 min · 1575 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

使用 go work 在本地開發解決同時開發 module 的問題

在 Golang 1.18 中 go workspace 的提案釋出後,golang 的官方文件或多或少也提到應該要怎麼做 multi module 的開發。相較於過去需要不斷的替換 go.mod 內的 replace 指令,go work 大幅改善了 multi module 的開發體驗。 為什麼需要 go work 專案逐漸變大 當你在維護一個小工具 side project 時,單一的 module 就能夠滿足所有需求,但當專案逐漸變大,會需要將專案拆分成多個 module。 可以透過一個例子來理解: 假設我們有一個專案,拆分成以下兩個 module: common-lib-golang: 存放所有專案都會用到的 function,例如 retry, logger, tracer 等等 backend: 實際提供 http api 的程式碼 最開始的檔案架構為: . ├── backend │ ├── go.mod │ ├── go.sum │ └── main.go └── common-lib-golang ├── go.mod ├── go.sum └── util.go 隨著專案變大,我們在開發 backend 時,時常會需要修改 common-lib-golang 內的程式碼,這時候就需要 go work 來協助我們。 ...

2025-02-20 · 1 min · 208 words

在習慣 go mod 後重新學習 git submodule

前言 使用 Golang 作為主要開發已經有五年的時間。最近因工作需要接觸到 Python 專案,並且該專案使用 git submodule 的方式來引用共同函式庫 common-lib-python。對於長久使用 Golang 的 go mod 的我來說,submodule 是一個相對陌生的概念。藉這個機會撰寫一篇文章,整理並紀錄一下 git submodule 的用法,也作為未來的參考。 這篇文章會假設已經對 git 的基本操作有一定程度的了解,並著重在 submodule 的概念、使用情境以及與 Golang 的 go mod 的差異比較。文章內容會以下列流程來呈現: 建立一個新的 Python 專案 my-python-repo 建立一個 Python 模組 my-python-module 作為 submodule 在 my-python-repo 中使用 my-python-module 作為 submodule 模擬需求變更,同時修改 my-python-repo 與 my-python-module,並分別發送 PR 與 Golang 的 go mod 進行比較 建立 Python 專案與模組 先建立兩個新的 git repo,分別是 my-python-repo 與 my-python-module。 # 建立 my-python-repo mkdir my-python-repo cd my-python-repo git init touch main.py git add . git commit -m "Initial commit" # 建立 my-python-module cd .. mkdir my-python-module cd my-python-module git init touch my_module.py git add . git commit -m "Initial commit" # 將兩個專案 push 到 Github/Gitlab 上 my-python-repo 與 my-python-module 都已經是一個獨立的 git repo。 ...

2025-01-24 · 3 min · 548 words
Golang Composition over Inheritance

Golang Composition over Inheritance

Golang 是一門簡潔有力的程式語言,相較於其他程式語言,更傾向於使用組合(composition)而不是繼承(inheritance),語言設計之初更是沒有提供繼承的關鍵字,這種設計哲學讓 Golang 在現代軟體開發中脫穎而出。 繼承固然有其優點,但在建構複雜的物件關係時,容易產生過於龐大的繼承層級結構。這使得程式碼難以閱讀和維護,就像是一棵盤根錯節的大樹,牽一髮而動全身。 過深的繼承層級會導致知名的「脆弱基類問題」(fragile base class problem),使得程式碼難以修改和擴展。 組合則不同,它鼓勵建立小型、專注的 struct,然後像樂高積木一樣,將這些 struct 組合成更大的結構。這種方式讓程式碼模組化,更容易理解和修改。 彈性 Golang 的 type system 支援我們靈活地組合各種 struct。可以建立一個新的 struct,並在其中「嵌入」其他 struct 作為其欄位。 type Car struct { make string model string year int } type Driver struct { name string car Car } func main() { myCar := Car{"Toyota", "Camry", 2020} driver := Driver{"John", myCar} fmt.Println(driver.name) // 輸出: John fmt.Println(driver.car.make) // 輸出: Toyota } 在這個例子中,Driver 透過組合 Car 來建立更豐富的資料結構。Driver 「has-a」 Car,而不是 「is-a」 Car,這提供了更高的彈性,讓 Driver 可以更專注在自身的邏輯。 程式碼複用 組合促進了程式碼的複用。可以建立許多小型、可複用的 struct,然後將它們組合成各種不同的結構。 繼承也能實現程式碼複用,但它也可能導致不必要的耦合,使得程式碼難以修改,因為對父類別的修改可能會影響到所有子類別。 type Engine struct { power int fuelType string } type Wheels struct { count int material string } type Vehicle struct { engine Engine wheels Wheels brand string } func (v Vehicle) getBrand() string { return v.brand } func (v Vehicle) getEnginePower() int { return v.engine.power } 在這個例子中,Vehicle 透過組合 Engine 和 Wheels 來複用這兩個 struct 的欄位和功能。Vehicle 「has-a」 Engine and 「has-a」 Wheels,並可以新增自己的欄位和方法,例如 brand、getBrand() 和 getEnginePower()。這種方式讓 Vehicle 可以專注於自身的邏輯,同時又能複用 Engine 和 Wheels 的功能。 ...

2025-01-11 · 3 min · 634 words