Go encoding/json JSON 後端開發 效能優化

Go 1.27 把 encoding/json 拆成 jsontext 加 v2 兩層:case-insensitive 匹配、nil slice 序列化成 null 這些預設全被翻案,UnmarshalerFrom streaming 把 kube-openapi 拉快 40 倍

Go 1.27 於 2026 年 8 月釋出,encoding/json 十五年來第一次動語意層——拆出 jsontext 處理語法、v2 負責語意映射、v1 從此靠 v2 內部驅動維持相容。四個沿用十五年的預設行為被當作 bug 翻案,UnmarshalerFrom streaming 介面讓 kube-openapi 那類巢狀 parse 場景把 O(n²) 拉回 O(n)。本文拆解分層邏輯、破壞性改變的實際影響與遷移策略。

encoding/json 從 Go 1.0 就已經在標準函式庫裡,用了十五年,語意層沒動過。這包東西是 Go 生態最被依賴的 encoding 套件之一,也累積了不少「大家以為是 feature、其實是實作偷懶」的預設行為。8 月釋出的 Go 1.27 把整包拆成 encoding/json/jsontextencoding/json/v2 兩層架構,把 v1 的內部改用 v2 實作驅動,維持 Go 1 相容承諾的同時,讓新程式碼有機會走 v2 拿到效能。

翻案的四個預設行為在遷移現場最痛。case-insensitive 欄位比對從此關閉、nil slice/map 不再序列化成 null、無效 UTF-8 直接吐錯、重複 key 也不再靜默覆寫。這幾件事任何跨語言 API 的實作者都碰過對應的 bug,但改成正確語意的同時,也意味著大量現有的整合測試會爆。本文拆解為什麼 Go 團隊選在 1.27 動這一刀、jsontext 跟 v2 的分層在做什麼、以及升級 1.27 之前該預先跑過哪些檢查。

為什麼一個 encoding 套件要拆成兩層

Go 團隊主導這次重寫的 Joe Tsai 從 2020 年開始就在 github.com/go-json-experiment/json 累積實作,2025 年進 GOEXPERIMENT,1.25 版可以用 GOEXPERIMENT=jsonv2 打開,1.27 直接進主線。整段動機在原本 v1 的架構問題上:解析、驗證、映射三件事糾纏在同一組 code path 上,任何自訂 UnmarshalJSON 都必須自己重新走一次 tokenizer,這在深巢狀 JSON 上很容易掉進 O(n²) 的漩渦。

新架構把「JSON 是什麼」跟「Go 要怎麼對應」拆開。jsontext 只負責 syntactic 層——tokenize、驗證合法性、產出 Value 這種一等公民的 JSON 節點。json/v2 負責 semantic 層——把 jsontext.ValueDecoder 對應到 Go 型別。這個切分讓自訂 unmarshaller 可以直接消費 stream,不用像 v1 那樣先把 raw bytes 重新丟回 json.Unmarshal 產生第二輪 parse。

Kubernetes 的 kube-openapi 是被引用最多的效能案例。那邊為了處理 OpenAPI schema 裡巢狀很深的物件,過去只能靠手寫 unmarshaller,遞迴一層 parse 一層,實測換到 UnmarshalerFrom 之後某些 schema 從幾百毫秒掉到十幾毫秒。這不是普通的常數倍加速,是複雜度階級的變化。

沿用十五年的四個預設,v2 一次翻案

case-insensitive 欄位比對是最刺人的一項。v1 允許 {"USER_ID":"abc"} 對到標籤 json:"user_id" 的欄位,行為看起來寬容,實際上讓 API 契約變得含糊——client 傳大小寫錯的欄位不會報錯、靜默對進來。v2 預設嚴格比對,欄位大小寫對不上就是零值,不會 raise error。需要保留舊行為的話得在 encode/decode 呼叫時傳入 json.MatchCaseInsensitiveNames(true) 選項;掛在 struct tag 上則可以逐欄位標記 case:ignore

nil slice/map 序列化影響最廣泛。v1 把 nil slice 寫成 null、v2 寫成 [];map 同理,nil{} 而不是 null。這個改變在型別語意上更接近多數 client library 的預期,但直接踩爆的是所有那些「用 null[] 區分『沒有這個欄位』跟『欄位存在但空』」的 REST API 設計。舊行為要用 json.FormatNilSliceAsNull(true)json.FormatNilMapAsNull(true) 補回來。

第三項是無效 UTF-8 處理。v1 遇到 lone surrogate 或 invalid byte 會靜默替換成 ,v2 直接吐 error。這個變動比想像中影響大——任何從 legacy 資料庫(存 latin1 假裝 UTF-8 的那種)撈出來直接 marshal 的 pipeline 會全數失敗。

第四項是重複 key 的處理。v1 允許 JSON 物件裡出現同一個 key 兩次、後者覆寫前者;v2 預設 raise 錯誤。RFC 8259 本來就允許實作 reject,v1 的寬容是為了跟 JavaScript 對齊,實際上開了不少 parser confusion 攻擊的門。過去多年出現過的 SSO token 混淆、JSON 型 SQL injection 都有這條語意分歧的影子在。

