ข้ามไปยังเนื้อหา

Messages & Fields

message คือกลุ่มของ field ที่มี type และมีชื่อ แต่ละ field มีสามส่วน: type, ชื่อ และ field number

syntax = "proto3";
package user.v1;
message User {
int64 id = 1;
string name = 2;
string email = 3;
bool verified = 4;
}

ชื่อ (name, email) มีไว้ให้คนอ่าน .proto และให้ generated code ใช้ ส่วน number (= 1, = 2) คือสิ่งที่เดินทางบน wire จริง ๆ การแยกสองอย่างนี้คือสิ่งสำคัญที่สุดที่ต้องเข้าใจเกี่ยวกับ protobuf

เวลา protobuf encode message แต่ละ field จะกลายเป็น tag — คือ field number รวมกับ wire type — ตามด้วย value ส่วนชื่อ field ไม่เคยออกไปจาก .proto เลย

flowchart LR
  field["name = "Ada"  (field #2)"] --> tag["tag: field 2, type length-delimited"]
  tag --> val["value: 3 bytes 'Ada'"]
  val --> bytes["บน wire: รวมไม่กี่ byte"]
field บน wire คือ numbered tag กับ value

สิ่งนี้ให้ผลตอบแทนใหญ่สองอย่าง:

  • กะทัดรัด string "verified" ไม่เคยเดินทาง มีแต่ number 4 ที่เดินทาง message เล็กอยู่เสมอไม่ว่าชื่อ field จะยาวแค่ไหน
  • rename ได้ฟรี เปลี่ยน name เป็น full_name ใน .proto แล้วทุก client ยังคุยกันได้ — เพราะ wire ถือแค่ field 2 เท่านั้น ชื่อคือ documentation ส่วน number คือ contract

คุณเป็นคนเลือก number เอง และการเลือกมีผลทั้งเรื่อง performance และความปลอดภัย

  • 1–15 encode เป็น byte เดียว (tag = number + wire type แพ็กรวมกัน) เก็บไว้ให้ field ที่มีอยู่ใน เกือบทุก message
  • 16–2047 ใช้สอง byte เหมาะกับ field ที่พบไม่บ่อย
  • 19000–19999 ถูก reserve โดยตัว protobuf เอง — ใช้ไม่ได้
  • ค่าสูงสุดคือ 536,870,911 (2^29 − 1) ซึ่งคุณไม่มีทางใช้ถึง

กฎหนึ่งข้อที่กัน bug ร้ายแรงที่สุดของ protobuf: เมื่อลบ field ห้ามให้ number ของตัวเองถูก reuse เด็ดขาด ถ้า client เก่ายังส่ง field 3 เป็น string แต่ server ใหม่เอา 3 ไปใช้เป็น int64 bytes จะถูกตีความผิด — เป็น failure ที่เงียบและทำให้ข้อมูลเสีย

protobuf ให้ reserved ไว้ล็อก number ที่ปลดระวางแล้ว (และชื่อเก่าของตัวเองด้วย) เพื่อให้ compiler ปฏิเสธการ reuse:

message User {
reserved 3; // field number เดิมของ 'email' — ห้าม reuse
reserved "email"; // และชื่อเดิม เพื่อดักความผิดพลาด
int64 id = 1;
string name = 2;
bool verified = 4;
}

ตอนนี้ใครก็ตามที่พยายามเพิ่ม field 3 ใหม่จะเจอ compile error แทนที่จะเป็น incident บน production เราจะกลับมาลงลึกเรื่องวินัยนี้ในบท Evolving Schemas — แต่ให้เริ่มนิสัยตั้งแต่ตอนนี้: ลบ field แล้ว reserve number ของตัวเอง

อะไรที่เดินทางบน wire จริง ๆ เพื่อระบุ field?
ทำไมควรให้ field ที่พบบ่อยที่สุดใช้ number 1–15?
จะเกิดอะไรถ้า reuse field number ที่ลบไปแล้วกับ type อื่น?
`reserved` keyword ทำอะไร?