หมายเลขที่คุณเอากลับมาใช้ซ้ำ และไม่มีอะไรแดงสักที่
ทีมหนึ่งลบ int32 quantity = 2; ออกจาก schema เพราะไม่มีใครใช้ช่องนั้นแล้ว การลบผ่าน review
ผ่าน build และผ่าน test ทุกตัว
ผ่านไปหลายเดือน อีกคนเปิด order.proto ขึ้นมาเพื่อเพิ่มช่องใหม่ เขาเห็นหมายเลข 1 ถูกใช้อยู่
และไม่เห็นหมายเลข 2 ที่ไหนเลย เขาจึงเลือก 2
บทนี้วัดว่า byte ชุดเก่าที่ยังวิ่งอยู่ในระบบ เกิดอะไรขึ้นกับมัน คำตอบมีสองทรง ทั้งสองทรง parse ผ่าน และทรงที่เงียบกว่าคือทรงที่แย่กว่า
orderctl คือเครื่องมือประมวลผลออเดอร์ที่สามคอร์สภาษาในเสานี้สร้างซ้ำทีละภาษา repo ตัวอย่าง
กำลังจัดทำ ระหว่างนี้ทุกตัวเลขในคอร์สนี้รันเองได้ สั่ง
scripts/wire-contract/up.sh เพื่อดึงเครื่องมือที่ตรึง version ไว้ แล้วตามด้วย
python3 scripts/wire-contract/probe.py
byte ชุดเก่าที่ไม่มีใครไปตามลบ
หัวข้อที่มีชื่อว่า “byte ชุดเก่าที่ไม่มีใครไปตามลบ”ออเดอร์ใบเดิมที่ Go เขียนไว้ยังเป็น byte แปดตัวชุดเดิม และการลบ field ไม่ได้แตะมันเลย
schema ตั้งต้นของคอร์สนี้มีสองช่อง และ Go เขียนออเดอร์หนึ่งใบออกมาเป็น 8 byte
message Order { string customer = 1; int32 quantity = 2;}0a04776972611007byte สองตัวท้ายคือ 1007 ตัว 10 เป็นtagtagค่าที่อยู่หัวของแต่ละกลุ่มบนสาย ซึ่งรวมหมายเลขกับ wire type ไว้ด้วยกัน · สูตรของมันคือ (หมายเลข << 3) | wire type · ตัวมันเองเป็น varint หมายเลข 1 ถึง 15 จึงยาว byte เดียว ส่วนหมายเลข 16 ยาวสอง byte · ยิ่งหมายเลขใหญ่ tag ยิ่งยาวของหมายเลข fieldfield numberหมายเลขประจำ field ที่ประกาศไว้ใน schema · เป็นสิ่งเดียวที่ทั้งสองฝั่งใช้จับคู่ค่าเข้ากับช่อง เปลี่ยนชื่อจึงฟรี แต่เปลี่ยนหมายเลขคือเปลี่ยนสัญญา 2 ชนิด
varintvarintการเข้ารหัสจำนวนเต็มที่ใช้ byte เท่าที่ค่านั้นต้องการ · เลข 7 กิน byte เดียว ส่วน int32 ที่เป็น −1 กินสิบ byte และ 07 เป็นค่า 7 นี่คือจำนวนจานของออเดอร์ใบนั้น
การลบ quantity ออกจาก schema เปลี่ยนแค่ของที่จะเขียนออกไปในอนาคต ส่วน byte ที่เขียนไปแล้ว
ยังนอนอยู่ใน queue ใน log และในแถวของฐานข้อมูลตามเดิม
ทรงที่หนึ่ง · หมายเลขเดิมกับ wire type ที่ต่างกัน
หัวข้อที่มีชื่อว่า “ทรงที่หนึ่ง · หมายเลขเดิมกับ wire type ที่ต่างกัน”ผู้อ่านทรงนี้ parse ผ่าน ช่องใหม่ไม่มีค่า และค่าเดิมไปนอนอยู่ใน unknown field หนึ่งช่อง
คนที่หยิบหมายเลข 2 ไปใช้ เลือกให้มันเป็นข้อความ ชื่อ size_name
message Order { string customer = 1; string size_name = 2;}ให้ผู้อ่านทรงนี้อ่าน byte แปดตัวข้างบน ผลที่วัดได้จาก Go และจาก Java ตรงกันทุกช่อง
| สิ่งที่วัด | ค่าที่ได้ |
|---|---|
| parse | ok |
customer | wira |
size_name | ไม่มีค่า |
| จำนวน unknown fieldunknown fieldfield ที่ผู้อ่านไม่รู้จัก เพราะ schema ของตัวเองไม่มีหมายเลขนั้น · ผู้อ่านเก็บ byte ดิบไว้เฉยๆ แล้วส่งคืนตอน serialize กลับ | 1 |
| byte ที่ค้างอยู่ใน unknown | 1007 |
| serialize กลับ | 0a04776972611007 |
ไม่มี exception ไม่มีรหัสออกที่ไม่ใช่ศูนย์ และไม่มีข้อความจากการ parse กลับมาสักบรรทัด ผู้อ่านได้
customer ครบ ส่วนช่อง size_name เป็นข้อความว่างเหมือนออเดอร์ที่ไม่ได้กรอกขนาดมา
ที่เกิดขึ้นกับตัว 1007 คือมันถูกเก็บไว้ทั้งชุดโดยไม่ถูกตีความ แล้วถูกส่งคืนตอน serialize กลับ
byte ที่ออกไปจึงเท่ากับ byte ที่รับเข้ามาทุกตัวอักษร
ผู้อ่านจับคู่ทั้งหมายเลขและชนิดบนสายwire typeสามบิตท้ายของ tag ที่บอกว่าค่าถัดไปมีรูปร่างแบบไหน · wire type 0 คือ varint และ wire type 2 คือ length-delimited ของที่มาถึงบอกว่าหมายเลข 2 เป็น varint แต่ schema ของผู้อ่านประกาศหมายเลข 2 ไว้เป็นแบบบอกความยาวนำหน้า ตัวอย่างนี้วัดได้ว่าทั้งสอง runtime พาค่านั้นไปไว้ที่ unknown field
ทรงที่สอง · หมายเลขเดิมกับ wire type ที่เท่ากัน
หัวข้อที่มีชื่อว่า “ทรงที่สอง · หมายเลขเดิมกับ wire type ที่เท่ากัน”ผู้อ่านทรงนี้ parse ผ่าน อ่าน
seatsได้ 7 และไม่มี unknown field สักช่อง
เปลี่ยนแค่ชนิดของช่องใหม่ จากข้อความเป็นจำนวนเต็ม หมายเลขยังเป็น 2 เท่าเดิม
message Order { string customer = 1; int32 seats = 2;}ให้ผู้อ่านทรงนี้อ่าน byte ชุดเดิม ผลที่วัดได้จาก Go และจาก Java ตรงกันทุกช่องอีกครั้ง
| สิ่งที่วัด | ค่าที่ได้ |
|---|---|
| parse | ok |
customer | wira |
seats | 7 |
| จำนวน unknown field | 0 |
| byte ที่ค้างอยู่ใน unknown | (ไม่มี) |
| serialize กลับ | 0a04776972611007 |
จำนวนจานเจ็ดจานเดินเข้าไปนั่งในช่องที่นั่ง แล้วกลายเป็นเจ็ดที่นั่ง ค่านั้นถูกทุกอย่าง ยกเว้นความหมาย
flowchart TD W["Go เขียนด้วยทรงตั้งต้น · 0a04776972611007"] W --> A["ผู้อ่านที่ถือ string size_name = 2"] W --> B["ผู้อ่านที่ถือ int32 seats = 2"] A --> A1["customer = wira · size_name ไม่มีค่า"] A --> A2["unknown 1 ช่อง · 1007"] B --> B1["customer = wira · seats = 7"] B --> B2["unknown 0 ช่อง"]
คำบรรยายภาพ: byte ชุดเดียวกันเดินเข้าหาผู้อ่านสองทรง ทรงซ้ายเก็บค่าเดิมไว้ที่ unknown field ส่วนทรงขวารับค่าเดิมเข้าช่องใหม่ตรงๆ โดยไม่เหลืออะไรไว้ที่ unknown เลย
แบบไหนแย่กว่ากัน
หัวข้อที่มีชื่อว่า “แบบไหนแย่กว่ากัน”ทรงแรกยังเหลือร่องรอยหนึ่งช่องบนสาย ส่วนทรงที่สองไม่เหลืออะไรให้ใครสงสัย
วางสองทรงเทียบกันทีละช่อง ช่องที่ต่างกันมีช่องเดียว
| สิ่งที่วัด | string size_name = 2 | int32 seats = 2 |
|---|---|---|
| parse | ok | ok |
| จำนวน unknown field | 1 | 0 |
| byte ที่ค้างอยู่ใน unknown | 1007 | (ไม่มี) |
| ช่องใหม่ได้ค่าอะไร | ไม่มีค่า | 7 |
| serialize กลับ | 0a04776972611007 | 0a04776972611007 |
ทรงแรกยังมีที่ให้ยืน ใครที่เปิดดูจำนวน unknown field ของ message ที่รับเข้ามา จะเห็นเลข 1
แล้วเห็น byte 1007 ที่ตัวเองอ่านไม่ออก นั่นคือหลักฐานว่ามีของที่ไม่ได้ตกลงกันไว้เดินมาด้วย
ทรงที่สองไม่มีหลักฐานอะไรเลย unknown field เป็น 0 ทุกช่องมีค่าครบ และไม่มีอะไรแยกออเดอร์ใบนี้ ออกจากออเดอร์ที่ฝั่งใหม่เขียนเองอย่างถูกต้อง
สังเกตแถวสุดท้ายของตารางให้ดี ทั้งสองทรง serialize กลับได้ 0a04776972611007 เท่าเดิม
การเทียบ byte ขาเข้ากับ byte ขาออกจึงแยกสองทรงนี้ไม่ออก และแยกทรงที่ผิดออกจากทรงที่ถูกไม่ได้ด้วย
นี่คือความหมายของความเข้ากันได้บนสายwire compatibilityการแก้ schema ที่ทั้งสองฝั่งยังอ่าน byte ของกันได้ตามที่ตั้งใจ · parse ผ่านไม่ใช่หลักฐานของข้อนี้ เพราะค่าที่ไปนั่งผิดช่องก็ parse ผ่านเหมือนกัน ที่คอร์สนี้ใช้ parse ผ่านไม่ใช่ หลักฐานของมัน เพราะค่าที่ไปนั่งผิดช่องก็ parse ผ่านเหมือนกัน
ค่า 7 ในช่อง seats เป็นตัวเลขที่ถูกต้องตามชนิด ตามช่วง และตาม schema ระบบปลายทางที่รับ
ตัวเลขนี้ไปคำนวณต่อ ไม่มีข้อมูลอะไรเหลือให้ย้อนกลับไปรู้ว่าเดิมมันคือจำนวนจาน
reserved คือทางเดียวที่ย้ายคำถามนี้ไปให้เครื่องมือ
หัวข้อที่มีชื่อว่า “reserved คือทางเดียวที่ย้ายคำถามนี้ไปให้เครื่องมือ”ประกาศ
reservedไว้ตั้งแต่ตอนลบ แล้ว protoc คืนรหัสออก 1 ให้คนที่หยิบหมายเลขเดิมกลับมาใช้
ทุกอย่างข้างบนเกิดขึ้นเพราะคนที่เปิด order.proto ในวันนั้น ไม่มีทางรู้ว่าหมายเลข 2 เคยมีเจ้าของ
สิ่งที่เขาเห็นคือช่องว่าง reservedreservedคำประกาศที่กันหมายเลขหรือชื่อไว้ ไม่ให้ใครเอากลับมาใช้อีก · protoc คืนรหัสออก 1 และบอกหมายเลขว่างถัดไปให้ด้วย คือคำประกาศที่เปลี่ยนช่องว่างนั้นให้เป็นป้ายห้าม
message Order { string customer = 1; reserved 2; reserved "quantity"; int32 seats = 2;}protoc ไม่ยอม compile ทรงนี้ มันคืนรหัสออก 1 แล้วพิมพ์ออกมาว่า
order.proto:7:12: Field "seats" uses reserved number 2. order.proto:7:12: Suggested field numbers for order.reserved.Order: 3reserved 2; กันหมายเลขไว้ ส่วน reserved "quantity"; กันชื่อไว้ด้วย เพื่อไม่ให้ใครเอาชื่อเดิม
ไปแปะบนหมายเลขอื่นแล้วทำให้คนอ่านสองฝั่งเข้าใจไม่ตรงกัน
นี่คือกับดักข้อเดียวในคอร์สนี้ที่มีเครื่องมือเตือน และมันเตือนตั้งแต่ตอน compile ก่อนของจะขึ้นระบบ คำตอบของ protoc ทั้งสองแบบอยู่เต็มๆ ที่ บทที่ 8
ราคาของหมายเลขที่ใช้ไปแล้ว
หัวข้อที่มีชื่อว่า “ราคาของหมายเลขที่ใช้ไปแล้ว”ชื่อ field เปลี่ยนได้ฟรีเพราะไม่เคยอยู่บนสาย ส่วนหมายเลขอยู่บนสายจริง จึงเปลี่ยนความหมายไม่ได้
บทที่ 1 บอกว่าชื่อ field เปลี่ยนได้ฟรี เพราะมันไม่เคยอยู่บนสาย บทนี้คือด้านที่ต้องจ่ายของกฎ เดียวกัน หมายเลขอยู่บนสายจริง มันจึงเปลี่ยนความหมายไม่ได้เลยสักครั้ง
ราคานี้ต่างจากราคาของ unknown field ใน บทที่ 3 ตรงที่บทนั้นเป็นด้านที่ได้เปล่า ผู้อ่านรุ่นเก่าถือของที่ไม่รู้จักไว้แล้วส่งคืนครบ ส่วนบทนี้ทรงที่สองไม่เหลือ unknown field ไว้ สักช่อง ร่องรอยที่บทนั้นได้มาฟรี จึงไม่มีให้ดูเลย
สังเกตว่าตลอดทั้งบท หมายเลขไม่เคยขยับไปไหนเลย มันเป็น 2 ตั้งแต่ต้นจนจบ แล้วสัญญาก็ยังพังได้ คำถามถัดไปจึงเป็นด้านกลับ คือหมายเลขอยู่ที่เดิมแต่ชนิดเปลี่ยน แล้วเกิดอะไรขึ้น คำตอบอยู่ที่ บทที่ 5
-
Protocol Buffers — Language Guide (proto 3) (เข้าถึง 2026-09-04) — ที่มาของ
reservedทั้งแบบหมายเลขและแบบชื่อ และของกฎว่าหมายเลขที่ลบแล้ว ห้ามเอากลับมาใช้ -
Protocol Buffers — Encoding (เข้าถึง 2026-09-04) — ที่มาของสูตร tag และของเรื่อง unknown field
-
ตัวเลขทุกตัวในบทนี้มาจาก
scripts/wire-contract/probe.pyซึ่งให้ Go เขียน byte แล้วให้ทั้ง Go และ Java อ่านกลับ ต้องสั่งscripts/wire-contract/up.shก่อนหนึ่งครั้ง ซึ่งดึงเครื่องมือสามตัวที่ตรึง version ไว้- protoc 36.1
- protoc-gen-go v1.36.12
- protobuf-java 4.36.1
เช็กความเข้าใจ — บทที่ 4
ข้อ 1 / 3ผู้อ่านที่ถือ `string size_name = 2;` อ่าน byte `0a04776972611007` ซึ่งเขียนตอนหมายเลข 2 ยังเป็น `int32 quantity` ผลที่วัดได้คืออะไร