Suốt 11 phần, ta đã mổ xẻ từng định dạng serialization — mỗi cái với triết lý và đánh đổi riêng. Bài cuối này (phần 12) không thêm định dạng mới; nó trả lời câu hỏi thực dụng nhất: khi đứng trước một bài toán, chọn cái nào? Và để không nói suông, mình chạy thật cả năm định dạng chính trên cùng một đơn hàng rồi đặt lên một bảng cân: kích thước, tốc độ mã hóa, tốc độ giải mã, số lần cấp phát.
Điểm lại: mỗi định dạng mạnh ở một chỗ
- JSON (phần 1–2): linh hoạt, người đọc được, chạy khắp nơi — nhưng lớn và giải mã chậm. Tối ưu bằng
Encoder/RawMessageđể giảm cấp phát. - Protobuf (phần 3–4): nhỏ ~2 lần, giải mã nhanh ~10 lần — đổi lại cần schema
.protovà bước sinh code. - gob (phần 5): định dạng riêng của Go, tự mô tả kiểu, lợi khi stream nhiều giá trị — nhưng chỉ Go-to-Go.
- MessagePack (phần 6): "JSON nhị phân", linh hoạt và đa ngôn ngữ, decode nhanh hơn JSON — nhưng chỉ nhỏ hơn JSON chút ít.
- CBOR (phần 7): chuẩn IETF với tag ngữ nghĩa, nền tảng của WebAuthn/COSE/IoT.
- varint/ZigZag (phần 8): kỹ thuật nền cho mọi định dạng nhị phân — số nhỏ ít byte, ZigZag cứu số âm.
- Nén (phần 9): gzip san bằng lợi thế kích thước giữa JSON và Protobuf.
- Schema evolution (phần 10): field number cho phép thêm/bớt field an toàn.
- Zero-copy (phần 11): đọc field bằng offset, 0 cấp phát — vô địch khi đọc chọn lọc từ message lớn.

Hình 1: Cây quyết định — bắt đầu từ nhu cầu (đọc được/nhỏ/nhanh/đa ngôn ngữ/chuẩn hóa/đọc chọn lọc) rồi chọn định dạng; kèm danh sách các trường hợp cứ JSON là đủ, đừng tối ưu sớm.
Đo thật: bảng so sánh tổng
Mình mã hóa cùng một đơn hàng (có struct lồng và 3 mặt hàng) qua JSON, Protobuf, MessagePack, CBOR, gob và đo:

