Sau Protobuf (cần schema) và gob (chỉ Go), ta gặp một định dạng hứa hẹn "được cả hai": MessagePack — thường được giới thiệu là "JSON nhị phân". Nó dùng cùng mô hình dữ liệu với JSON (map, array, string, số, bool, nil), tự mô tả nên linh hoạt và liên ngôn ngữ (có thư viện cho hầu hết ngôn ngữ), lại mã hóa nhị phân nên gọn và nhanh hơn. Nghe như bữa trưa miễn phí. Bài này (phần 6 loạt Serialization) chạy thật để xem "gọn hơn" thực sự là bao nhiêu — và con số có thể khiến bạn bất ngờ.

Cơ chế: byte format đứng đầu mỗi giá trị

Dùng MessagePack trong Go gần như y hệt JSON — chỉ đổi hàm:

m, _ := msgpack.Marshal(order)   // struct -> []byte nhị phân
var o Order
msgpack.Unmarshal(m, &o)         // []byte -> struct

Điểm cốt lõi: mỗi giá trị bắt đầu bằng một byte format cho biết kiểu và (với giá trị nhỏ) nhét luôn dữ liệu vào đó. Map nhỏ, chuỗi ngắn, số nhỏ đều gói gọn:

byte format ý nghĩa
0x8N fixmap N field (vd 0x86 = map 6 field)
0xaN fixstr N byte (vd 0xa2 = string 2 byte)
0x00..0x7f số dương 0..127 chỉ 1 byte
0xc0 / 0xc2 / 0xc3 nil / false / true

Điểm mấu chốt — và là lý do MessagePack không nhỏ như Protobuf: nó vẫn lưu tên field ("id", "ma", "khach_hang"...) trong map, giống hệt JSON. Đây là cái giá của tính tự mô tả: bên nhận không cần schema, nhưng mỗi bản ghi phải mang theo tên field.

Ảnh chụp đoạn mã Go nền tối minh hoạ MessagePack, dùng y như JSON đổi hàm Marshal m bằng msgpack Marshal order struct sang byte nhị phân var o Order msgpack Unmarshal m o byte sang struct cùng mô hình JSON map array string int float bool nil, cơ chế byte format đứng đầu mỗi giá trị map nhỏ string ngắn số nhỏ nhét gọn vào byte format 0x8N fixmap N field 0x86 map 6 field 0xaN fixstr N byte 0xa2 string 2 byte 0x00 tới 0x7f positive fixint số 0 tới 127 chỉ 1 byte 0xc0 nil 0xc2 c3 false true khác Protobuf msgpack vẫn lưu tên field id ma trong map tự mô tả linh hoạt nhưng không gọn bằng Protobuf, số nguyên mặc định vs nén UseCompactInts enc UseCompactInts true nén int64 về dạng nhỏ nhất mặc định int64 bằng 0xd3 cộng 8 byte kể cả số nhỏ phí compact 90000 vừa 4 byte nhỏ hơn rõ

Hình 1: MessagePack dùng như JSON (đổi hàm Marshal); mỗi giá trị bắt đầu bằng byte format (0x86 fixmap 6 field, 0xa2 fixstr 2 byte...); nó vẫn lưu tên field như JSON — khác Protobuf; số nguyên nên bật UseCompactInts để nén.

Đo thật: gọn hơn JSON bao nhiêu?

Mình mã hóa cùng đơn hàng bằng MessagePack và JSON, đo kích thước và benchmark:

Ảnh chụp bảng kết quả chạy thật MessagePack output thật, một kích thước cùng dữ liệu JSON 407 byte MessagePack mặc định 378 byte nhỏ hơn JSON 1.08x MessagePack compact 326 byte nhỏ hơn JSON 1.25x để so Protobuf bài 4 205 byte 2x nhỏ hơn nhiều vì Protobuf không lưu tên field msgpack thì có như JSON, hai byte format 12 byte đầu 86 a2 69 64 d3 00 00 00 00 00 01 5f 86 fixmap 6 field a2 69 64 fixstr 2 bằng id d3 int64 cố định 8 byte mặc định compact sẽ nhỏ hơn thấy rõ tên field id nằm ngay trong byte chi phí lặp mỗi record, ba benchmark vs JSON JSONMarshal 478.2 ns op 528 B op 2 allocs op MsgpackMarshal 763 ns op 1120 B op 6 allocs op chậm hơn JSON JSONUnmarshal 2715 ns op 936 B op 21 allocs op MsgpackUnmarshal 1126 ns op 712 B op 17 allocs op nhanh 2.4x, kết luận kích thước chỉ nhỏ hơn JSON 1.08 tới 1.25x không bằng Protobuf 2x mã hóa chậm hơn JSON một chút giải mã nhanh 2.4x JSON đọc byte không parse text điểm mạnh liên ngôn ngữ tự mô tả decode nhanh đổi 1 dòng code

