สัญญาที่ตรวจได้ก่อนของจะขึ้น — กับดักเดียวในคอร์สนี้ที่เครื่องมือปฏิเสธให้
เจ็ดบทที่ผ่านมามีอย่างหนึ่งเหมือนกันหมด คือไม่มีบทไหนที่อะไรแดง ทุกทรงที่แก้แล้ว compile ผ่าน ทุก byte ที่ส่งข้ามฝั่ง parse ผ่าน และทุกฝั่งได้คำตอบกลับมาโดยไม่ต้องจับ error
บทนี้เปลี่ยนเรื่อง มันเป็นบทเดียวของคอร์สที่เครื่องมือตอบว่าไม่ ก่อนที่ของจะเดินทางไปไหน และก่อนที่จะมี code ให้ใครเรียกด้วยซ้ำ
orderctl คือเครื่องมือประมวลผลออเดอร์ที่สามคอร์สภาษาในเสานี้สร้างซ้ำทีละภาษา repo ตัวอย่าง
กำลังจัดทำ ระหว่างนี้ทุกตัวเลขในคอร์สนี้รันเองได้ สั่ง
scripts/wire-contract/up.sh เพื่อดึงเครื่องมือที่ตรึง version ไว้ แล้วตามด้วย
python3 scripts/wire-contract/probe.py
หมายเลขที่จองไว้ แล้วเผลอเอากลับมาใช้
หัวข้อที่มีชื่อว่า “หมายเลขที่จองไว้ แล้วเผลอเอากลับมาใช้”protoc ออกด้วยรหัส 1 และบอกมาด้วยว่าหมายเลขว่างถัดไปคือ 3
บทที่ 4 จบด้วยคำสั่งเดียวที่ย้ายเรื่อง หมายเลขซ้ำไปให้เครื่องมือตอบ คือ การจองหมายเลขไว้ไม่ให้ใครใช้ซ้ำreservedคำประกาศที่กันหมายเลขหรือชื่อไว้ ไม่ให้ใครเอากลับมาใช้อีก · protoc คืนรหัสออก 1 และบอกหมายเลขว่างถัดไปให้ด้วย บทนี้วัดว่ามันตอบว่าอะไรจริงๆ
ทรงข้างล่างจองหมายเลข 2 กับชื่อ quantity ไว้ตามกฎ แล้วประกาศ field ใหม่ทับหมายเลขที่จองไปแล้ว
syntax = "proto3";
package order.reserved;
message Order { string customer = 1; reserved 2; reserved "quantity"; int32 seats = 2;}สั่ง protoc -I . -o /dev/null order.proto กับทรงนี้ ผลคือ รหัสออก 1 และไม่มี code
ออกมาให้ compile ต่อ ข้อความที่ protoc พิมพ์ออกมาคือ
order.proto:7:12: Field "seats" uses reserved number 2. order.proto:7:12: Suggested field numbers for order.reserved.Order: 3ข้อความนี้ยาวสองประโยค และ probe ยุบช่องว่างทั้งหมดให้เหลือบรรทัดเดียวตอนบันทึก สิ่งที่อยู่ในนั้นจึงมีสองเรื่อง ไม่ใช่เรื่องเดียว
- ประโยคแรกคือการปฏิเสธ ระบุชื่อ field ที่ผิดและหมายเลขที่ชนไว้ตรงๆ
- ประโยคที่สองคือข้อเสนอ protoc ไล่ดูหมายเลขที่ยังว่างใน
order.reserved.Orderแล้วบอกว่า 3
ตำแหน่งที่ protoc ชี้ในตัวอย่างนี้คือบรรทัดที่ 7 column 12 ซึ่งตรงกับเลข 2 ในบรรทัด
reserved 2; ไม่ใช่บรรทัดของ seats มันชี้ไปที่ข้อจองที่ถูกละเมิด ไม่ใช่ที่ผู้ละเมิด
ชื่อที่จองไว้ แล้วเผลอเอากลับมาใช้
หัวข้อที่มีชื่อว่า “ชื่อที่จองไว้ แล้วเผลอเอากลับมาใช้”การจองชื่อกับการจองหมายเลขเป็นคนละข้อ ผ่านข้อหนึ่งแล้วยังติดอีกข้อได้
ทรงที่สองทำตามข้อเสนอของ protoc ทุกอย่าง คือเลิกใช้หมายเลข 2 แล้วไปใช้หมายเลข 3 ที่ protoc บอกเองว่าว่าง แต่มันเอาชื่อเดิมกลับมาใช้
syntax = "proto3";
package order.reservedname;
message Order { string customer = 1; reserved 2; reserved "quantity"; int32 quantity = 3;}ผลคือ รหัสออก 1 อีกครั้ง ข้อความสั้นกว่าเดิมและไม่มีข้อเสนอต่อท้าย
order.proto:9:9: Field name "quantity" is reserved.คราวนี้ตำแหน่งชี้ที่บรรทัดที่ 9 column 9 ซึ่งตรงกับตัวอักษรแรกของชื่อ quantity เอง
เพราะข้อจองที่ถูกละเมิดคือชื่อ และชื่อมีที่อยู่เดียวคือตรงนั้น
การจองชื่อไม่ได้ป้องกันเรื่องบนสาย เพราะชื่อไม่เคยเดินทางไปกับ byte ตามที่ บทที่ 1 วัดไว้ มันป้องกันเรื่องของคน คือกันไม่ให้ชื่อเดิมกลับมาหมายถึงของคนละอย่างในรุ่นถัดไป
ทำไมกับดักนี้ไม่เหมือนกับดักอีกหกอัน
หัวข้อที่มีชื่อว่า “ทำไมกับดักนี้ไม่เหมือนกับดักอีกหกอัน”กับดักอีกหกอันผ่าน protoc และ parse ผ่านทั้งสองฝั่ง ส่วนกับดักนี้ถูกปฏิเสธก่อนมี code
การเปลี่ยน schema ตามเวลาschema evolutionการแก้ schema ทีละครั้งโดยที่ของซึ่ง deploy ไปแล้วยังทำงานต่อได้ · คอร์สนี้วัดว่าการแก้แต่ละแบบทำอะไรกับ byte จริงบ้างในคอร์สนี้แบ่งได้เป็นสองกอง กองที่เครื่องมือเห็น กับกองที่เครื่องมือไม่มีทางเห็น และกองแรกมีสมาชิกอยู่รายการเดียว
flowchart TD
A["แก้ schema แล้วสั่ง protoc"] --> B{"หมายเลขหรือชื่อนั้น ประกาศ reserved ไว้หรือยัง"}
B -->|"ประกาศไว้แล้ว"| C["protoc ออกด้วยรหัส 1 และยังไม่มี code ให้ compile"]
B -->|"ยังไม่ได้ประกาศ"| D["protoc ออกด้วยรหัส 0 และได้ code ตามปกติ"]
D --> E["ทั้งสองฝั่ง parse ผ่าน"]
E --> F["ค่าไปอยู่ผิดช่อง หรือหายไป โดยไม่มีอะไรแดง"]
คำบรรยายภาพ: ทางเดินสองเส้นออกจากการแก้ schema ครั้งเดียวกัน เส้นซ้ายจบที่เครื่องมือปฏิเสธ เส้นขวาจบที่ค่าผิดที่ไม่มีใครเตือน และเส้นขวาคือเส้นที่กับดักอีกหกอันของคอร์สเดินอยู่ทั้งหมด
เหตุผลที่เครื่องมือไม่เห็นกองที่สอง คือมันไม่มีของสองรุ่นให้เทียบ protoc อ่าน schema ที่วางอยู่ ตรงหน้ามันรุ่นเดียว แล้วสร้าง โครงสร้างที่บรรยาย schemadescriptorของที่ถือชื่อ หมายเลข และชนิดของทุก field ไว้ครบ · มันอยู่ข้าง code ที่ generate ออกมา และไม่ได้เดินทางไปกับ message ของรุ่นนั้น
ความเข้ากันได้บนสายwire compatibilityการแก้ schema ที่ทั้งสองฝั่งยังอ่าน byte ของกันได้ตามที่ตั้งใจ · parse ผ่านไม่ใช่หลักฐานของข้อนี้ เพราะค่าที่ไปนั่งผิดช่องก็ parse ผ่านเหมือนกันเป็นคำถามระหว่างสองรุ่น ไม่ใช่คำถามของรุ่นใดรุ่นหนึ่ง
reserved ทำงานได้เพราะมันย้ายความรู้ของรุ่นเก่าเข้ามาอยู่ในตัว schema รุ่นใหม่
และ descriptor ตัวนั้นไม่ได้เดินทางไปกับ message ผู้อ่านปลายทางถือ descriptor ของตัวเอง ที่สร้างจาก schema ของตัวเอง สองฝั่งจึงไม่มีโอกาสเทียบกันเองตอนรัน
ตารางรวมกับดักทั้งเจ็ดของคอร์ส
หัวข้อที่มีชื่อว่า “ตารางรวมกับดักทั้งเจ็ดของคอร์ส”เจ็ดแถว หกแถว protoc จับไม่ได้ เหลือแถวเดียวที่ protoc ปฏิเสธ
ทุกช่องในตารางนี้มาจาก probe ตัวเดียวกัน หกแถวแรกมี parse เป็น ok ทุกแถว
ส่วนแถวสุดท้ายไม่มีช่อง parse เพราะ protoc ไม่ยอมสร้าง code ให้ตั้งแต่ต้น
| บท | สิ่งที่แก้ใน schema | สิ่งที่วัดได้ | protoc จับได้อะไรก่อนของขึ้น |
|---|---|---|---|
| 2 | ปล่อย quantity ไว้ที่ค่าเริ่มต้น | ทั้งใบเหลือ 6 byte คือ 0a0477697261 และผู้อ่านแยกไม่ออกว่าไม่ได้ตั้งค่า หรือตั้งเป็นศูนย์ | ไม่มี ต้องประกาศ optional เอง |
| 4 | เอาหมายเลข 2 ไปใช้กับ string size_name | parse ผ่าน unknown 1 หมายเลข คือ 1007 และช่องใหม่ไม่มีค่า | ไม่มี เว้นแต่ประกาศ reserved 2; ไว้ก่อน |
| 4 | เอาหมายเลข 2 ไปใช้กับ int32 seats | parse ผ่าน unknown 0 และ seats ได้ค่า 7 ของช่องเดิม | ไม่มี เว้นแต่ประกาศ reserved 2; ไว้ก่อน |
| 5 | เปลี่ยน int32 เป็น uint32 แล้วอ่าน byte ของ −1 | parse ผ่าน Go ได้ 4294967295 ส่วน Java ได้ -1 และ serialize กลับได้ 0a047769726110ffffffff0f ซึ่งไม่เท่าเดิม | ไม่มี |
| 5 | เปลี่ยน int32 เป็น sint32 | ผู้อ่าน sint32 อ่าน byte ของ int32 ได้ -4 และผู้อ่าน int32 อ่าน byte ของ sint32 ได้ 14 | ไม่มี |
| 6 | เปลี่ยน customer เป็น repeated แล้วเขียนสองชื่อ | 17 byte ผู้อ่านตัวเดียว parse ผ่าน ได้ somchai unknown 0 และ serialize กลับเหลือ 11 byte | ไม่มี |
| 8 | เอาหมายเลขหรือชื่อที่ reserved ไว้กลับมาใช้ | protoc ออกด้วย รหัส 1 ทั้งสองแบบ และแบบหมายเลขเสนอหมายเลข 3 ให้ด้วย | จับได้ ตั้งแต่ก่อนมี code ให้ compile |
ช่องขวาสุดคือช่องที่สำคัญที่สุดของตารางนี้ หกแถวแรกเขียนว่าไม่มี เพราะ protoc เห็น schema รุ่นเดียว ข้อมูลที่จะตัดสินได้จึงไม่อยู่ในสิ่งที่มันมองเห็น เครื่องมือที่เทียบสองรุ่นเป็นคนละเรื่อง และอยู่ในรายการที่คอร์สนี้ไม่ได้ตอบ
สามแถวที่ปลอดภัยจริง
หัวข้อที่มีชื่อว่า “สามแถวที่ปลอดภัยจริง”คอร์สนี้ไม่ได้มีแต่กับดัก สามการแก้ข้างล่างวัดแล้วว่าไม่ทำให้อีกฝั่งได้ค่าผิด
| บท | สิ่งที่แก้ใน schema | สิ่งที่วัดได้ |
|---|---|---|
| 1 | เปลี่ยนชื่อ customer เป็น customer_name | hex เท่าเดิมทุกตัว คือ 0a04776972611007 |
| 3 | เพิ่ม string branch = 3; ต่อท้าย | 18 byte ผู้อ่านรุ่นเก่าถือ unknown 1 หมายเลข แล้ว serialize กลับได้ byte เท่าเดิม |
| 5 | ขยาย int32 เป็น int64 ซึ่งเป็นการขยายทางเดียว | byte เท่าเดิม คือ 0a04776972611007 และอ่านได้ค่า 7 เท่าเดิม |
สามแถวนี้ไม่เปลี่ยนความหมายของช่องที่ผู้อ่านอีกฝั่งถืออยู่ ส่วนเจ็ดแถวข้างบนเปลี่ยน
และมันเปลี่ยนคนละที่กัน บ้างที่หมายเลข บ้างที่ชนิด บ้างที่จำนวนค่าในช่องเดียว และบ้างที่ผู้อ่านตอบไม่ได้ว่าค่าถูกตั้งไว้หรือไม่ ทั้งที่ไม่มีใครแตะ schema เลยสักบรรทัด
สิ่งที่คอร์สนี้ไม่ได้ตอบ
หัวข้อที่มีชื่อว่า “สิ่งที่คอร์สนี้ไม่ได้ตอบ”สี่หัวข้อที่จงใจไม่แตะ สามข้อแรกด้วยเหตุผลเดียวกัน และข้อสุดท้ายด้วยเหตุผลคนละข้อ
- gRPC ในระดับ service คือชื่อ method รหัส
UNIMPLEMENTEDและ streaming - การ map เป็น JSON ซึ่งมีกฎของตัวเองคนละชุดกับสิ่งที่คอร์สนี้วัด
- proto2 และ editions ซึ่งเปลี่ยนกฎของ field presence ไปจากที่บทที่ 2 วัดไว้
buf breakingและ schema registry ซึ่งเป็นเครื่องมือ ไม่ใช่กฎของสาย
สามข้อแรกวัดได้ด้วย probe ตัวเดิม แต่ทุกข้อเพิ่มแกนที่สองเข้าไปในตารางทุกใบของคอร์ส ตารางที่เคยมีแถวเป็นทรงของ schema จะกลายเป็นแถวคูณด้วยรูปแบบที่สอง
ข้อสุดท้ายเป็นเครื่องมือที่เปลี่ยน version บ่อยกว่าที่คอร์สจะตามทัน สิ่งที่คอร์สนี้ให้ไป คือรายการข้อที่ควรเอาไปถามเครื่องมือ ไม่ใช่วิธีตั้งค่าเครื่องมือตัวใดตัวหนึ่ง
เดินต่อจากที่นี่
หัวข้อที่มีชื่อว่า “เดินต่อจากที่นี่”สามคอร์สในเสาเดียวกันที่สร้าง orderctl ตัวเดียวกัน ทีละภาษา
คอร์สนี้อ่าน byte ที่วิ่งระหว่างสองบริการ ส่วนสามคอร์สข้างล่างเขียนตัวบริการเอง
- Go จากศูนย์ — สร้าง
orderctlตัวแรกด้วย standard library ล้วน - Java จากศูนย์ — สร้าง
orderctlตัวเดิมใหม่ทั้งตัวด้วย Java 25 - Java 8 ถึง 25 — สำหรับคนที่เขียน Java เป็นแล้ว แต่ความรู้หยุดที่ 8 หรือ 11
ถ้าอยากกลับไปดูช่องที่ Go กับ Java ตอบไม่เท่ากันอีกครั้ง มันอยู่ที่ บทที่ 7 และช่องนั้นมีอยู่ช่องเดียวในทั้งคอร์ส
-
Protocol Buffers — Language Guide (proto 3) (เข้าถึง 2026-09-04) — ที่มาของกฎ
reservedทั้งแบบจองหมายเลขและแบบจองชื่อ -
Protocol Buffers — Proto Best Practices (เข้าถึง 2026-09-04) — ที่มาของคำแนะนำให้จองหมายเลขและชื่อทันทีที่ลบ field ทิ้ง
-
Protocol Buffers — Encoding (เข้าถึง 2026-09-04) — ที่มาของกฎว่าสิ่งที่เดินทางไปบนสายคือหมายเลขกับชนิด ไม่ใช่ชื่อ
-
ตัวเลขทุกตัวในบทนี้มาจาก
scripts/wire-contract/probe.pyซึ่งสั่ง protoc ตัวจริงกับทรงที่ผิด แล้วบันทึกรหัสออกกับข้อความที่ได้ ต้องสั่งscripts/wire-contract/up.shก่อนหนึ่งครั้ง ซึ่งดึงเครื่องมือสามตัวที่ตรึง version ไว้- protoc 36.1
- protoc-gen-go v1.36.12
- protobuf-java 4.36.1
เช็กความเข้าใจ — บทที่ 8
ข้อ 1 / 3schema ประกาศ `reserved 2;` ไว้ แล้วมีคนเพิ่ม `int32 seats = 2;` เข้าไป protoc ตอบว่าอะไร