如何利用 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....

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....

2025-05-31 · 6 min · 1162 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....

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 可以更專注在自身的邏輯。...

2025-01-11 · 3 min · 634 words

Golang 1.22 中 http routing 的改進

Golang 作為一個偏向 server 應用的程式語言,一般的 web server 並不會直接使用原生的 package net/http,而更多的使用 gin-gonic/gin 或是 gorilla/mux,後來也有 labstack/echo 以及 go-chi/chi 等等選擇,在效能、輕量、好維護、好擴充中,都能找到對應的 third party package,其中的原因不外乎是原生的 package 提供的功能過於簡潔。 好在 1.22 中,官方改進了 net/http 中對於多工器、路由,甚至出了一篇部落格,現在更可以「大膽的」直接使用 standard library。 Path Parameter 若要將應用的 Web API 定義成 RESTful,我們會使用 /資源/{資源唯一識別符}/子資源/{子資源唯一識別符} 來定義路徑。假如要獲取一個使用者的訂單,則會使用 GET /users/1/orders 來獲取。在 1.22 以前,我們只能定義到 /users,再自行解析往後的 path: http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { subPath := strings.TrimPrefix(req.URL.Path, "/users/") if len(subPath) == 0 { xxx } else { ooo } ... }) 而在 1.22 中新增了 net....

2024-10-30 · 4 min · 656 words

使用 TinyGo 與 Raspberry pi pico 實現溫濕度感測器

前言 作為嵌入式系統學習的微小專案,我決定使用 TinyGo 來實現一個簡單的溫濕度感測器。過去我大多使用 Arduino 作為微控制器(MCU)開發平台,但這次想嘗試使用 TinyGo 來進行單片機的學習。 TinyGo 是 Go 語言的一個子集,專門針對小型設備和微控制器進行了優化,使得我們可以在資源受限的硬體上運行 Go 程式。 全部的程式碼都可以到 omegaatt36/pico-bme280 中找到。 硬體選擇 Raspberry Pi Pico 我選擇了 Raspberry Pi Pico 作為本專案的主控板。Pico 是一款基於 RP2040 晶片的微控制器開發板,具有以下特點: 雙核 ARM Cortex-M0+ 處理器,時脈可達 133 MHz 264KB 的 SRAM 和 2MB 的板載閃存 支援 USB 1.1 主機和設備功能 低功耗睡眠和休眠模式 可編程 I/O(PIO)狀態機 30 個 GPIO 引腳 便宜 價格僅需要 5 美元,後繼款的 Pico 2 仍然維持 5 美元,還額外增加了 RISC-V 架構的支援,可以一次玩到兩種處理器架構。 Pico 的這些特性使其非常適合用於各種嵌入式專案,包括我們的溫濕度感測器。 BME280 感測器 最初我購買了 BMP280 感測器,但後來發現它只能測量溫度和氣壓,無法測量濕度。因此,我轉而選擇了 BME280 感測器,它可以同時測量溫度、濕度和氣壓。...

2024-10-02 · 2 min · 337 words

透過內建的 pprof 工具來分析 Golang 發生的 memory leak

前言 某天下午,公司的 cronjob daemon 無預警的被 GCP OOM Kill 了,且程式碼沒有看出明顯的原因。 根據過去的經驗,local 開發時會使用 go tool pprof 來分析 CPU profile 或是 memory 與 trace 的問題,詳細可以參考 Go 官方文件。 由於我們的程式碼是一個基於 gin 的 http service,因此可以使用 gin 提供的 pprof 來快速建立 endpoint。 gin pprof gin 的 pprof package 提供了數個基於 net/http/pprof 的 endpoint,可以分別為: /: 基本的 pprof 的 static page,可以分析 CPU 與 memory 的問題。 /cmdline: 分析 command line 的問題。 /profile: 分析 CPU profile 的問題,可以透過 query string 來指定 CPU profile 的 duration。 /symbol: 分析 symbol table 的問題。 /trace: 分析 trace 的問題。 /goroutine: 分析 goroutine 的問題,這也是本文中重點查看的 endpoint。 /heap: 分析 heap 的問題。 可以在 http server 中加入以下程式碼來啟用 pprof:...

2024-08-17 · 3 min · 441 words