Unrounded Scaling:Go 1.27 浮點數轉換演算法的深度拆解

Unrounded Scaling:Go 1.27 浮點數轉換演算法的深度拆解

前言 如果你對浮點數還不了解,可以服用這個影片。 TL;DR 前面三分之二是演算法拆解,有興趣再啃。只想知道「Go 1.27 實際上改了什麼、變多快、輸出有沒有跑掉」的話,可以直接跳到 Go 1.27 到底改了什麼,結論是 878 行三套演算法變成 290 行一套、快了 11–21%、輸出零變化。 我把 Go 升到 1.27 之後,照慣例翻了一遍 release notes,也寫了新特性整理。整份文件從頭到尾沒有一個字提到 strconv。 但是 Go 1.27 把 fmt.Sprintf("%v", f)、strconv.ParseFloat、json.Marshal 底下那顆浮點數轉換引擎整個換掉了。舊的實作是三套演算法各自為政:最短列印用 Dragonbox、固定位數列印用 Ryū 風格的程式碼、解析用 Eisel-Lemire,新的實作是一個叫 unrounded scaling 的東西,作者是 Russ Cox,論文是 2026 年 1 月才發表的。 這件事之所以值得寫一篇,是因為 Russ Cox 在 2011 年寫過一篇 Floating Point to Decimal Conversion is Easy,副標題是 Part 1。文章的論點是: Floating point to decimal conversions have a reputation for being difficult. At heart, they’re really very simple and straightforward. ...

2026-08-26 · 15 分鐘 · 3162 字 · map[email:[email protected] name:Raiven Kao]
Spec-First API:用 codegen 約束 LLM 與人類的 drift

Spec-First API:用 codegen 約束 LLM 與人類的 drift

