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 .proto và 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.

Ảnh chụp cây quyết định nền tối chọn định dạng serialization, bạn cần gì nhất người đọc được debug bằng mắt API công khai linh hoạt JSON mặc định tốt phần 1 chậm tốn alloc đường nóng JSON tối ưu Encoder RawMessage phần 2, nhỏ cộng giải mã nhanh cộng đa ngôn ngữ cộng hợp đồng schema Protobuf nhỏ 2x giải mã nhanh 10x cần proto phần 3-4, chỉ Go nói với Go luồng bền RPC nội bộ cache checkpoint gob không schema ngoài lợi khi stream nhiều phần 5, nhị phân nhanh nhưng giữ linh hoạt kiểu JSON đa ngôn ngữ MessagePack JSON nhị phân decode nhanh 2x phần 6, chuẩn hoá IETF bảo mật IoT WebAuthn COSE token CBOR tag ngữ nghĩa RFC 8949 phần 7, đọc chọn lọc vài field từ message lớn 0 cấp phát mmap zero-copy FlatBuffers Cap'n Proto phần 11, khi nào cứ JSON là đủ API CRUD thông thường dữ liệu nhỏ lưu lượng vừa cần debug bằng mắt log đọc được đối tác gọi bằng curl chưa đo được serialization là nút cổ chai nếu vướng băng thông JSON cộng gzip thu hẹp gần hết khoảng cách phần 9 chọn định dạng phức tạp hơn chỉ khi đo thấy đáng

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:

Ảnh chụp bảng so sánh tổng nền tối output thật, một kích thước byte 1 bản ghi Protobuf 205 nhỏ hơn CBOR 326 nhỏ hơn MessagePack 378 nhỏ hơn JSON 407 nhỏ hơn gob 443 gob 1 giá trị gồm mô tả kiểu stream nhiều thì khoảng 215 mỗi giá trị phần 5, hai mã hóa marshal ns op và allocs op gob tái dùng 340.4 ns 1 alloc Protobuf 348.6 ns 18 allocs bản thủ công code sinh tối ưu hơn CBOR 404.8 ns 2 allocs JSON 505.9 ns 2 allocs MessagePack 735.4 ns 6 allocs, ba giải mã unmarshal ns op và allocs op khác biệt lớn Protobuf 142.5 ns 5 allocs nhanh nhất bản rút gọn MessagePack 1172 ns 17 allocs CBOR 1236 ns 13 allocs JSON 2793 ns 21 allocs chậm nhất parse text cộng reflection, rút ra từ số đo nhỏ nhất Protobuf đọc người JSON nhị phân linh hoạt MessagePack CBOR giải mã nhị phân nhanh hơn JSON nhiều JSON đắt nhất ở decode nén gzip san bằng kích thước zero-copy vô địch đọc chọn lọc 0 alloc không có tốt nhất tuyệt đối chọn theo bài toán đo trên dữ liệu thật

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 protoc tố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ề

  1. 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.
  2. 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á.
  3. Đừ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

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.