MarshalerTo / UnmarshalerFrom 才是真正的效能來源

換 import 路徑不會白拿到 10 倍加速,效能主要來自新的 streaming 介面。v1 的 Marshaler/Unmarshaler 介面吃 []byte、吐 []byte,實作端必須跟 buffer 打交道;v2 的 MarshalerTo/UnmarshalerFrom 直接吃 jsontext.Encoder/Decoder,可以在 stream 上原地寫入或消費 token,跳過中間那層 []byte copy。

技術面差異看起來抽象,實際上決定了大量現有函式庫在 v2 上的效能天花板。任何為了控制序列化格式而實作 UnmarshalJSON 的型別——常見的例子有時間格式、UUID、Decimal——在 v2 上維持舊介面時效能等同於 v1,改實作 UnmarshalJSONFrom 才能拿到零額外配置的解析路徑。

實測數字:v2 對 struct 型別的 unmarshal 大約是 v1 的 2.7 到 10 倍快,記憶體配置次數少 60% 上下。這個效能不是靠語意妥協換來的,是新架構把配置從 heap 拉回 stack、把重複 tokenize 消掉的結果。marshal 端提升沒這麼誇張,因為 v1 本來就寫得還可以,v2 的 marshal 大致跟 v1 打平或略快,主要收益還是 unmarshal。

遷移策略:GOEXPERIMENT 開一週再決定

實務上升級 Go 1.27 之前有一條務實路徑:在現有的 1.25/1.26 上跑 GOEXPERIMENT=jsonv2 go test ./...,讓現有的整合測試在 v2 語意下重跑一遍。四個 breaking changes 影響到的 code path 幾乎都會在測試層面浮現——case-sensitive matching 讓 API contract test 掉光、nil slice/map 讓 JSON snapshot test 全部 diff、UTF-8 讓資料庫 fixture 的整合測試炸開。

真的動手升級之後有兩種策略。追求穩定的服務把 breaking changes 用 json.WithFlags 或個別的 Format* 選項全部復原,維持 v1 語意的同時拿到 v2 內部實作的部分效能收益。這條路成本最低,缺點是拿不到 streaming 介面帶來的階級加速。

追求效能的服務把主流量的 marshaller/unmarshaller 改成 MarshalerTo/UnmarshalerFrom 實作,其他次要型別維持 v1 介面。這個混合方式在中大型服務上實測的收益最明顯,代價是要重寫幾個核心型別的序列化邏輯。多數團隊建議走這條——REST API handler 保守派用選項復原、SDK 內部的高熱路徑走新介面。

第三方套件的生態這波遷移會很長。encoding/json 的介面本來就外露到極多套件裡——ORM 的 Scan/Value、gRPC-Gateway 的 payload 轉譯、各家 config loader、每一種 SDK 的 REST client——只要這些套件還在用 v1 介面,服務就沒辦法完全走 streaming 路徑。Go 1.27 這波只是把時鐘啟動,生態要跟上估計還要一年。

動手升級之前該先做的兩件事

線上服務短期內留在 Go 1.25/1.26 的 v1 沒問題,v1 不會被 deprecated,Go 團隊明確承諾了。但任何新開的專案,即使暫時綁在 Go 1.25 上,也應該把型別的自訂 marshaller 直接寫成 UnmarshalerFrom 介面——這個介面在 1.25 已經可以用,只是要透過 GOEXPERIMENT 打開;未來版本升到 1.27+ 就是零成本啟用。

現有服務要動手改的話,第一輪 audit 集中在兩個地方:所有實作了 MarshalJSON/UnmarshalJSON 的型別、以及所有回應 JSON 包含 slice/map 欄位的 API handler。前者決定了效能上限、後者決定了 API 契約是否會斷。

跑在 VPS 上的 Go 服務——不管是 gRPC-Gateway、Gin、Fiber,還是自家長時間跑的 daemon——這波升級的實際收益在單機 QPS 上會很明顯。JSON encode/decode 在多數 API 服務的 profile 上佔 15% 到 30% CPU 是常態,10 倍加速就算稀釋掉還是十幾個百分點的整體吞吐提升。這對高頻 API、串流服務、資料 pipeline 的成本結構影響直接。

NCSE Network 在臺灣是方電訊機房提供搭載 Intel Gold CPU 與 NVMe SSD 的 VPS 主機,適合部署高 QPS 的 Go 後端服務跟 Go 1.27 遷移的實測環境。想找一個穩定的地方跑起自架後端、把 encoding/json/v2 的實際效益量到自家 workload 上的團隊,可以參考 NCSE Network 的方案。

需要技術開發支援?

NCSE Network 提供 Discord Bot、LINE Bot、AI Agent、爬蟲、監控系統等客製化開發服務,從規劃到上線一站式完成。

洽談專案 →