Golang 1.25 testing/synctest 初體驗:告別在測試中寫 time.Sleep 的日子

前言 在我們目前的應用程式架構中,由於高度與 Kubernetes 耦合,服務啟動與運作期間需要頻繁地去讀取 K8s 中的 ConfigMap。為了達成配置熱更新(Hot Reload),我們引入了 Kubernetes client-go 中的 informers 機制來監聽 ConfigMap 的 CRUD 事件。 雖然 K8s 官方提供了 fake client 讓我們能測試 informers 的邏輯,但在 Service Code 的層級,我們往往需要封裝一層更適合業務邏輯的 ConfigWatcher。Golang 引以為傲的輕量級 Goroutine 與 Channel 搭配非常適合用來處理這種非同步的事件傳遞。 然而,一旦涉及到 Goroutine 的非同步測試,「時間」往往就成了最大的敵人。 遇到的問題:不穩定的測試與魔法數字 為了模擬 ConfigMap 的變更通知,我們定義了一個 ConfigMapWatcher 介面與對應的 Event 結構: const ( ConfigMapUpdateEventTypeAdded ConfigMapUpdateEventType = iota ConfigMapUpdateEventTypeModified ConfigMapUpdateEventTypeDeleted ) type ConfigMapUpdateEvent struct { Name string Type ConfigMapUpdateEventType Value map[string]string } type ConfigMapWatcher interface { Watch(ctx context.Context, eventCh chan<- ConfigMapUpdateEvent) error } 接著,我們很自然地在 testing 中實作了一個 fake 物件來模擬事件發送: type fakeConfigMapWatcher struct { injectCh chan ConfigMapUpdateEvent watchErr error watchOnce sync.Once } func newFakeConfigMapWatcher() *fakeConfigMapWatcher { return &fakeConfigMapWatcher{ injectCh: make(chan ConfigMapUpdateEvent), } } func (f *fakeConfigMapWatcher) sendEvent(event ConfigMapUpdateEvent) { f.injectCh <- event } func (f *fakeConfigMapWatcher) Watch(ctx context.Context, eventCh chan<- ConfigMapUpdateEvent) error { if f.watchErr != nil { return f.watchErr } f.watchOnce.Do(func() { go func() { for { select { case <-ctx.Done(): return case e := <-f.injectCh: eventCh <- e } } }() }) return nil } 問題來了,當我們在寫單元測試時,呼叫 sendEvent 將事件送入 channel 後,消費者端(也就是我們的業務邏輯 Goroutine)並不會「立刻」收到並處理完成。為了確保 assert 斷言執行時,業務邏輯已經跑完了,我們被迫在測試中加入 time.Sleep: ...

2026-01-18 · 3 min · 505 words

Container Image Optimization 那些年我們寫錯的 Dockerfile

最近在檢視公司內部的專案時,針對其中一個 container image 進行了優化。在一個簡單的 commit 後,我們的 image size 從 1.82GB 修正到了 1.18GB。 透過 dive 查看,Image Efficiency 更是從不及格的 69% 飆升到了 99%。 這讓我回想起過去寫 Dockerfile 時,常常因為不了解 Docker Layer 的機制,或是為了寫起來「方便」,而踩到了許多效能與安全的地雷。 致命的 chown 這次優化的核心,其實源自於一個非常常見的操作:修改檔案權限。 在我們的案例中,Dockerfile 原本是這樣寫的: # Bad Practice: recursive chown after copy FROM ubuntu:22.04 WORKDIR /app COPY . . # ... install dependencies ... RUN dpkg -i packages/*.deb # Change ownership for security reasons RUN groupadd -r appuser && useradd -r -g appuser appuser RUN chown -R appuser:appuser /app 看起來邏輯很正確:把檔案複製進去,安裝套件,最後為了安全性將檔案權限交給非 root 使用者。 ...

2026-01-14 · 3 min · 506 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

透過內建的 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