Go 1.27 ra mắt API JSON mới: encoding/json/v2

Công nghệ05 tháng 11, 2026·6 phút đọc

Go 1.27 chính thức đưa gói encoding/json/v2 và encoding/json/jsontext vào trạng thái ổn định, khắc phục hàng loạt lỗi hành vi của encoding/json cũ và tăng tốc giải mã JSON lên tới 10 lần. Gói cũ vẫn được hỗ trợ vô thời hạn và v1 được triển khai dựa trên v2 để đảm bảo tương thích hai chiều.

Go 1.27 ra mắt API JSON mới: encoding/json/v2

Go 1.27 chính thức đưa hai gói encoding/json/v2 và encoding/json/jsontext vào trạng thái ổn định, sau khoảng 6 năm phát triển. Đây là bản nâng cấp lớn đầu tiên của JSON trong thư viện chuẩn Go, nhằm sửa những khiếm khuyết tồn tại từ trước Go 1.0 và mang lại hiệu năng vượt trội. Đáng chú ý, gói encoding/json cũ vẫn được hỗ trợ vô thời hạn và có thể dùng song song với gói mới.

Vì sao cần một API JSON mới?

encoding/json đã phục vụ tốt trong hơn 15 năm, nhưng người dùng dần chỉ ra nhiều vấn đề cố hữu. Những vấn đề này bắt nguồn từ chính thiết kế API cũ nên gần như không thể sửa trong phạm vi lời hứa tương thích Go 1.

Các lỗi hành vi đáng chú ý gồm:

  • Chấp nhận UTF-8 không hợp lệ, trong khi chuẩn RFC 8259 yêu cầu UTF-8 hợp lệ, dẫn tới nguy cơ âm thầm làm hỏng dữ liệu.
  • Chấp nhận object có tên thành phần trùng nhau — điều từng bị khai thác trong CVE-2017-12635 — thay vì báo lỗi.
  • Marshal slice hoặc map nil thành null, gây lỗi khi bên nhận không cho phép null cho kiểu mảng hoặc object.
  • Giải mã không phân biệt hoa thường khi khớp tên thành phần JSON với tên trường struct, một mặc định gây bất ngờ và tiềm ẩn rủi ro bảo mật.
  • Gọi method MarshalJSON trên con trỏ không nhất quán, nhưng không thể sửa vì quá nhiều ứng dụng phụ thuộc hành vi hiện tại.

Về API, việc giải mã đúng từ io.Reader rất khó — json.NewDecoder(r).Decode(v) không từ chối dữ liệu thừa phía sau. Các tùy chọn chỉ đặt được trên Encoder/Decoder mà không dùng được với hàm Marshal/Unmarshal, và không thể truyền xuống các method tùy biến. Các hàm Compact, Indent, HTMLEscape lại chỉ ghi vào bytes.Buffer.

Sơ đồ phân tách giữa tầng cú pháp và tầng ngữ nghĩa trong API JSON mới của GoSơ đồ phân tách giữa tầng cú pháp và tầng ngữ nghĩa trong API JSON mới của Go

Về hiệu năng, MarshalJSON buộc phải cấp phát []byte, còn UnmarshalJSON buộc phải phân tích toàn bộ giá trị rồi phân tích lại lần nữa. Cả hai gói đều đệm toàn bộ giá trị JSON trong bộ nhớ, và nếu method tùy biến gọi đệ quy Marshal/Unmarshal thì hiệu năng suy giảm theo cấp số nhân.

jsontext: nền tảng xử lý cú pháp

Gói encoding/json/jsontext chỉ lo phần cú pháp — tức việc xử lý JSON theo ngữ pháp RFC 8259 — và không phụ thuộc vào reflection. Gói này cung cấp Encoder và Decoder hoạt động thực sự theo dạng streaming, cùng các kiểu Value (tương đương RawMessage cũ, nay là type alias) và Token không cấp phát bộ nhớ.

API cơ bản của jsontext:

package jsontext

type Encoder struct { ... }
func NewEncoder(io.Writer, ...Options) *Encoder
func (*Encoder) WriteValue(Value) error
func (*Encoder) WriteToken(Token) error

type Decoder struct { ... }
func NewDecoder(io.Reader, ...Options) *Decoder
func (*Decoder) ReadValue() (Value, error)
func (*Decoder) ReadToken() (Token, error)

Sự tách bạch giữa cú pháp (encode/decode) và ngữ nghĩa (marshal/unmarshal) là điểm cốt lõi: jsontext xử lý cú pháp, còn json/v2 gán ý nghĩa cho dữ liệu cú pháp đó.

encoding/json/v2: API mới

Gói encoding/json/v2 được thiết kế quen thuộc với người dùng cũ, với mục tiêu các đoạn mã v1 chuyển sang v2 sẽ chạy gần như giữ nguyên. API cơ bản:

package json

