field ที่ไม่เคยออกจากบ้าน — จำนวนเป็นศูนย์แล้วทั้งช่องหายไปจากสาย
บทที่ 1 จบด้วยออเดอร์ที่ customer เป็น wira และ quantity เป็น 7 ซึ่ง Go เขียนออกมา
ได้ 8 byte บทนี้แก้ค่าเดียวในใบเดิม คือให้ quantity เป็น 0 แล้ววัดใหม่
ผลที่ได้คือ 6 byte สิ่งที่หายไปไม่ใช่แค่ค่า แต่เป็นทั้งช่อง tag ของ field หมายเลข 2 ก็ไม่ได้ขึ้นสายไปด้วย
orderctl คือเครื่องมือประมวลผลออเดอร์ที่สามคอร์สภาษาในเสานี้สร้างซ้ำทีละภาษา repo ตัวอย่าง
กำลังจัดทำ ระหว่างนี้ทุกตัวเลขในคอร์สนี้รันเองได้ สั่ง
scripts/wire-contract/up.sh เพื่อดึงเครื่องมือที่ตรึง version ไว้ แล้วตามด้วย
python3 scripts/wire-contract/probe.py
สี่ใบจาก schema เดียวกัน
หัวข้อที่มีชื่อว่า “สี่ใบจาก schema เดียวกัน”schema ไม่เปลี่ยนเลยสักบรรทัด เปลี่ยนแค่ค่าที่ใส่ แล้วขนาดไล่จาก 8 ลงมาถึง 0 byte
Order ทรงตั้งต้นของคอร์สยังเป็นทรงเดิมทุกตัวอักษร
syntax = "proto3";
package order.v1;
message Order { string customer = 1; int32 quantity = 2;}Go เขียนสี่ใบจาก schema ใบนี้ ต่างกันที่ค่าที่ใส่ลงไปอย่างเดียว
| ค่าที่ใส่ | byte | hex |
|---|---|---|
customer = wira และ quantity = 7 | 8 | 0a04776972611007 |
customer = wira และ quantity = 0 | 6 | 0a0477697261 |
customer ว่าง และ quantity = 7 | 2 | 1007 |
| ค่าเริ่มต้นทั้งคู่ | 0 | (ไม่มี byte สักตัว) |
แถวที่สองคือใบที่บทนี้สนใจ hex ของมันคือสิบสองตัวอักษรแรกของแถวแรกเป๊ะ แล้วจบตรงนั้น
สอง byte ท้ายซึ่งเคยเป็น 10 กับ 07 ไม่มีอยู่
แถวที่สามตัดจากอีกด้าน ชื่อลูกค้าที่เป็นข้อความว่างไม่ขึ้นสาย เหลือแค่ช่องจำนวน
ซึ่งเป็น 1007 สอง byte
แถวสุดท้ายคือใบที่ทั้งสองช่องเป็นค่าเริ่มต้น ผลที่วัดได้คือ 0 byte ไม่มีหัว ไม่มีท้าย
และไม่มีอะไรที่บอกว่านี่คือ Order
ทำไมมันหายทั้งช่อง ไม่ใช่หายแค่ค่า
หัวข้อที่มีชื่อว่า “ทำไมมันหายทั้งช่อง ไม่ใช่หายแค่ค่า”proto3 ตั้งต้นด้วย implicit presence ค่าที่เท่ากับค่าเริ่มต้นจึงไม่ถูกเขียนลงสายเลย
การมีอยู่ของค่าfield presenceคำถามว่าค่านั้นถูกตั้งไว้ หรือเป็นค่าเริ่มต้นที่ไม่มีใครแตะ · เป็นคำถามคนละข้อกับค่าที่อ่านได้ และผู้อ่านบางทรงตอบไม่ได้เลยคือคำถามว่าค่านั้นถูกตั้งไว้ หรือเป็นค่าเริ่มต้นที่ไม่มีใครแตะ มันเป็นคำถามคนละข้อกับคำถามว่าค่าคืออะไร
proto3 ตอบคำถามข้อนี้ด้วยกฎที่ชื่อ implicit presenceimplicit presenceพฤติกรรมตั้งต้นของ proto3 · ค่าที่เท่ากับค่าเริ่มต้นไม่ขึ้นสายเลย ผู้อ่านจึงแยกไม่ออกว่าไม่ได้ตั้ง หรือตั้งเป็นศูนย์ ผู้เขียนไม่เขียน field ที่ค่าเท่ากับ ค่าเริ่มต้นลงบนสาย และผู้อ่านที่ไม่เห็น field นั้นก็เติมค่าเริ่มต้นให้เอง
กฎนี้จึงตัดทั้งกลุ่ม ไม่ใช่ตัดแค่ byte ของค่า เพราะหนึ่งกลุ่มบนสายคือ tag แล้วตามด้วยค่า เขียน tag ทิ้งไว้เฉยๆ โดยไม่มีค่าตามหลังไม่ได้ ผู้เขียนที่ทำตามกฎนี้จึงไม่เขียนทั้งกลุ่ม แล้ว tag กับค่าก็หายไปพร้อมกัน
ค่าเริ่มต้นของแต่ละชนิดเป็นค่าที่ proto3 กำหนดไว้ตายตัว ไม่ใช่ค่าที่ผู้เขียน schema เลือกเอง
int32 ได้ 0 ส่วน string ได้ข้อความว่าง
ราคาที่ผู้อ่านจ่าย คือคำถามที่ตอบไม่ได้
หัวข้อที่มีชื่อว่า “ราคาที่ผู้อ่านจ่าย คือคำถามที่ตอบไม่ได้”ผู้อ่าน proto3 ทรงธรรมดาถูกถาม presence แล้วตอบว่า
n/aทั้ง Go และ Java
probe ให้ผู้อ่านสองภาษาอ่าน byte ทั้งสองชุด แล้วถามสองคำถามแยกกัน คือค่าคืออะไร และ presence ของช่องนั้นเป็นอย่างไร
| อ่าน byte ชุดไหน | ได้ quantity เป็นอะไร | presence ของ quantity |
|---|---|---|
0a04776972611007 | 7 | n/a |
0a0477697261 | 0 ที่ผู้อ่านเติมเอง | n/a |
n/a ไม่ได้แปลว่า probe วัดไม่ได้ มันแปลว่าผู้อ่านทรงนี้ไม่มีคำตอบให้ ทรงที่ protoc
generate ออกมาสำหรับ field แบบนี้ ไม่มีทางถามเรื่อง presence เลย ทั้งฝั่ง Go และฝั่ง Java
ผลคือผู้อ่านที่ได้ 0a0477697261 มา เห็น quantity เป็น 0 เท่านั้น มันแยกไม่ออกว่า
อีกฝั่งตั้งค่าเป็นศูนย์จริง หรือไม่เคยแตะช่องนั้นเลย
ความต่างสองอย่างนี้สำคัญกับออเดอร์จริง “สั่ง 0 จาน” กับ “ยังไม่ได้กรอกจำนวน” เป็นคนละเรื่องกันสำหรับคนที่รับออเดอร์ไปทำ แต่บนสายมันคือ byte ชุดเดียวกัน
ประกาศ optional แล้วศูนย์ได้ขึ้นสาย
หัวข้อที่มีชื่อว่า “ประกาศ optional แล้วศูนย์ได้ขึ้นสาย”เติมคำว่า
optionalหน้า field เดียว แล้วquantity = 0เขียนออกมาเป็น 8 byte
ทรงที่สองต่างจากทรงแรกที่คำเดียว หมายเลขเท่าเดิม ชนิดเท่าเดิม และชื่อเท่าเดิม
message Order { string customer = 1; optional int32 quantity = 2;}optional ใน proto3 คือคำขอ explicit presenceexplicit presenceสิ่งที่ optional คืนกลับมาให้ · จำนวนที่เป็นศูนย์ขึ้นสายจริงเป็น 1000 และผู้อ่านตอบ presence ได้ว่า true หรือ false · ราคาคือแปด byte แทนหก byte ผู้เขียนจะเขียน field นั้นลงสาย
เมื่อมันถูกตั้งค่า ไม่ว่าค่านั้นจะเท่ากับค่าเริ่มต้นหรือไม่
| ทรง | ค่าที่ใส่ | byte | hex |
|---|---|---|---|
| proto3 ธรรมดา | quantity = 0 | 6 | 0a0477697261 |
optional | ตั้ง quantity เป็น 0 | 8 | 0a04776972611000 |
optional | ไม่ตั้ง quantity เลย | 6 | 0a0477697261 |
แถวกลางคือของใหม่ของบทนี้ ช่องจำนวนกลับขึ้นมาอยู่บนสายแล้ว ทั้งที่ค่าของมันเป็นศูนย์
| byte | ค่า | ความหมาย |
|---|---|---|
| 1 | 0a | tag ของ field หมายเลข 1 ชนิด length-delimited |
| 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 | 00 | ค่าคือ 0 |
สอง byte ท้ายคือ 1000 ซึ่งเป็น tag ตัวเดิมของบทที่ 1 แล้วตามด้วยค่าศูนย์
varint ของเลข 0 คือ byte เดียวที่มีค่าเป็นศูนย์ ไม่ใช่การไม่มี byte
แถวล่างสุดของตารางบอกว่า optional ไม่ได้บังคับให้ทุกใบยาวขึ้น ทรง optional ที่ไม่ตั้งค่า
เขียนออกมาได้ 0a0477697261 ซึ่งเท่ากับที่ทรงธรรมดาเขียนตอนจำนวนเป็นศูนย์ ทุกตัวอักษร
ผู้อ่าน optional ตอบสองคำตอบที่ต่างกัน
หัวข้อที่มีชื่อว่า “ผู้อ่าน optional ตอบสองคำตอบที่ต่างกัน”byte ชุดเดียวกันได้คำตอบ
falseส่วน byte ของตัวเองได้trueทั้ง Go และ Java
ตรงนี้คือสิ่งที่ optional ซื้อมาจริงๆ ผู้อ่านทรงนี้ตอบคำถาม presence ได้ และคำตอบของมัน
ไม่ได้เหมือนกันทุกครั้ง
| ผู้อ่านถือทรง | อ่าน byte ชุดไหน | ได้ quantity เป็นอะไร | presence |
|---|---|---|---|
| proto3 ธรรมดา | 0a0477697261 | 0 ที่ผู้อ่านเติมเอง | n/a |
optional | 0a0477697261 | 0 ที่ผู้อ่านเติมเอง | false |
optional | 0a04776972611000 | 0 ที่มาจาก byte บนสาย | true |
สองแถวล่างคือช่องเดียวกัน ค่าที่อ่านได้เป็นศูนย์เหมือนกัน แต่ presence ต่างกัน เพราะ byte ชุดหนึ่งมี tag ของ field 2 อยู่ ส่วนอีกชุดไม่มี
Go กับ Java ตอบตรงกันทั้งสามแถว ทั้งค่าที่อ่านได้ จำนวน unknown field ซึ่งเป็น 0 ทุกแถว และ byte ที่ได้เมื่อ serialize กลับ ซึ่งเท่าเดิมทุกตัวทั้งสามแถว
flowchart LR A["ทรง proto3 ธรรมดา ตั้งจำนวนเป็นศูนย์"] --> B["0a0477697261 หก byte"] C["ทรง optional ไม่ตั้งจำนวนเลย"] --> B B --> D["ผู้อ่านทรงธรรมดา ถาม presence ได้ n/a"] B --> E["ผู้อ่านทรง optional ถาม presence ได้ false"]
คำบรรยายภาพ: ผู้เขียนสองทรงที่ต่างกันเขียน byte ชุดเดียวกันออกมา แล้วผู้อ่านสองทรง อ่าน byte ชุดนั้นได้คำตอบเรื่อง presence คนละแบบ คำตอบจึงมาจาก schema ของผู้อ่าน ไม่ใช่จาก byte
ราคาของ explicit presence
หัวข้อที่มีชื่อว่า “ราคาของ explicit presence”ใบที่จำนวนเป็นศูนย์และถูกตั้งค่าไว้ จ่าย 8 byte แทน 6 byte และจ่ายทุกใบแบบนั้น
optional ไม่ได้ให้ของฟรี ทุกใบที่ตั้งจำนวนเป็นศูนย์ ต้องพา tag กับค่าศูนย์ไปด้วย
คือสอง byte ที่ทรงเดิมไม่เคยส่ง
ราคาแบบนี้จ่ายทุกครั้งที่ส่ง เหมือนราคาของหมายเลข field ที่เกิน 15 ในบทที่ 1 ซึ่งจ่ายทุก message เท่ากัน ต่างกันตรงที่ราคานั้นผูกกับหมายเลขที่เลือกไว้ ส่วนราคานี้ผูกกับค่าที่ใส่ในใบนั้น
คำถามที่ต้องตอบก่อนเติมคำนั้นจึงมีข้อเดียว คือช่องนี้มีความต่างระหว่าง “ศูนย์” กับ “ยังไม่ได้กรอก” ไหม ถ้าไม่มี implicit presence ก็พอ และไม่ต้องจ่ายสอง byte นั้น
สรุปสิ่งที่บทนี้วัดได้ไว้สามข้อ
- ค่าเริ่มต้นใน proto3 ไม่ขึ้นสายเลย ทั้ง tag และค่า
- ผู้อ่านทรงธรรมดาตอบ presence ไม่ได้ ทั้ง Go และ Java ตอบ
n/aเหมือนกัน optionalให้คำตอบtrueกับfalseกลับมา โดยแลกกับ 8 byte แทน 6 byte ในใบที่จำนวนเป็นศูนย์
บทนี้แก้ค่าที่ใส่ กับเติมคำเดียวหน้า field เดิม โดยไม่ได้เพิ่มช่องใหม่เลยสักช่อง ส่วนการเพิ่ม field เข้าไปคือการแก้ schema แบบถัดไปที่คอร์สนี้วัด แล้วฝั่งที่ยังไม่ได้ deploy จะทำอย่างไรกับมัน อยู่ที่ บทที่ 3
-
Protocol Buffers — Application Note: Field Presence (เข้าถึง 2026-09-04) — ที่มาของกฎว่า proto3 ตั้งต้นด้วย implicit presence และของกฎว่า
optionalคือคำขอ explicit presence -
Protocol Buffers — Language Guide (proto 3) (เข้าถึง 2026-09-04) — ที่มาของรายการค่าเริ่มต้นประจำชนิด และของกฎว่าค่าที่เท่ากับ ค่าเริ่มต้นไม่ถูกเขียนลงสาย
-
Protocol Buffers — Encoding (เข้าถึง 2026-09-04) — ที่มาของกฎว่าแต่ละกลุ่มบนสายขึ้นต้นด้วย tag และของ varint
-
ตัวเลขทุกตัวในบทนี้มาจาก
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
เช็กความเข้าใจ — บทที่ 2
ข้อ 1 / 3schema ทรงเดิมของบทที่ 1 ถูกใส่ค่า `customer = wira` และ `quantity = 0` แล้ว Go เขียนออกมาได้กี่ byte