ชื่อเดียว หรือหลายชื่อ
ออเดอร์ใบหนึ่งเคยมีลูกค้าคนเดียว วันหนึ่งทีมหนึ่งอยากให้ใบเดียวมีลูกค้าได้หลายคน
การแก้ที่ตรงที่สุดคือเติมคำว่า repeated หน้าช่องเดิม โดยไม่แตะหมายเลขของมัน
การแก้แบบนี้ไม่มี error ไม่มีคำเตือนจาก protoc และอีกฝั่งที่ยังไม่ได้ deploy ก็อ่านผ่าน บทนี้วัดว่าฝั่งที่ยังไม่ได้ deploy อ่านได้ค่าอะไร และมันเขียนกลับออกมาเป็นอะไร
orderctl คือเครื่องมือประมวลผลออเดอร์ที่สามคอร์สภาษาในเสานี้สร้างซ้ำทีละภาษา repo ตัวอย่าง
กำลังจัดทำ ระหว่างนี้ทุกตัวเลขในคอร์สนี้รันเองได้ สั่ง
scripts/wire-contract/up.sh เพื่อดึงเครื่องมือที่ตรึง version ไว้ แล้วตามด้วย
python3 scripts/wire-contract/probe.py
หมายเลข 1 โผล่สองครั้งบนสายเส้นเดียว
หัวข้อที่มีชื่อว่า “หมายเลข 1 โผล่สองครั้งบนสายเส้นเดียว”สองชื่อในช่อง
repeatedขึ้นสายเป็น byte 17 ตัว และ tag0aโผล่สองครั้ง
schema ทรงนี้ต่างจากทรงตั้งต้นที่คำเดียว คือคำว่า repeated หน้าช่องหมายเลข 1
หมายเลขเท่าเดิม ชนิดของค่าเท่าเดิม
message Order { repeated string customer = 1; int32 quantity = 2;}Go ใส่ชื่อสองชื่อคือ wira และ somchai แล้วใส่ quantity เป็น 7 ผลที่ได้คือ
17 byte เขียนเป็น hex ได้แบบนี้
0a04776972610a07736f6d636861691007แกะทีละกลุ่มด้วยกฎเดียวกับบทที่ 1 คือทุกกลุ่มขึ้นต้นด้วย tag แล้วตามด้วยค่าของมัน
| byte | ค่า | ความหมาย |
|---|---|---|
| 1 | 0a | tag ของ field หมายเลข 1 ชนิดบอกความยาวนำหน้า |
| 2 | 04 | ความยาวของค่าแรก คือ 4 byte |
| 3–6 | 77697261 | ตัวอักษรของ wira |
| 7 | 0a | tag ของ field หมายเลข 1 อีกครั้ง |
| 8 | 07 | ความยาวของค่าที่สอง คือ 7 byte |
| 9–15 | 736f6d63686169 | ตัวอักษรของ somchai |
| 16 | 10 | tag ของ field หมายเลข 2 ชนิด varint |
| 17 | 07 | ค่าคือ 7 |
สังเกตว่าไม่มี byte ตัวไหนบอกว่ารายการเริ่มตรงไหน ไม่มีตัวไหนบอกว่ามีสมาชิกกี่ตัว
และไม่มีตัวไหนบอกว่าผู้เขียนประกาศช่องนี้เป็น repeated
รูปแบบบนสายwire formatรูปแบบของ byte ที่ protobuf ใช้จริงบนสาย · แต่ละ field คือ tag แล้วตามด้วยค่า และไม่มีชื่อ field เดินทางไปด้วยสักตัวของช่องเดียวกับของช่องที่ถือสองค่าในตัวอย่างนี้ จึงต่างกันแค่จำนวนครั้ง ที่หมายเลขนั้นโผล่ ตัวหมายเลขกับชนิดบนสายไม่ได้เปลี่ยนเลย
ข้อนี้เป็นข้อของช่องชนิด string ซึ่งส่งความยาวนำหน้าค่าเสมอ เอกสาร proto3 ระบุไว้ว่า
repeated ของชนิดตัวเลขไม่ได้ขึ้นสายเป็นกลุ่มแยกแบบนี้ มันขึ้นเป็นกลุ่ม packed กลุ่มเดียว
บทนี้ไม่ได้วัดทรงนั้น ทุกข้อสรุปข้างล่างจึงเป็นข้อสรุปของช่องชนิด string
ผู้อ่านที่ยังประกาศช่องเดียว
หัวข้อที่มีชื่อว่า “ผู้อ่านที่ยังประกาศช่องเดียว”ผู้อ่านทรงตั้งต้นอ่าน byte ชุดนั้นผ่าน ไม่มี unknown field และเก็บค่าสุดท้ายไว้
ผู้อ่านที่ยังถือ schema ทรงตั้งต้น ประกาศช่องหมายเลข 1 เป็น string customer = 1;
ตัวเดียว มันอ่าน byte 17 ตัวข้างบน แล้วตอบแบบนี้
| ช่องที่วัด | Go ตอบ | Java ตอบ |
|---|---|---|
| parse | ok | ok |
customer | somchai | somchai |
quantity | 7 | 7 |
| จำนวน unknown fieldunknown fieldfield ที่ผู้อ่านไม่รู้จัก เพราะ schema ของตัวเองไม่มีหมายเลขนั้น · ผู้อ่านเก็บ byte ดิบไว้เฉยๆ แล้วส่งคืนตอน serialize กลับ | 0 | 0 |
สองภาษาตอบตรงกันทุกช่อง ไม่มี exception ไม่มีคำเตือน และไม่มีหมายเลขไหนตกค้างเป็น unknown field ให้ใครสงสัยภายหลัง
ค่าที่เหลืออยู่ในช่องคือ somchai ซึ่งเป็นค่าสุดท้ายบนสาย ไม่ใช่ wira ซึ่งเป็นค่าแรก
กฎข้อนี้เป็นกฎของ protobuf เอง เอกสาร encoding เรียกมันว่า last one wins
และเอกสาร proto3 ระบุไว้ในหัวข้อเรื่องการแก้ schema ว่าผู้อ่านที่ประกาศช่องเดียว
จะรับค่าสุดท้ายเมื่อช่องนั้นเป็นชนิดพื้นฐาน
flowchart TD W["byte 17 ตัวจากผู้เขียน repeated"] --> G1["กลุ่มแรก tag 0a ค่า wira"] W --> G2["กลุ่มที่สอง tag 0a ค่า somchai"] G1 --> S["ช่อง customer ช่องเดียวของผู้อ่าน"] G2 --> S S --> V["ค่าที่เหลืออยู่คือ somchai"] V --> O["เขียนกลับได้ 11 byte"]
คำบรรยายภาพ: สองกลุ่มบนสายชี้ไปที่ช่องเดียวกัน ผู้อ่านจึงเขียนทับของเดิมแล้วเหลือค่าสุดท้ายไว้ค่าเดียว ตอนเขียนกลับ มันมีของให้เขียนแค่ค่านั้นค่าเดียว
ชื่อแรกหายจากสาย ไม่ใช่แค่ไม่ถูกแสดง
หัวข้อที่มีชื่อว่า “ชื่อแรกหายจากสาย ไม่ใช่แค่ไม่ถูกแสดง”สั่งให้ผู้อ่านตัวนั้น serialize กลับ ได้ byte 11 ตัว และไม่มี
wiraอยู่ในนั้น
คำถามที่สำคัญกว่า “ผู้อ่านเห็นค่าอะไร” คือ “ผู้อ่านส่งอะไรต่อไป” เพราะบริการส่วนมาก อ่าน message มาแล้วเขียนกลับออกไปให้บริการถัดไป
ให้ผู้อ่านทรงตั้งต้นเขียนสิ่งที่มันเพิ่งอ่านกลับออกมา สองภาษาได้ผลตรงกัน
| ทิศทาง | byte | hex |
|---|---|---|
ผู้เขียน repeated เขียนออกมา | 17 | 0a04776972610a07736f6d636861691007 |
| ผู้อ่านช่องเดียวเขียนกลับ | 11 | 0a07736f6d636861691007 |
byte ที่หายไปหกตัวคือกลุ่มแรกทั้งกลุ่ม ทั้ง tag ทั้งความยาว และทั้งสี่ตัวอักษรของ wira
การเดินทางไปกลับround tripการอ่าน byte เข้ามาแล้ว serialize กลับออกไป · เท่าเดิมทุก byte ในเกือบทุกงาน ยกเว้นสองงานที่เหลือสิบสอง byte และสิบเอ็ด byteรอบนี้จึงไม่คืนของเท่าเดิม
ตรงนี้คือจุดที่ต้องแยกให้ชัด wira ไม่ได้ถูกซ่อนไว้แล้วรอเขียนกลับ มันไม่มีที่อยู่ใน
message ของผู้อ่านตั้งแต่ตอน parse จบ ของที่ไม่มีที่อยู่ ก็ไม่มีอะไรให้เขียนกลับ
บริการถัดไปที่รับ byte 11 ตัวนี้ ไม่มีทางรู้เลยว่าเคยมีชื่อสองชื่อ เพราะสิ่งที่มันได้รับ เหมือนกับ message ที่มีชื่อเดียวมาตั้งแต่ต้นทางทุกตัวอักษร
ทำไมบทนี้ไม่เหมือนบทที่ 3 และบทที่ 4
หัวข้อที่มีชื่อว่า “ทำไมบทนี้ไม่เหมือนบทที่ 3 และบทที่ 4”สามงานอ่านของสองบทนั้นคืน byte เท่าเดิม ทั้งที่จำนวน unknown field ไม่เท่ากันทุกงาน
บทที่ 3 กับบทที่ 4 วัดงานอ่านสี่งานที่ parse ผ่านแล้วเขียนกลับได้ byte เท่าเดิม สามงานในนั้นอยู่ในตารางข้างล่าง สองงานแรกผู้อ่านไม่มีช่องรองรับของที่เจอ ส่วนงานที่สามมีช่องรองรับพอดี
| กรณี | unknown | เขียนกลับ |
|---|---|---|
| ผู้อ่าน v1 เจอ field หมายเลข 3 ที่เพิ่มมาใหม่ · บทที่ 3 | 1 | เท่าเดิม |
| ผู้อ่านที่เอาหมายเลข 2 ไปใช้กับ wire type อื่น · บทที่ 4 | 1 | เท่าเดิม |
ผู้อ่านที่เอาหมายเลข 2 ไปใช้กับ int32 ตัวอื่น · บทที่ 4 | 0 | เท่าเดิม |
| ผู้อ่านช่องเดียวเจอชื่อสองชื่อ · บทนี้ | 0 | ไม่เท่า |
unknown field คือกลไกที่แบกของข้ามผู้อ่านรุ่นเก่าไปให้ปลายทาง และกลไกนั้นทำงานก็ต่อเมื่อ ผู้อ่านไม่รู้จักหมายเลขที่เจอ สองแถวแรกจึงคืน byte ครบด้วยกลไกนี้
แถวที่สามคืน byte ครบโดยไม่ได้ใช้กลไกนั้นเลย ผู้อ่านรู้จักหมายเลข 2 มีช่องรองรับ และรับค่าเข้าช่องใหม่ทั้งค่า unknown จึงเป็น 0 · เลข 0 ในช่องนั้นไม่ได้แปลว่าของหาย
ความต่างของแถวสุดท้ายอยู่ที่จำนวนกลุ่มบนสาย เทียบกับจำนวนค่าที่ช่องของผู้อ่านเก็บได้ สองกลุ่มถือหมายเลข 1 เข้ามา แต่ช่องของผู้อ่านเก็บได้ค่าเดียว ค่าแรกจึงไม่มีที่อยู่
นี่คือรูปที่อันตรายกว่าของบทที่ 3 และบทที่ 4 หนึ่งขั้น เพราะสามงานนั้นยังเหลือของ อยู่ใน message ทุกงาน ส่วนบทนี้ของหายไปโดยที่ทุกด่านฝั่งผู้อ่านเขียวหมด
ทางกลับกันปลอดภัย
หัวข้อที่มีชื่อว่า “ทางกลับกันปลอดภัย”ผู้อ่านที่ประกาศ
repeatedอ่าน byte ของทรงตั้งต้นได้หนึ่งค่า และเขียนกลับได้เท่าเดิม
สลับสองฝั่ง ให้ผู้เขียนถือทรงตั้งต้นที่มีชื่อเดียว แล้วให้ผู้อ่านถือทรง repeated
| ช่องที่วัดในทางกลับ | Go ตอบ | Java ตอบ |
|---|---|---|
| parse | ok | ok |
customer | wira | wira |
quantity | 7 | 7 |
| จำนวน unknown field | 0 | 0 |
| เขียนกลับ | 0a04776972611007 | 0a04776972611007 |
byte ที่เขียนกลับเท่ากับ 8 byte ของทรงตั้งต้นทุกตัวอักษร ผู้อ่านได้สมาชิกหนึ่งตัวลงในรายการ ของมัน แล้วเขียนกลับออกมาเป็นกลุ่มเดียวเหมือนที่รับมา
ทิศนี้จึงเป็นความเข้ากันได้กับของเดิมbackward compatibilityผู้อ่านรุ่นใหม่อ่าน byte ที่ผู้เขียนรุ่นเก่าส่งมาได้ · วัดแล้วว่าผู้อ่าน v2 อ่านของ v1 ได้ค่าครบ และไม่มี unknown สักหมายเลขที่แท้จริง ผู้อ่านรุ่นใหม่ที่ประกาศ
repeated อ่านของที่ผู้เขียนรุ่นเก่าส่งมาได้ครบ และไม่ทำของหายระหว่างทาง
ทั้งสองทิศในบทนี้วัดกับช่องชนิด string ช่องเดียว เอกสาร proto3 ระบุความเข้ากันได้ระหว่าง
ช่องเดียวกับ repeated ไว้กับ string bytes และ message เท่านั้น และกันชนิดตัวเลข
ออกจากข้อนี้ เพราะ repeated ของชนิดตัวเลขขึ้นสายเป็นกลุ่ม packed
ราคาของการเติมคำว่า repeated
หัวข้อที่มีชื่อว่า “ราคาของการเติมคำว่า repeated”การแก้ครั้งเดียวปลอดภัยด้านหนึ่งและทำของหายอีกด้านหนึ่ง ลำดับ deploy จึงเป็นสิ่งที่ต้องเลือก
สรุปสองทิศที่วัดได้ในบทนี้เป็นรายการเดียว
- ผู้อ่านที่ประกาศ
repeatedอ่านของช่องเดียว ได้ค่าครบ และเขียนกลับได้ 8 byte เท่าเดิม - ผู้อ่านที่ประกาศช่องเดียว อ่านของ
repeatedที่มีสองชื่อ ได้ค่าสุดท้ายค่าเดียว - ผู้อ่านตัวหลังเขียนกลับได้ 11 byte จาก 17 byte ที่รับมา และไม่มีอะไรฟ้อง
สิ่งที่ตามมาคือลำดับของ deploy ไม่ใช่เรื่องรอง ถ้าฝั่งอ่านขึ้นก่อนฝั่งเขียน ทุกทางเดิน อยู่ในทิศที่ปลอดภัย ถ้าสลับกัน จะมีช่วงเวลาที่ชื่อแรกหายไปเงียบๆ
และถ้าปลายทางของ message คือบริการที่สาม ความเสียหายไม่ได้จบที่บริการกลาง มันเดินต่อไป กับ byte 11 ตัวที่ถูกเขียนใหม่แล้ว
ทุกงานอ่านในหกบทที่ผ่านมาถูกวัดสองรอบ ทั้ง Go และ Java ส่วนฝั่งเขียนเป็น Go ตัวเดียว บทที่ 7 วางงานอ่านทั้งชุดลงในตารางเดียว แล้วตอบว่าการยืนยันด้วยสองภาษาซื้ออะไรกลับมาบ้าง
-
Protocol Buffers — Encoding (เข้าถึง 2026-09-04) — ที่มาของกฎ last one wins และของกฎว่าสมาชิกของช่อง
repeatedชนิดstringแต่ละตัวขึ้นต้นด้วย tag ของตัวเอง ส่วนชนิดตัวเลขขึ้นเป็นกลุ่ม packed กลุ่มเดียว -
Protocol Buffers — Language Guide (proto 3) (เข้าถึง 2026-09-04) — หัวข้อการแก้ schema ที่ระบุว่าผู้อ่านซึ่งประกาศช่องเดียว รับค่าสุดท้ายเมื่อช่องนั้นเป็นชนิดพื้นฐาน และที่จำกัดความเข้ากันได้ระหว่างช่องเดียว กับ
repeatedไว้กับstringbytesและ message -
ตัวเลขทุกตัวในบทนี้มาจาก
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
เช็กความเข้าใจ — บทที่ 6
ข้อ 1 / 3ผู้เขียนที่ประกาศ `repeated string customer = 1;` ใส่สองชื่อแล้วเขียนออกมา byte ชุดนั้นหน้าตาเป็นอย่างไร