前言 事情是這樣開始的。 我請 agent 幫忙加一個 endpoint,它三分鐘寫完,handler 乾淨、error handling 完整、unit test 也跟著補齊,go test ./... 全綠。此時我想到的是,agent 真的有做完嗎?route 真的有被註冊嗎? 換成三年前的我,也會漏,那要怎麼樣讓 Agent 不會漏?這個系統從一開始就沒有任何機制能發現有人漏改。以前它靠的是 reviewer 的眼力與同一批人的肌肉記憶撐著;現在把產出速度乘上十倍,這條防線就直接被沖垮了。這不是 agent 的問題,是架構債被提早引爆。 這篇想記錄的,就是後來把這個服務從 code-first 遷移到 spec-first 的過程:用 OpenAPI 當單一真值來源(SSOT),用 oapi-codegen 產生 typed interface,讓 compiler 與 CI 去做原本靠人記憶的事情。 問題不是「有三份檔案」 遷移前,同一條 API 的定義散落在三個地方: infra route definition ← path / method / authorizer cmd/*/main.go ← mux.HandleFunc 手寫註冊 Go swag comments ← @Router / @Param / @Success 三份檔案本身不是問題,同一個概念散在多個檔案是常態。 真正的問題是,它們彼此不知道對方存在。 改 path,可能只改到 Go route,忘了 infra;改 response shape,可能只改 handler struct,忘了註解;授權設定只活在 infra 層,Go 跟 OpenAPI 都不知道有這回事。而 swaggo 的註解在 repo 裡形同空轉,它是「程式碼的產物」,而我們沒有任何機制拿它回頭驗證程式碼。 ...

2026-08-11 · 10 分鐘 · 1996 字 · map[email:[email protected] name:Raiven Kao]

Golang 1.27 新特性整理:泛型補完與轉正的實驗性功能

半年前寫完 Go 1.26 新特性 那篇,文末問了一句「準備好更新你的 go.mod 到 go 1.26 了嗎?」,沒想到一轉眼 Go 1.27 也要來了。撰文的當下 Go 1.27 即將正式發布,以下內容是根據 release candidate 整理,如果正式版有微調,還是以官方 release notes 為準。 如果說 Go 1.26 給我的感覺是數量多、廣度大,那 Go 1.27 更像是把之前欠的債還一還。generic method 從 issue 一路等到現在終於落地,goroutineleak profile 從需要 GOEXPERIMENT 才能開的實驗功能轉正成預設行為,encoding/json/v2 也從實驗畢業成為預設實作。這篇就挑幾個我覺得值得記錄的改動來聊聊。 語言層面:泛型設計的最後一塊拼圖 Generic Method 真的來了 這件事我在 Go 1.27 即將支援 Generic Method 那篇已經寫得很詳細了,這裡就不重複展開,只放結論:method 現在可以宣告自己的 type parameter,不用再繞道用 package-level function 把 receiver 當參數傳進去。 type List[T any] struct { items []T } func NewList[T any](items ...T) List[T] { return List[T]{items: items} } // MapTo 是 generic method,帶著自己的型別參數 U, // 跟 receiver 的型別參數 T 是兩回事。 func (l List[T]) MapTo[U any](f func(T) U) List[U] { out := make([]U, len(l.items)) for i, v := range l.items { out[i] = f(v) } return List[U]{items: out} } type Cat struct { Name string Age int } cats := NewList(Cat{"Mittens", 3}, Cat{"Tama", 5}) names := cats.MapTo(func(c Cat) string { return c.Name }) fmt.Println(names.items) // [Mittens Tama] 老話一句:interface 目前還是無法宣告帶 type parameter 的 method,這是 dynamic dispatch 架構性的限制,不是漏掉沒做。想看完整的來龍去脈跟 workaround 的比較,去讀前面連結的那篇。 ...

Go 1.27 即將支援 Generic Method:告別 package-level 函式的 workaround

Go 1.27 即將支援 Generic Method:告別 package-level 函式的 workaround

前言 Go 1.18 帶來泛型之後,我們終於可以寫出型別安全的通用函式,不再需要到處 interface{} 加型別斷言。但一個眾所周知的限制一直存在到現在,method 沒辦法有自己的 type parameter。 換句話說,這段程式碼在目前的 Go 版本中是非法的: func (c *Client) doRequest[T any](ctx context.Context, method, path string, body any) (*T, error) { // 編譯錯誤:cannot use generic method doRequest without instantiation } 為了繞過這個限制,大家只能退而求其次,把 receiver 當作普通參數傳進去,改用 package-level 的 generic function。 最近 Go issue #77273 的進展讓這件事有了轉機,generic method 的設計已被接受,預計在 Go 1.27 正式釋出。 為什麼現在沒有 Generic Method? 這不是 Go team 沒想到,而是刻意的設計決策。 Go 的 interface 是核心功能,method 必須能夠被 dispatch 到具體的實作上。如果 method 自身帶有 type parameter,編譯器在 dispatch 時需要知道 T 是什麼,這在動態 dispatch(interface dispatch)的情境下是個難題,runtime 無法事先知道要生成哪個型別的實例化版本。 所以泛型剛推出時,generic method 被刻意排除在外,等待一個比較完整的設計方案。這個等待從 Go 1.18 一路等到了 1.27(暫定)。 ...

Pragmatic Clean Architecture in Go

Pragmatic Clean Architecture in Go

前言 在大型系統或需要長期維護的產品中,導入 DDD(Domain-Driven Design)或 Clean Architecture 幾乎是業界的標準答案,分層清晰、職責明確、可測試性高,這些優點毋庸置疑。 但問題是,不是每個專案都是大型系統。 當你面對的是一個內部工具、side project、或是剛起步的新產品,Clean Architecture 的大全套 Use Case、Port、Adapter、Aggregate、Repository Interface 全放 domain,往往會讓你在還沒寫第一行商業邏輯之前,就先在資料夾結構裡迷路了三個小時,或是讓不熟悉的貢獻者花費大量時間在閱讀架構。 這篇文章想討論的是:在不犧牲可測試性與可維護性的前提下,哪些抽象可以捨棄、哪些值得保留,以及我自己踩過的一些坑。 捨棄什麼 獨立的 Port/Adapter 層 Clean Architecture 中,Use Case 透過明確定義的 input/output port 與外界溝通,搭配 Adapter 負責格式轉換。這在大型系統中確實有其價值,但它帶來的代價是:為了讓每一層都能獨立替換,你需要維護大量的介面與轉換邏輯,而這些轉換邏輯往往只是把 A struct 的欄位複製到 B struct。 在小專案中,這條轉換鏈可以大幅縮短。HTTP handler 本身就可以負責 DTO 的轉換,不需要再多一層 Adapter。 DDD 的 Aggregate Aggregate 是 DDD 中保護業務不變式(invariant)的邊界,透過 Aggregate Root 統一管理相關物件的存取。這個概念本身沒有問題,但在小專案中,如果業務規則還不夠複雜到需要嚴格的不變式保護,過早引入 Aggregate 反而會讓簡單的 CRUD 操作變得冗長。 保留什麼 捨棄了部分複雜度之後,剩下的四層架構已經足以應付絕大多數的小專案需求。 package domain 這裡只放 Domain Model 與 Value Object。 值得特別說明 Domain Model 的定義。它不等同於 DDD 裡的 Domain,不是什麼深奧的設計概念,Domain Model 只是在描述「這個應用程式如何跟外部世界互動、邊界在哪」,也就是你的核心資料結構與它們身上的行為。有些「賣課」的人喜歡把 Domain Model 直接等同於 DDD 的 Domain,實際上它是個更基礎、更普遍的概念,可以參考這支影片的解釋。Domain Model 中不需要也不該出現任何技術細節,例如回傳是否是 JSON 等等,這些 Domain Model 甚至要能讓 PM 與非技術人員也「聽的懂」。 ...

放棄 Electron 與 Tauri? 我用 Wails + HTMX 打造 3MB 的跨平台桌面應用

放棄 Electron 與 Tauri? 我用 Wails + HTMX 打造 3MB 的跨平台桌面應用

Web 仔要有 Web 仔的自覺 前言 前陣子收到了 Murphy 的需求,嘗試使用 Tauri 寫了一個桌面 App。Tauri 結合 Rust 與 React 的體驗相當不錯,打包出來的體積也十分理想。 但身為一個主要撰寫 Golang 的後端工程師,內心總有一個聲音:「如果能用 Go 來寫桌面應用,那該有多好?」 眾所周知,Golang 在原生的 Desktop GUI 框架上一直沒有特別強勢的殺手級專案(過去使用 Fyne 的經驗不算太好)。在環顧了一眾基於 Chromium 的 Electron 或是基於系統 WebView 的 Tauri 後,決定走一條稍微不一樣的「老路」:使用 Wails 搭配 HTMX 與 Templ。 最終的產物就是 Dub 一個跨平台的批次檔案重新命名工具。更甚,在 Linux 上打包出來的可執行檔只有約 3MB,並且完美支援 macOS 與 Linux,且幾乎沒有複雜的前端狀態管理負擔。 為什麼是 Wails + HTMX? 在選擇桌面應用程式的技術方案時,我們通常有幾個考量 Electron: 開發體驗極佳,生態系最豐富,但代價是動輒上百 MB 的體積與高昂的記憶體佔用。對於一個小工具來說,太過沉重。(這也是我優先選擇 Zed.dev 作為主要編輯器的原因) Tauri: 效能極佳,體積小,但後端需要寫 Rust。雖然 Rust 很香,但有時候只想用最熟悉的 Go 快速把邏輯刻出來。 Wails: 與 Tauri 類似,採用前端網頁技術結合系統原生 WebView,但後端語言是 Golang。 既然選了 Wails,前端該用什麼? 現代前端框架(React, Vue, Svelte)通常需要建置複雜的 SPA(Single Page Application),並透過 JSON API 與後端溝通。但對於一個狀態並不複雜的工具軟體,不想在前端再維護一份狀態。 ...

Clean Craftsmanship:在 LLM 時代重拾軟體工匠精神

Clean Craftsmanship:在 LLM 時代重拾軟體工匠精神

為什麼在這個時間點讀這本書 最近對於如何在 LLM 時代帶領團隊一起提昇生產力感到困惑。當 AI 能在幾秒鐘內生成幾百行程式碼,「寫程式」這件事的門檻似乎降到了前所未有的低點。但門檻低了,品質呢? 當「每個人」都在養龍蝦,有了生產力焦慮,TypeScript 寫的 OpenClaw 才剛出現,Python 的 NanoBot 馬上跟上,接著是 Golang 的 picoclaw,然後 Rust 的 ZeroClaw 也來了。 根據 ZeroClaw 做的 benchmark,這些 Agent 已經降到小於 5MB 的記憶體佔用與 10ms 的啟動時間,「種族」為人類的我們對於產出軟體來說還剩什麼? 帶著這個問題,我決定先從自身出發,找出在 LLM 時代還能保持軟體工程「工匠精神」的誘因。於是翻開了 Robert C. Martin 的 Clean Craftsmanship。 這本書分為三個部份:紀律、標準、道德。一半以上的篇幅在講述 TDD 這個老生常談的開發方式,但透過 TDD,我們更能知道何謂軟體的「品質」。以下是我特別書籤的幾個段落。 紀律 童子軍規則 離開營地時,要比你來時更乾淨。 這也是在 Clean Code 一書就提到的概念。每次微小的重構,都能小程度的減少技術債的產生。不需要一次大刀闊斧,只要每次經過一段程式碼時,順手讓它變得更好一點。 讓我想到 Claude is not a senior engineer (yet) 這篇文章中提到的 Sweeks——一位被稱為「園丁」的 distinguished engineer,他不斷地重寫、收緊抽象,讓經過他手的程式碼都變得更乾淨。我們都想成為 Sweeks,對吧? 在 LLM 時代,AI 擅長的是「組裝」現有解決方案,但它缺乏 Sweeks 那種「看到可以更好的地方就會忍不住動手」的靈魂。童子軍規則提醒我們:這份靈魂不能丟。 Test Doubles 的正名 書中透過實戰的例子講述了所有 test double:Dummy、Stub、Spy、Mock、Fake。 坦白說,我曾經在諸多 repo 中看到這些名詞卻沒有實際使用它們。頂多在 DI 時製作了一個「用於模擬 database repository 的 implement」,或是使用了 gomock 這種套件來產生 mock,然後把所有替身都統稱為 mock。 ...

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

Golang 1.26 新特性在數量上史無前例的多

隨著時間來到 2026 年初,Go 語言迎來了 1.26 版本的更新。如果說 Go 1.18 的泛型是語言層面的重大變革,那麼 Go 1.26 則是在「數量」與「廣度」上讓人感到驚艷的一次釋出。從語言特性的語法糖、標準庫的實用擴充,到 Runtime 效能的顯著提升(Green Tea GC),甚至是實驗性的 SIMD 支援,這次的更新內容豐富到讓人目不暇給。 本文將挑選其中幾個我認為對日常開發最重要、或最有趣的改動來進行介紹。 語言層面的改動 new(expr):終於不用再寫輔助函數了 在 Go 1.26 之前,如果我們想要取得一個基本型別(如 int, bool, string)的 pointer,通常需要宣告一個變數或者寫一個輔助函數。這在定義 struct 的 literal 時特別煩人,尤其是當 struct 欄位是 *bool 或 *int 用來區分「零值」與「未設定」的時候。 回顧過去,我們為了這個小需求付出了不少努力: 在 Go 1.18 泛型出現之前:我們經常需要定義一堆如 Int64Ptr(v int64) *int64 或 Float64Ptr(v float64) *float64 的輔助函數(AWS SDK 的使用者應該對此非常熟悉)。 匿名函數大法:如果不想要定義全域的輔助函數,有時甚至會看到像 enabled := func(b bool) *bool { return &b }(true) 這種冗長且難讀的寫法。 泛型時代:雖然可以用一個通用的 ptr[T] 解決,但還是需要額外的程式碼。 以前我們可能需要這樣做: func ptr[T any](v T) *T { return &v } type Config struct { Enabled *bool } conf := Config{ Enabled: ptr(true), } 在 Go 1.26 中,內建的 new 函數得到了增強,現在它不僅接受型別,還可以直接接受表達式(Expression)。 ...

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 也要跟著改動? ...