func Marshal(in any, opts ...Options) (out []byte, err error)
func MarshalWrite(out io.Writer, in any, opts ...Options) error
func MarshalEncode(out *jsontext.Encoder, in any, opts ...Options) error
func Unmarshal(in []byte, out any, opts ...Options) error
func UnmarshalRead(in io.Reader, out any, opts ...Options) error
func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) error

Điểm khác biệt lớn là tùy chọn (Options) trở thành tham số hạng nhất cho mọi hàm marshal và unmarshal, thay vì chỉ gắn được trên Encoder/Decoder.

V2 bổ sung hai interface tùy biến kiểu MarshalerTo và UnmarshalerFrom, cho phép kiểu dữ liệu tự biểu diễn qua jsontext.Encoder/Decoder. Nhờ đó tùy chọn có thể truyền xuống theo call stack. Bên cạnh đó, phía gọi hàm cũng có thể chỉ định cách biểu diễn JSON cho bất kỳ kiểu nào qua WithMarshalers/WithUnmarshalers, vốn được ưu tiên hơn method của kiểu hoặc biểu diễn mặc định.

Những thay đổi hành vi quan trọng

Dù nhắm tới hành vi gần giống v1, v2 thay đổi một số điểm để khắc phục lỗi cũ:

  • Báo lỗi khi gặp UTF-8 không hợp lệ.
  • Báo lỗi khi object JSON có tên thành phần trùng nhau.
  • Marshal slice hoặc map nil thành mảng hoặc object rỗng tương ứng, thay vì null.
  • Giải mã object JSON vào struct theo khớp phân biệt hoa thường.
  • Định nghĩa lại omitempty để bỏ trường nếu nó mã hóa thành giá trị JSON "rỗng" (null, "", [], {}).
  • Báo lỗi khi tuần tự hóa time.Duration, vốn chưa có biểu diễn mặc định, nhưng cung cấp tùy chọn để người gọi tự quyết định.

Hầu hết thay đổi hành vi đều có struct tag hoặc tùy chọn để điều chỉnh theo ngữ nghĩa v1, v2 hoặc hành vi tùy biến khác.

Hiệu năng và tương thích

Hiệu năng Marshal của v2 xấp xỉ v1 — đôi khi nhanh hơn, đôi khi chậm hơn chút. Ngược lại, Unmarshal nhanh hơn đáng kể, với benchmark cho thấy cải thiện tới 10 lần. Để tận dụng tối đa, các kiểu đã cài Marshaler/Unmarshaler nên chuyển sang MarshalerTo/UnmarshalerFrom nhằm xử lý streaming hoàn toàn.

Ví dụ thực tế: việc phân tích đệ quy đặc tả OpenAPI trong method UnmarshalJSON từng làm chậm nghiêm trọng một dịch vụ của Kubernetes (kubernetes/kube-openapi#315); chuyển sang UnmarshalJSONFrom giúp hiệu năng tăng lên nhiều bậc. Nhóm phát triển cho biết sẽ tiếp tục tối ưu trong Go 1.28.

Về tương thích, gói v1 vẫn được hỗ trợ vô thời hạn và không bắt buộc chuyển đổi. Điểm mấu chốt là v1 được triển khai dựa trên v2 cùng bộ tùy chọn tương thích đầy đủ, nên hai phiên bản dùng chung được trong cùng một hệ sinh thái: một gói dùng v2 vẫn marshal được kiểu có MarshalJSON của v1, và ngược lại v1 hỗ trợ ngầm MarshalJSONTo của v2. Cách này cũng cho phép chuyển đổi dần từng đoạn mã, ví dụ chọn toàn bộ hành vi v2 trừ việc cho phép tên trùng, hoặc ngược lại.

Góc nhìn

Với lập trình viên Go tại Việt Nam, đây là thời điểm hợp lý để thử nghiệm encoding/json/v2 trong các dự án mới, đặc biệt là dịch vụ API có lưu lượng lớn — nơi mức tăng tới 10 lần ở khâu giải mã có thể giảm đáng kể CPU và độ trễ. Với mã cũ, không cần vội chuyển đổi: chiến lược an toàn là bật các tùy chọn v2 cho từng hành vi riêng lẻ, chạy kiểm thử để phát hiện chỗ phụ thuộc vào mặc định cũ như khớp tên không phân biệt hoa thường hay null từ slice nil.

Các nhóm đang dùng thư viện JSON bên thứ ba hoặc tự viết bộ tuần tự hóa nên cân nhắc chuyển sang jsontext làm lớp nền cú pháp, vì nó thuần streaming và không qua reflection. Bài học từ Kubernetes cho thấy các method tùy biến gọi đệ quy là điểm nghẽn hiệu năng thường bị bỏ qua, nên khi nâng cấp, hãy rà lại những kiểu như vậy trước tiên.

Nguồn: go.dev · Quy trình biên tập

Chia sẻ:FacebookX