Hình 2: Chạy thật — MessagePack mặc định 378 byte (1,08x nhỏ hơn JSON 407), compact 326 byte (1,25x); byte đầu 86=fixmap6, a2 69 64="id", d3=int64 8 byte; benchmark marshal 763 ns (chậm hơn JSON 478 ns) nhưng unmarshal 1.126 ns (nhanh ~2,4x JSON 2.715 ns).

Đọc kết quả đo được — và đây là chỗ cần trung thực:

  • Kích thước gọn hơn khiêm tốn: MessagePack mặc định chỉ 378 byte so với JSON 407 byte — nhỏ hơn 1,08 lần, gần như không đáng kể. Bật UseCompactInts được 326 byte (1,25 lần). So với Protobuf ở phần 4 (205 byte, ~2 lần), MessagePack lớn hơn hẳn. Lý do lộ rõ khi nhìn byte: a2 69 64 là chuỗi "id" nằm ngay trong dữ liệu — tên field lặp lại mỗi bản ghi, đúng như JSON.
  • Cái bẫy số nguyên mặc định: byte d3 cho thấy thư viện mã hóa id=90000 thành int64 cố định 8 byte — trong khi JSON viết "90000" chỉ 5 byte text! Đây là lý do mặc định gọn ít. Bật UseCompactInts(true) mới nén số về dạng nhỏ nhất, kéo kích thước xuống 326 byte.
  • Mã hóa chậm hơn JSON: MsgpackMarshal 763 ns so với JSONMarshal 478 ns — chậm hơn. encoding/json của Go được tối ưu rất kỹ qua nhiều năm; thư viện MessagePack bên thứ ba không luôn thắng ở khâu mã hóa.
  • Giải mã là điểm mạnh thật: MsgpackUnmarshal 1.126 ns so với JSONUnmarshal 2.715 ns — nhanh ~2,4 lần, vì đọc thẳng byte theo format thay vì parse text. Với hệ đọc nhiều, đây là lợi thế đáng kể.

Đánh đổi cần cân nhắc

MessagePack không phải "Protobuf dễ hơn" — nó là "JSON nhanh hơn". Nếu mục tiêu là nhỏ nhất có thể, MessagePack không phải lựa chọn: nó vẫn mang tên field nên chỉ nhỏ hơn JSON chút ít, thua xa Protobuf. Chọn MessagePack khi bạn muốn giữ sự linh hoạt và tự mô tả của JSON (không schema, dữ liệu động, thêm field tự do) nhưng cần giải mã nhanh hơn và liên ngôn ngữ — đổi đúng một dòng code từ json.Marshal sang msgpack.Marshal.

Nhớ bật compact ints và đo trên dữ liệu thật. Mặc định của thư viện Go này encode số không compact — một cái bẫy khiến nhiều người đo thấy MessagePack "chẳng nhỏ hơn JSON" rồi bỏ. Với struct nhiều số nguyên, UseCompactInts tạo khác biệt thật. Và như mọi bài trong loạt: con số phụ thuộc dữ liệu — struct nhiều số nhỏ sẽ lợi hơn, struct toàn chuỗi dài thì cả hai gần bằng nhau.

Tự mô tả nghĩa là không có kiểm tra schema. Giống JSON, MessagePack không ép kiểu qua schema: bên gửi đổi một field từ số sang chuỗi, bên nhận vẫn giải mã "thành công" vào giá trị sai kiểu nếu không cẩn thận. Bạn được sự linh hoạt nhưng mất lưới an toàn mà Protobuf (có .proto) cung cấp. Tính tương thích khi đổi schema hoàn toàn do bạn tự quản.

Ba ý mang về

  1. MessagePack là "JSON nhị phân": cùng mô hình, tự mô tả, liên ngôn ngữ: đổi đúng một dòng (json.Marshal → msgpack.Marshal), giữ nguyên sự linh hoạt và không cần schema như JSON.
  2. Gọn hơn JSON khiêm tốn vì vẫn lưu tên field: đo thật chỉ nhỏ hơn 1,08 lần (mặc định) đến 1,25 lần (compact ints) — thua xa Protobuf ~2 lần; nhớ bật UseCompactInts để nén số, nếu không số nguyên tốn 8 byte cố định.
  3. Điểm mạnh thật nằm ở giải mã: đo thật unmarshal nhanh ~2,4 lần JSON (1.126 vs 2.715 ns) dù marshal chậm hơn — chọn MessagePack khi cần tốc độ đọc và liên ngôn ngữ mà vẫn muốn giữ sự tiện của JSON, không phải khi cần nhỏ nhất.

Nguồn

Phần sau ta gặp CBOR (RFC 8949) — một định dạng nhị phân tự mô tả rất giống MessagePack nhưng là chuẩn IETF, được dùng trong COSE, WebAuthn, và IoT; đo xem nó khác MessagePack thế nào về kích thước và vì sao được chuẩn hóa lại quan trọng.