Author Avatar

Raiven Kao

a piece of me
2025 Recap

2025 Recap

當兩個人意見不一致,可能其中一個人是錯的。如錯錯的人是你,難道你不想知道嗎? 出自 Ray Dalio 的《原則》一書,是我 2025 年對自己的提問。 接續 2023 年展望,與 2024 Recap,用這篇文章來紀錄今年的自己。 可以搭配今年度最常聽的音樂 Again (Yui Acoustic Version) 工作 去年把工作放到最後,今年則是放到第一位。今年換到了新的公司、新的工作崗位,從十幾位 RD 的新創公司,換到了有一千多位 RD 的大公司。即便已經沒有過去擔憂的「過度官僚」文化,但仍在職也不好多說什麼。 整體來說目前待的團隊,是夾雜著無趣卻也有趣的產品,無趣的點在於產品本身並不性感,目標客群是企業客戶,幾乎沒有什麼創新的概念。有趣的點在於,如何在有限的時間內拓展產品線,並且以 UX 的角度來思考企業客戶如何使用產品,並花費更多時間在前期的 design,而不是單就功能進行開發,時常會需要花費八成的時間在前期規劃,真正的開發時間就是靠 AI(LLM) 解決。 隨著團隊拓展,我也承擔了 mentor 的責任,第一次帶新人多少有些徬徨與無助。隨著團隊擴展,漸漸的開始反思現代軟體開發,一件一個人能完成的事情被拆成四五個人都要有貢獻,導致交付速度不升反降,這樣真的是正確的嗎?我想還需要更多的時間積累才能找到答案。 不得不提的就是,目前的公司有「近乎無限」的 AI 資源(特別指 LLM),讓手速不再成為寫程式的瓶頸。與前東家不同的是,這間公司是在今年才開始轉型 AI First,並且透過 sharing & AI Hackathon 來推廣 AI 在內部開發與產品上的使用。不過觀察下來,年初的 AI 使用率應該是不高的,就連我這個 AI 質疑論者,都能被邀請去 All Hands 進行 AI 使用的 session。 健康 今年在健康上只能說完全荒廢,與去年底期許的目標,不僅完全沒有維持,更糟糕的是因為通勤距離變遠了,導致運動時間大幅下降。 但… 我想這一切都只是藉口,單純只是變懶惰、沒了動力。只能把希望寄託在 2026 年,能透過「12周做完一年工作」這本書內提到的方法,不設定目標,而是設定行動,先把行動給完成再看看怎麼推進了。 旅遊 隨著離開新創公司,又回到了下班不認識的那個自己,今年多數旅遊都是跟 Murphy 兩人,連爬山也變少了,希望 2026 年能投入更多時間在旅遊上。 ...

2025-12-28 · 1 min · 206 words

Helm Smart Resource:讓你的 Chart 學會與既有資源和平共處

前言 在 Kubernetes 的世界裡,Helm 無疑是管理應用程式部署的霸主。它標準化了資源的定義,讓我們可以用宣告式的方式管理整套系統。然而,在真實世界的維運場景中,事情往往沒那麼單純。 我們常遇到一種尷尬的情況:某些資源(例如 Database 的 Secret、外部系統的 ConfigMap)可能在 Helm Chart 安裝之前就已經由維運人員手動建立,或是由另一個流程(如 Terraform)預先準備好了。 這時候,如果直接執行 helm install,往往會收到 “resource already exists” 的錯誤;如果使用 helm upgrade --install,又擔心 Helm 會覆蓋掉這些既有設定。 這篇文章將分享一種「Smart Resource」的設計模式,透過 Helm 的 lookup 函數與樣板邏輯,讓你的 Chart 能夠聰明地判斷:「這東西是我管的嗎?如果是,我才動它;如果不是,我就尊重現狀。」 核心難題:Ownership 在 Kubernetes 中,資源的「所有權」觀念至關重要。Helm 預設認為它 release 中的所有資源都應該由它全權管理。但當我們需要與外部資源協作時,我們需要更細緻的控制。 我們的目標很明確: 若資源不存在:建立它,並標記為 Helm 管理。 若資源已存在且由 Helm 管理:更新它(Patch/Merge)。 若資源已存在但由外部管理:保持原狀,不進行覆蓋或刪除。 為了達成這個目標,我們需要一個輔助函數來判斷資源的歸屬權。 實作細節 1. 定義所有權檢查 首先,我們在 _helpers.tpl 中定義一個檢查函數。Helm 會在它建立的資源上打上特定的 Annotations(meta.helm.sh/release-name 和 meta.helm.sh/release-namespace)。我們可以利用這一點來判斷資源是否屬於當前的 Release。 {{/* Check if a resource is owned by this Helm release Returns "true" or "false" as string for stable piping */}} {{- define "visionone-filesecurity.isOwnedByRelease" -}} {{- $resource := .resource -}} {{- $releaseName := .releaseName -}} {{- $releaseNamespace := .releaseNamespace -}} {{- $owned := and $resource (hasKey $resource.metadata "annotations") (eq (get $resource.metadata.annotations "meta.helm.sh/release-name") $releaseName) (eq (get $resource.metadata.annotations "meta.helm.sh/release-namespace") $releaseNamespace) -}} {{ printf "%t" $owned }} {{- end -}} 這段程式碼邏輯很簡單:只有當資源存在,且其 Annotations 中的 Release Name 與 Namespace 都與當前 Release 相符時,才視為「Owned」。 ...

2025-11-22 · 4 min · 715 words

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 · 8 min · 1575 words