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.

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:

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 64là 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
d3cho thấy thư viện mã hóaid=90000thà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ậtUseCompactInts(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:
MsgpackMarshal763 ns so vớiJSONMarshal478 ns — chậm hơn.encoding/jsoncủ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:
MsgpackUnmarshal1.126 ns so vớiJSONUnmarshal2.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ề
- 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. - 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. - Đ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
- MessagePack — Đặc tả định dạng: https://github.com/msgpack/msgpack/blob/master/spec.md
- Go — vmihailenco/msgpack: https://pkg.go.dev/github.com/vmihailenco/msgpack/v5
- Trang chủ MessagePack: https://msgpack.org/
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.