Bài 2: Kiểu dữ liệu nâng cao, Enums và Nested Messages trong Protobuf
Ở Bài 1, chúng ta đã làm quen với các kiểu dữ liệu cơ bản (Scalar types) và quy tắc đánh số trường cốt lõi của Protobuf. Tuy nhiên, các bài toán thực tế trong hệ thống Backend đòi hỏi cấu trúc dữ liệu phức tạp hơn thế rất nhiều: danh sách sản phẩm, trạng thái đơn hàng (đã thanh toán, đang giao, đã hủy), hay các đối tượng lồng nhau (Nested objects).
Trong bài học này, chúng ta sẽ khám phá các cấu trúc nâng cao đó trong chuẩn proto3.
1. Mảng và Danh sách với từ khóa repeated
Trong JSON, bạn biểu diễn một mảng bằng dấu ngoặc vuông []. Trong Protobuf, để biểu diễn một danh sách các phần tử cùng kiểu dữ liệu, bạn sử dụng từ khóa repeated.
Khi biên dịch sang Golang, một trường repeated sẽ tự động được chuyển đổi thành Slice tương ứng.
Protocol Buffers
syntax = "proto3";
package order;
option go_package = "github.com/your-username/your-repo/pb;pb";
message Order {
int64 order_id = 1;
string customer_name = 2;
// repeated khai báo đây là một danh sách (Slice trong Go: []string)
repeated string item_codes = 3;
// repeated cũng có thể chứa các message lồng nhau
repeated OrderItem items = 4;
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
double price = 3;
}
- Bí mật hiệu năng: Dưới lớp vỏ nhị phân, các phần tử của
repeatedkhông được bọc trong một mảng JSON cồng kềnh, mà được đóng gói liên tiếp bằng cơ chế Packed Encoding, giúp tối ưu hóa dung lượng mạng xuống mức thấp nhất.
2. Kiểu liệt kê với enum
Khi một trường chỉ có thể nhận một tập hợp các giá trị cố định (ví dụ: trạng thái đơn hàng, phương thức thanh toán), chúng ta sử dụng enum.
Các quy tắc vàng khi viết enum trong proto3:
-
Giá trị đầu tiên bắt buộc phải bằng 0: Theo tiêu chuẩn của proto3, giá trị số
0luôn đóng vai trò là giá trị mặc định (Zero value). Nếu bạn không truyền giá trị đó khi khởi tạo, Protobuf sẽ tự động lấy số0làm mặc định. -
Tên định danh rõ ràng: Nên viết hoa toàn bộ tên các hằng số enum kèm theo tiền tố tên enum để tránh xung đột namespace.
Protocol Buffers
enum OrderStatus {
// Giá trị 0 BẮT BUỘC phải tồn tại và thường dùng làm trạng thái chưa xác định/lỗi
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_PAID = 2;
ORDER_STATUS_SHIPPING = 3;
ORDER_STATUS_COMPLETED = 4;
ORDER_STATUS_CANCELLED = 5;
}
message PaymentResponse {
int64 transaction_id = 1;
OrderStatus status = 2; // Sử dụng enum vừa khai báo
}
3. Cấu trúc lồng nhau với Nested Messages
Protobuf cho phép bạn định nghĩa các message bên trong một message khác (Nested Messages). Kỹ thuật này giúp bạn gom nhóm các dữ liệu có liên quan chặt chẽ với nhau, làm sạch mã nguồn và dễ dàng tái sử dụng.
Protocol Buffers
message UserProfile {
int64 user_id = 1;
string full_name = 2;
// Định nghĩa một Nested Message ngay bên trong
message Address {
string street = 1;
string city = 2;
string country = 3;
string postal_code = 4;
}
// Sử dụng Address làm kiểu dữ liệu cho trường shipping_address
Address shipping_address = 3;
// Hoặc tạo một danh sách địa chỉ phụ
repeated Address alternative_addresses = 4;
}
Khi biên dịch sang Golang, Nested Message sẽ tự động sinh ra các struct độc lập nhưng được đặt tên theo quy tắc gộp tiền tố (ví dụ: UserProfile_Address), giúp phân chia rõ ràng phạm vi sử dụng.
4. Sử dụng Well-Known Types (Kiểu dữ liệu chuẩn có sẵn)
Trong quá trình thiết kế hệ thống, bạn thường xuyên phải đối mặt với các bài toán kinh điển như: Làm sao để truyền thời gian (Timestamp)? Làm sao phân biệt được một trường số nguyên bằng 0 thực sự hay do người dùng không truyền lên (Nullable)?
Google đã chuẩn bị sẵn một bộ thư viện các kiểu dữ liệu chuẩn gọi là Well-Known Types, nằm trong package google/protobuf/....
Ví dụ xử lý thời gian với google.protobuf.Timestamp:
Protocol Buffers
syntax = "proto3";
package event;
option go_package = "github.com/your-username/your-repo/pb;pb";
// Import thư viện chuẩn của Google để dùng Timestamp
import "google/protobuf/timestamp.proto";
message UserEvent {
int64 event_id = 1;
string event_type = 2;
// Thay vì dùng int64 cho Unix timestamp, hãy dùng chuẩn này
google.protobuf.Timestamp created_at = 3;
}
Khi biên dịch sang Go, google.protobuf.Timestamp sẽ tự động map trực tiếp thành struct *timestamppb.Timestamp, tương thích tuyệt đối với package time chuẩn của Go.
Tổng kết
Với repeated (mảng), enum (trạng thái liệt kê), Nested Messages (cấu trúc lồng nhau) và Well-Known Types (kiểu dữ liệu chuẩn), bạn đã có đầy đủ "vũ khí" để thiết kế bất kỳ mô hình dữ liệu phức tạp nào trong hệ thống Microservices.
Bây giờ, cấu trúc dữ liệu đã sẵn sàng. Đã đến lúc chúng ta cài đặt các công cụ biên dịch để biến những file .proto này thành mã nguồn Golang thực thụ.
All rights reserved