半年前寫完 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 的比較,去讀前面連結的那篇。
Struct Literal Field Selectors:終於能直接寫嵌入欄位
以前如果 struct 有欄位是透過內嵌(embedding)帶進來的,寫 literal 時只能乖乖巢狀展開。Go 1.27 讓 struct literal 的 key 可以是任何合法的 field selector,不只是頂層欄位名稱,可以直接指定被 promote 的欄位。
type Base struct {
ID int
}
type Cat struct {
Base
Name string
}
// c := Cat{Base{ID: 42}, Name: "Tama"}
c := Cat{ID: 42, Name: "Tama"}
fmt.Println(c.ID, c.Name) // 42 Tama
這種語法糖不會改變任何語意,但少一層巢狀之後,讀起來確實順眼不少,尤其是那種內嵌了三四層 struct 的老 codebase。
Generalized Function Type Inference:型別推導的守備範圍變大了
以前 generic function 的型別推導只在呼叫時比較好用,遇到型別轉換或是複合字面量(composite literal)這種場景,常常還是得手動把 type argument 寫出來。Go 1.27 把推導的適用範圍擴大到所有使用 generic function 的情境。
func Double[T int | float64](v T) T { return v * 2 }
func Square[T int | float64](v T) T { return v * v }
// 這是一個 map literal(複合字面量),
// 塞進去的 Double、Square 完全不用手動寫成 Double[int]、Square[int]。
ops := map[string]func(int) int{
"double": Double,
"square": Square,
}
fmt.Println(ops["double"](21)) // 42
fmt.Println(ops["square"](5)) // 25
Double 和 Square 塞進 map literal 時完全不用手動實例化,編譯器自己推得出來。屬於那種平常不會特別注意到,但少了它會覺得卡卡的改動。
標準庫畢業季:JSON v2 與 UUID
encoding/json 換引擎,v2 成為預設實作
encoding/json/v2 從實驗性套件畢業了,encoding/json 現在底層直接由 v2 實作驅動。行為上大致保持相容,主要差異是部分錯誤訊息的文字內容不同。
type Cat struct {
Name string `json:"name"`
Age int `json:"age"`
}
data, err := json.Marshal(Cat{Name: "Mittens", Age: 3})
fmt.Println(string(data), err)
// 輸出: {"name":"Mittens","age":3} <nil>
要注意的一個關鍵差異是,v2 預設不會對 map key 排序(換來更快的序列化速度),如果你的服務依賴穩定輸出(例如拿 JSON 結果去做 hash 比對、或是 snapshot 測試),需要另外指定 json.Deterministic 選項才能維持跟以前一樣的行為。升級前建議搜一下專案裡有沒有依賴 map key 排序順序的地方。
標準庫終於有自己的 uuid 套件
這大概是這次release我最無感動但最實用的一個。長年以來 Go 生態圈產生 UUID 幾乎都靠 google/uuid 這個第三方套件,Go 1.27 把它收進標準庫了,新的頂層 uuid 套件依照 RFC 9562 產生與解析 UUID,並使用密碼學安全的亂數源。
a := uuid.MustParse("f81d4fae-7dec-11d0-a765-00a0c91e6bf6")
fmt.Println("parsed:", a)
fmt.Println("v4:", uuid.NewV4()) // 隨機
fmt.Println("v7:", uuid.NewV7()) // 時間排序,適合當資料庫 key
NewV7 值得特別提一下,它是時間排序的 UUID,拿來當資料庫的 primary key 比 NewV4 對 B-tree index 友善很多(不會隨機打散寫入位置)。以後新專案應該可以少一個 go.mod 裡的第三方依賴了。
這件事與其說是借鑒 Rust,不如說是在追 Python 的進度。Python 的 uuid module 早在 2006 年 Python 就已經內建了,人家孩子都可以轉生到異世界了,Go 才在 2026 年把這個基本款收進標準庫,晚了整整 20 年,只能說遲到總比不到好。
其他值得注意的標準庫更新
-
strings.CutLast/bytes.CutLast:在最後一次出現的分隔符處切割字串,取代掉一堆手寫LastIndex再切片的寫法。before, after, found := strings.CutLast("/api/v1/users/42", "/") fmt.Printf("%q %q %v\n", before, after, found) // 輸出: "/api/v1/users" "42" true -
math/big.Int.Divide:整數除法終於可以指定明確的捨入模式(Trunc、Floor、Round、Ceil),不用再自己處理負數除法的邊界情況。 -
hash/maphash新增Hasher[T]介面:把Hash和Equal綁在一起,是為了未來的 hash-based 資料結構鋪路。 -
math/rand/v2新增(*Rand).N:終於可以直接從自己的*Rand來源取得有界隨機數,不用再繞一手全域函式。 -
unicode套件從 Unicode 15 升級到 Unicode 17,多了不少新字符和屬性可以用。
開發與除錯:goroutineleak profile 兌現承諾
還記得我在 1.26 那篇 裡寫的嗎?當時 goroutineleak profile 還得靠 GOEXPERIMENT=goroutineleakprofile 才能開,文末我寫了句「預計在 Go 1.27 會預設開啟」。這次真的兌現了,runtime/pprof 現在直接公開 goroutineleak profile,不需要任何 GOEXPERIMENT。
func waitForever() {
done := make(chan struct{})
<-done // 沒有人會 close 這個 channel,goroutine 永遠卡在這裡
}
go waitForever()
runtime.Gosched()
pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
// 輸出: goroutineleak profile: total 1
它掃描 GC 的過程去找出那些永久卡在同步原語上、又不可能被其他 runnable goroutine 喚醒的孤兒 goroutine。對於那種「服務跑一週後記憶體緩慢變大」的排查場景,這是可以直接掛進生產環境監控的功能,不用再等到 OOM 才 dump 一堆 goroutine stack 大海撈針。
順帶一提,traceback 現在也會在每個 goroutine 的標題行帶上 runtime/pprof 的 label,方便在 crash dump 或 SIGQUIT 的輸出裡直接看出這個 goroutine 是在處理哪個 request。
ctx := context.Background()
pprof.Do(ctx, pprof.Labels("job", "resize-image"), func(ctx context.Context) {
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
fmt.Printf("%s", buf[:n])
})
// 輸出包含: goroutine 1 [running] {job: resize-image}:
搭配 goroutineleak profile 一起看,排查併發問題的體驗確實好上不少。
Runtime 與效能:更精細的記憶體配置
編譯器現在會針對小於 80 bytes 的物件,直接呼叫「size-specialized」的記憶體配置例程,官方數據是能把這類配置的成本降低最多 30%。代價是二進位檔案會多出約 60 KB,對於配置密集的程式(例如大量小 struct、短生命週期物件的服務)應該會有感。跟 1.26 的 Green Tea GC、cgo 呼叫瘦身一樣,屬於那種不用改一行 code 就能拿到的效能紅利。如果不放心,可以用 GOEXPERIMENT=nosizespecializedmalloc 關掉觀察差異。
安全性:後量子時代的簽章
新的 crypto/mldsa 套件實作了 ML-DSA,也就是 FIPS 204 規範的後量子數位簽章方案,提供 MLDSA44、MLDSA65、MLDSA87 三種參數集,數字越大代表安全強度越高,但簽章也越大。
priv, _ := mldsa.GenerateKey(mldsa.MLDSA65())
msg := []byte("mittens says meow")
sig, _ := priv.Sign(rand.Reader, msg, crypto.Hash(0))
fmt.Println("scheme: ", mldsa.MLDSA65())
fmt.Println("sig size:", mldsa.MLDSA65().SignatureSize())
fmt.Println("verified:", mldsa.Verify(priv.PublicKey(), msg, sig, nil) == nil)
// 輸出: scheme: ML-DSA-65, sig size: 3309, verified: true
後量子密碼學(PQC)已經不是紙上談兵的話題了,crypto/mldsa 加上 crypto/x509、crypto/tls 陸續補上的支援,代表 Go 官方在為「量子電腦真的威脅到現有非對稱加密」這件事提前佈局。目前應該還輪不到大部分人的日常專案,但如果你的服務有合規要求(金融、政府相關),這是值得先關注起來的方向。
實驗性功能:可移植的 SIMD
1.26 帶來的 simd/archsimd 是針對 amd64 的低階存取,1.27 更進一步加入了實驗性的 simd 套件,走的是可移植、向量寬度無關的設計,Float32 在一台機器上可能對應 4 個 lane,在另一台可能是 16 個,寫程式的人不用管底層硬體細節。
weights := []float32{0.5, 1.5, 2.5, 3.5}
bias := []float32{1, 1, 1, 1}
vw := simd.LoadFloat32s(weights)
vb := simd.LoadFloat32s(bias)
result := vw.Add(vb)
out := make([]float32, result.Len())
result.Store(out)
fmt.Println(out) // 輸出: [1.5 2.5 3.5 4.5]
需要 GOEXPERIMENT=simd 才能開啟。跟我上次寫的一樣,SIMD 再快也快不過 GPU 在 LLM 訓練推論上的地位,但拿來加速一些邊緣運算場景(Vector DB 的 embedding、tokenization、影像前處理)應該會愈來愈有感,尤其現在 runtime 自己內部也開始吃這個新套件的紅利。
這種「不綁死向量寬度」的設計其實不是 Golang 獨有的巧思,比較像是低階 SIMD library 的通用做法。C++ 的 std::experimental::simd(脫胎自 Vc library)、Google 的 Highway,還有 Java 的 Vector API,走的都是類似的哲學:不讓開發者寫死 lane 數量,讓同一份 code 在編譯期依據目標硬體展開成對應寬度的指令。反而 Rust nightly 的 std::simd(portable_simd)走的是另一條路,開發者得自己指定固定的 lane 數 N(例如 Simd<f32, 8>),再交給編譯器想辦法在不同硬體上把這個 N 落地,跟 Go 這次完全不讓你選寬度的做法方向不太一樣。真要說 Go 1.27 這次比較像抄誰的作業,大概比較接近 C++、Java 陣營,而不是 Rust。
測試工具的小確幸
synctest.Sleep
我在 testing/synctest 初體驗 那篇裡,靠 synctest.Wait() 解決了測試中魔法數字 time.Sleep 的問題。Go 1.27 幫「推進虛擬時鐘再等所有 goroutine 穩定」這個常見組合包了一層 synctest.Sleep,一次呼叫搞定。
synctest.Test(t, func(t *testing.T) {
start := time.Now()
go func() {
time.Sleep(3 * time.Second)
fmt.Println("worker done at", time.Since(start))
}()
synctest.Sleep(5 * time.Second)
fmt.Println("main resumed at", time.Since(start))
})
// 輸出: worker done at 3s, main resumed at 5s
以前得自己寫 time.Sleep 加 synctest.Wait() 兩行,現在一行搞定,算是小改動但用起來蠻順手的。
httptest.NewTestServer:記憶體內測試伺服器
httptest.NewTestServer 建立的伺服器由記憶體內的 fake network 支撐,不會真的佔用 TCP port,測試結束會自動清理,不用再手動 defer srv.Close()。
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "pong")
})
srv := httptest.NewTestServer(t, handler)
resp, _ := srv.Client().Get(srv.URL)
body, _ := io.ReadAll(resp.Body)
fmt.Print(string(body))
// 輸出: pong
對於跑大量整合測試的 CI 來說,不用搶佔真實 port 這件事其實蠻重要的,過去偶爾會遇到 port 被佔用導致測試 flaky 的情況,這個算是順手解決了。
其他值得一提的改動
time.After、time.NewTimer、time.NewTicker回傳的 channel 現在一律是無緩衝的,相關的asynctimerchanGODEBUG 開關也被移除,時間處理的行為更一致。- HTTP/1 的
Response.Body在Close()時會把還沒讀完的內容排空,讓連線可以被重用,可以用Transport.DisableKeepAlives關閉這個行為。 - HTTP/2 伺服器開始遵守 RFC 9218 的 client priority 訊號,
Server.DisableClientPriority = true可以恢復舊行為。 crypto/x509在 Windows 和 macOS 上也會尊重SSL_CERT_FILE、SSL_CERT_DIR環境變數,從磁碟載入根憑證,方便跨平台的憑證管理統一。
另外還有一些藏在角落的改動,像是 HTTP/2 終於從 h2_bundle.go 變成真正獨立的套件、HTTP/3 也在標準庫裡悄悄成形(還沒 export),有興趣的話值得自己去挖一下官方 release notes。
寫在最後
Go 1.26 的關鍵字是多,Go 1.27 的關鍵字更像是兌現。generic method 從 proposal 等到落地,goroutineleak profile 從實驗性選項轉正成預設功能,encoding/json/v2 畢業成為預設實作,連 UUID 這種生態圈用了多年的第三方套件都被收進標準庫。這種「把欠的債一次還清」的release,對長期維護服務的人來說其實比單一個大特性更讓人安心,代表這些東西已經被夠多人在生產環境驗證過了。