Hình 2: Chạy thật — kích thước Protobuf 205 < CBOR 326 < MessagePack 378 < JSON 407 < gob 443; marshal gob/Protobuf/CBOR nhanh hơn JSON, MessagePack chậm nhất; unmarshal Protobuf 142 ns (nhanh nhất) so với JSON 2.793 ns (chậm nhất, 21 allocs).
Đọc kết quả đo được:
- Kích thước: Protobuf 205 byte nhỏ nhất, rồi CBOR 326, MessagePack 378, JSON 407, gob 443 (một giá trị lẻ; stream nhiều thì gob ~215/giá trị). Các định dạng bỏ tên field (Protobuf) hoặc nén số tốt (CBOR) thắng về kích thước.
- Mã hóa: gob tái dùng (340 ns, 1 alloc) và Protobuf (348 ns) nhanh nhất; CBOR 404 ns; JSON 505 ns; MessagePack 735 ns chậm nhất. Lưu ý: Protobuf thủ công tốn 18 allocs (code sinh bằng
protoctối ưu hơn nhiều), gob tái dùng chỉ 1 alloc. - Giải mã — khác biệt lớn nhất: Protobuf 142 ns nhanh vượt trội (bản rút gọn), rồi MessagePack 1.172 ns, CBOR 1.236 ns, và JSON 2.793 ns với 21 allocs — chậm nhất. Đây là bài học xuyên suốt loạt: JSON đắt nhất ở khâu giải mã vì phải parse text và dùng reflection. Trong hệ đọc nhiều, đây thường là điểm nóng thật.
Không có "tốt nhất" tuyệt đối
Điều quan trọng nhất khi khép lại loạt bài: không định dạng nào thắng mọi mặt. Mỗi cái là một điểm trên đường đánh đổi giữa dễ đọc — kích thước — tốc độ — linh hoạt — độ phức tạp vận hành:
- Cần nhỏ nhất và nhanh nhất, chấp nhận schema → Protobuf.
- Cần người đọc được, linh hoạt, chạy khắp nơi → JSON (và gzip nếu lo băng thông).
- Cần nhị phân nhanh nhưng giữ tính động của JSON → MessagePack hoặc CBOR.
- Chỉ Go, luồng bền → gob.
- Đọc chọn lọc field từ message khổng lồ, 0 cấp phát → zero-copy.
Đánh đổi cần cân nhắc
Đừng tối ưu định dạng khi chưa đo. Cám dỗ lớn sau loạt bài này là đổi hết sang Protobuf "cho nhanh". Nhưng với đa số ứng dụng — API CRUD, dữ liệu nhỏ, lưu lượng vừa — chi phí serialization là không đáng kể so với mạng và CSDL, còn JSON cho bạn khả năng debug bằng mắt và đối tác gọi bằng curl. Chỉ chọn định dạng phức tạp hơn khi đo được serialization là nút cổ chai thật.
Kích thước và tốc độ là hai trục khác nhau. Loạt bài cho thấy chúng không luôn đi cùng: MessagePack nhỏ hơn JSON nhưng mã hóa chậm hơn; JSON+gzip nhỏ ngang Protobuf nhưng giải mã vẫn chậm hơn nhiều. Xác định rõ bạn bị giới hạn bởi băng thông (thì ưu tiên kích thước + nén) hay CPU (thì ưu tiên tốc độ giải mã) trước khi chọn.
Số benchmark là tương đối, dữ liệu là tối thượng. Mọi con số trong loạt này đo trên một struct đơn hàng cụ thể trong một container cụ thể. Struct nhiều số nhỏ nghiêng về Protobuf; nhiều chuỗi dài thu hẹp khác biệt; chất lượng thư viện (như fxamacker/cbor tối ưu tốt) ảnh hưởng nhiều như bản thân định dạng. Luôn benchmark trên dữ liệu và thư viện thật của bạn trước khi quyết định.
Ba ý mang về
- Chọn định dạng theo nhu cầu, không theo "cái nào nhanh nhất": đọc-người → JSON, nhỏ+nhanh+schema → Protobuf, nhị phân linh hoạt đa ngôn ngữ → MessagePack/CBOR, Go-to-Go luồng bền → gob, đọc chọn lọc 0 alloc → zero-copy.
- JSON đắt nhất ở giải mã, đó là điểm nóng thật: đo thật unmarshal JSON 2.793 ns/21 allocs so với Protobuf 142 ns, MessagePack/CBOR ~1.200 ns — nếu hệ đọc nhiều và CPU là nút cổ chai, chuyển sang nhị phân đáng giá.
- Đừng tối ưu sớm, và luôn đo trên dữ liệu thật: với đa số ứng dụng JSON là đủ (debug được, chạy khắp nơi, gzip thu hẹp kích thước); chỉ đổi định dạng khi đo được nó là nút cổ chai — không có "tốt nhất" tuyệt đối, chỉ có phù hợp nhất với bài toán.
Nguồn
- Protocol Buffers — Overview: https://protobuf.dev/overview/
- Martin Kleppmann — Designing Data-Intensive Applications (chương 4: Encoding and Evolution): https://dataintensive.net/
- Go — encoding/json, encoding/gob: https://pkg.go.dev/encoding
Loạt Serialization và định dạng dữ liệu khép lại ở đây sau 12 phần: từ JSON quen thuộc (phần 1) tới bài tổng kết này. Xuyên suốt, mỗi phần đều chạy demo thật trong container và báo số đo trung thực — kể cả khi kết quả phản trực giác (JSON+gzip nhỏ hơn Protobuf+gzip, MessagePack chỉ nhỏ hơn JSON 1,08 lần, gob lớn hơn JSON cho một giá trị lẻ, số -1 tốn 10 byte trong varint thuần). Hy vọng bạn rời loạt bài không phải với một "định dạng thắng cuộc", mà với thói quen quan trọng hơn: hỏi mình cần gì — đọc được hay nhỏ hay nhanh — rồi đo trên dữ liệu thật trước khi chọn.