ออเดอร์ใบหนึ่งเป็น byte แปดตัว และไม่มีชื่อ field อยู่ในนั้นเลย
บริการที่เขียนด้วย Go ส่งออเดอร์ไปให้บริการที่เขียนด้วย Java สองฝั่งนี้คนละทีม คนละ repo และไม่เคยขึ้น production พร้อมกันสักครั้ง
สิ่งเดียวที่ผูกสองฝั่งไว้ด้วยกันคือ byte ที่วิ่งอยู่ระหว่างมัน คอร์สนี้เริ่มจากการอ่าน byte เหล่านั้นให้ออกก่อน แล้วค่อยถามว่าแก้ schema แบบไหนที่ทำให้อีกฝั่งอ่านผิด
orderctl คือเครื่องมือประมวลผลออเดอร์ที่สามคอร์สภาษาในเสานี้สร้างซ้ำทีละภาษา repo ตัวอย่าง
กำลังจัดทำ ระหว่างนี้ทุกตัวเลขในคอร์สนี้รันเองได้ สั่ง
scripts/wire-contract/up.sh เพื่อดึงเครื่องมือที่ตรึง version ไว้ แล้วตามด้วย
python3 scripts/wire-contract/probe.py
schema ตั้งต้น กับ byte ที่ได้ออกมา
หัวข้อที่มีชื่อว่า “schema ตั้งต้น กับ byte ที่ได้ออกมา”ออเดอร์ที่มีชื่อลูกค้าและจำนวนจาน ขึ้นสายเป็น byte แปดตัว
Order ทรงตั้งต้นของคอร์สนี้มีสองช่อง ช่องหนึ่งเป็นข้อความ อีกช่องเป็นจำนวนเต็ม
syntax = "proto3";
package order.v1;
message Order { string customer = 1; int32 quantity = 2;}ฝั่ง Go ใส่ค่าลงไปสองค่า คือ customer เป็น wira และ quantity เป็น 7 แล้วสั่ง
proto.Marshal ผลที่ได้คือ 8 byte เขียนเป็น hex ได้แบบนี้
0a04776972611007แปด byte นี้คือทั้งหมดที่เดินทางไป ไม่มีวงเล็บ ไม่มีเครื่องหมายคั่น และไม่มีคำว่า
customer หรือ quantity อยู่ในนั้นเลยสักตัวอักษร
แกะทั้งแปดตัว
หัวข้อที่มีชื่อว่า “แกะทั้งแปดตัว”byte แปดตัวแบ่งเป็นสองกลุ่ม กลุ่มละหนึ่งค่า และแต่ละกลุ่มขึ้นต้นด้วย tag
รูปแบบบนสายwire formatรูปแบบของ byte ที่ protobuf ใช้จริงบนสาย · แต่ละ field คือ tag แล้วตามด้วยค่า และไม่มีชื่อ field เดินทางไปด้วยสักตัวของ protobuf วางของเรียงกันเป็นกลุ่ม กลุ่มหนึ่งคือหนึ่งค่าของช่องหนึ่งช่อง และไม่มีอะไรคั่นระหว่างกลุ่ม ตัวที่บอกว่ากลุ่มใหม่เริ่มแล้วคือ tag ที่อยู่หัวกลุ่ม
| byte | ค่า | ความหมาย |
|---|---|---|
| 1 | 0a | tagtagค่าที่อยู่หัวของแต่ละกลุ่มบนสาย ซึ่งรวมหมายเลขกับ wire type ไว้ด้วยกัน · สูตรของมันคือ (หมายเลข << 3) | wire type · ตัวมันเองเป็น varint หมายเลข 1 ถึง 15 จึงยาว byte เดียว ส่วนหมายเลข 16 ยาวสอง byte · ยิ่งหมายเลขใหญ่ tag ยิ่งยาว ของ field หมายเลข 1 ชนิด length-delimitedlength-delimitedwire type 2 · ส่งความยาวก่อนหนึ่ง byte แล้วตามด้วย byte ตามจำนวนนั้น · string, bytes และ message ที่ซ้อนกันใช้รูปนี้ |
| 2 | 04 | ความยาวของค่า คือ 4 byte |
| 3–6 | 77 69 72 61 | ตัวอักษร w i r a |
| 7 | 10 | tag ของ field หมายเลข 2 ชนิดตัวเลขแบบ varintvarintการเข้ารหัสจำนวนเต็มที่ใช้ byte เท่าที่ค่านั้นต้องการ · เลข 7 กิน byte เดียว ส่วน int32 ที่เป็น −1 กินสิบ byte |
| 8 | 07 | ค่าคือ 7 |
ช่องข้อความต้องบอกความยาวมาด้วย เพราะผู้อ่านไม่มีทางรู้เองว่าข้อความจบตรงไหน ส่วนช่องตัวเลขไม่ต้องบอก เพราะ varint บอกจุดจบไว้ในตัวมันเองแล้ว
tag เก็บสองอย่างไว้ในตัวเดียว
หัวข้อที่มีชื่อว่า “tag เก็บสองอย่างไว้ในตัวเดียว”tag คือหมายเลขของ fieldfield numberหมายเลขประจำ field ที่ประกาศไว้ใน schema · เป็นสิ่งเดียวที่ทั้งสองฝั่งใช้จับคู่ค่าเข้ากับช่อง เปลี่ยนชื่อจึงฟรี แต่เปลี่ยนหมายเลขคือเปลี่ยนสัญญากับชนิดบนสายwire typeสามบิตท้ายของ tag ที่บอกว่าค่าถัดไปมีรูปร่างแบบไหน · wire type 0 คือ varint และ wire type 2 คือ length-delimited อัดรวมกันด้วยสูตรเดียว
สูตรคือเลื่อนหมายเลขไปทางซ้ายสาม bit แล้วเอาชนิดมาต่อท้าย
tag = (หมายเลข << 3) | ชนิดลองกับ byte จริงทั้งสองตัวที่แกะได้ข้างบน
0x0aคือ 10 ในระบบสิบ ·10 >> 3ได้ 1 และ10 & 7ได้ 2 · จึงเป็น field 1 ชนิด 20x10คือ 16 ในระบบสิบ ·16 >> 3ได้ 2 และ16 & 7ได้ 0 · จึงเป็น field 2 ชนิด 0
ชนิดมีที่อยู่แค่สาม bit จึงมีได้ไม่เกินแปดค่า ที่นิยามไว้จริงมีหกค่า และในนั้นเลิกใช้ไปสองค่า สองค่าที่คอร์สนี้ใช้คือ 0 สำหรับ varint และ 2 สำหรับของที่บอกความยาวนำหน้า
tag เองก็เป็น varint เหมือนกัน
หัวข้อที่มีชื่อว่า “tag เองก็เป็น varint เหมือนกัน”ย้ายหมายเลขจาก 15 ไป 16 ทำให้ทุก message ยาวขึ้นหนึ่ง byte ตลอดไป
ตัวอย่างข้างบนใช้หมายเลข 1 กับ 2 ซึ่ง tag ลงได้ใน byte เดียวทั้งคู่ แต่นั่นเป็นเรื่องของ หมายเลขที่เลือก ไม่ใช่กฎของ tag
วัดได้ด้วยการเลื่อนหมายเลขของ quantity ทีละขั้น โดยไม่แตะอย่างอื่นเลย
หมายเลขของ quantity | tag บนสาย | ทั้งใบ |
|---|---|---|
| 2 | 10 | 8 byte |
| 15 | 78 | 8 byte |
| 16 | 8001 | 9 byte |
หมายเลข 15 คือตัวสุดท้ายที่ยังลงได้ใน byte เดียว เพราะ (15 << 3) | 7 ได้ 127
ซึ่งเป็นค่ามากที่สุดที่ varint หนึ่ง byte ถือได้ ตัวถัดไปจึงต้องใช้สอง byte
ช่องความยาวก็เป็น varint ด้วยเหตุผลเดียวกัน ชื่อลูกค้าที่ยาว 130 ตัวอักษรทำให้ช่องความยาว
กลายเป็น 8201 สอง byte และทั้งใบเป็น 135 byte
นี่คือที่มาของคำแนะนำว่าให้เก็บหมายเลข 1 ถึง 15 ไว้ให้ช่องที่ส่งบ่อยที่สุด ราคาไม่ได้จ่ายครั้งเดียว มันจ่ายทุก message
flowchart LR B["byte 0a"] --> S["เลื่อนขวา 3 bit"] B --> M["และกับ 7"] S --> N["หมายเลข field = 1"] M --> T["ชนิดบนสาย = 2"] N --> R["ช่อง customer"] T --> R
คำบรรยายภาพ: byte เดียวถูกอ่านสองครั้ง ครั้งแรกได้หมายเลขของ field ครั้งที่สองได้ชนิดของมัน แล้วสองค่านี้รวมกันจึงชี้ไปที่ช่องเดียวใน schema
เปลี่ยนชื่อ field แล้ววัดใหม่
หัวข้อที่มีชื่อว่า “เปลี่ยนชื่อ field แล้ววัดใหม่”เปลี่ยน
customerเป็นcustomer_nameแล้ว byte ทั้งแปดตัวเหมือนเดิมทุกตัว
schema ทรงที่สองต่างจากทรงแรกที่ชื่อของ field หมายเลข 1 อย่างเดียว หมายเลขเท่าเดิม ชนิดเท่าเดิม และค่าที่ใส่ก็เท่าเดิม
message Order { string customer_name = 1; int32 quantity = 2;}Go เขียนออกมาได้ 0a04776972611007 ซึ่งเท่ากับของทรงแรกทุกตัวอักษร และเมื่อให้ Java
ที่ถือ schema ชื่อใหม่อ่าน byte ที่ทรงชื่อเก่าเขียนไว้ มันได้ customer_name เป็น wira
กับ quantity เป็น 7
ผู้อ่านไม่ได้ผิดหวังกับชื่อที่ไม่ตรง เพราะมันไม่เคยเห็นชื่อของผู้เขียนตั้งแต่แรก มันเห็นแค่ว่ามีของมาที่ช่องหมายเลข 1 แล้วมันเปิด schema ของตัวเองดูว่าช่องนั้นชื่ออะไร
เรื่องนี้เกิดขึ้นจริงตอนสร้างคอร์สนี้
หัวข้อที่มีชื่อว่า “เรื่องนี้เกิดขึ้นจริงตอนสร้างคอร์สนี้”ผู้เขียนคอร์สเปลี่ยนชื่อ field ทุกตัวในทุกทรงพร้อมกัน แล้ว hex 11 จาก 12 ค่าไม่ขยับเลย
ตอนแรกคอร์สนี้ใช้โดเมนคลินิก ช่องทั้งสามชื่อ patient room และ clinic ต่อมาย้ายมาใช้
โดเมนออเดอร์ ชื่อทั้งสามจึงกลายเป็น customer quantity และ branch
ตอนนั้นตาราง WRITES ใน file ตัวเลขที่ probe เขียนไว้ ถือ hex อยู่ 12 ค่า วัดทั้งก่อนและหลังการย้าย ผลคือ
11 ค่าไม่ขยับสักตัว ผู้อ่านตรวจเองได้ด้วยการเทียบ git show bd44452 กับ
git show ac61437
ค่าที่ขยับมีค่าเดียว และมันเป็นค่าเดียวที่เปลี่ยนค่าข้างใน ไม่ใช่เปลี่ยนชื่อ
คือข้อความ clinic-b ที่กลายเป็น branch-b
การแก้ครั้งนั้นไม่ได้ตั้งใจจะเป็นการทดลอง แต่มันวัดกฎของบทนี้ได้ตรงกว่าการทดลองที่ตั้งใจ
ราคาของกฎข้อนี้
หัวข้อที่มีชื่อว่า “ราคาของกฎข้อนี้”ชื่อเปลี่ยนได้ฟรี เพราะมันไม่เคยอยู่บนสาย และหมายเลขเปลี่ยนไม่ได้ ด้วยเหตุผลเดียวกัน
ด้านที่ได้เปล่าคือชื่อ ทีมหนึ่งเปลี่ยน customer เป็น customer_name วันนี้ได้เลย
โดยไม่ต้องรออีกทีม เพราะอีกทีมไม่เคยอ่านชื่อนั้น
ด้านที่ต้องจ่ายคือหมายเลข หมายเลข 1 ในทีมของคุณกับหมายเลข 1 ในทีมอีกฝั่ง ต้องหมายถึง ของอย่างเดียวกันตลอดไป และไม่มีใครในสองทีมเห็นข้อตกลงนี้จาก code ที่ตัวเองเขียน
บทที่ 2 ถามคำถามที่มาก่อนเรื่องหมายเลข คือ field ที่มีค่าเป็นค่าเริ่มต้น มันเดินทางไปด้วยหรือเปล่า ส่วนคำถามเรื่องหมายเลขที่ถูกใช้ซ้ำ รออยู่ที่ บทที่ 4
-
Protocol Buffers — Encoding (เข้าถึง 2026-09-04) — ที่มาของสูตร tag และของรายการ wire type
-
Protocol Buffers — Language Guide (proto 3) (เข้าถึง 2026-09-04) — ที่มาของกฎว่าหมายเลข field คือสิ่งที่ผูกสัญญาไว้ ไม่ใช่ชื่อ
-
ตัวเลขทุกตัวในบทนี้มาจาก
scripts/wire-contract/probe.pyซึ่งให้ Go เขียน byte แล้วให้ Java อ่านกลับ ต้องสั่งscripts/wire-contract/up.shก่อนหนึ่งครั้ง ซึ่งดึงเครื่องมือสามตัวที่ตรึง version ไว้- protoc 36.1
- protoc-gen-go v1.36.12
- protobuf-java 4.36.1
เช็กความเข้าใจ — บทที่ 1
ข้อ 1 / 3byte ตัวแรกของออเดอร์คือ `0a` มันบอกอะไรกับผู้อ่าน