ข้าม​ไป​ยัง​เนื้อหา

field ที่​ไม่​เคย​ออก​จาก​บ้าน — จำนวน​เป็น​ศูนย์​แล้ว​ทั้ง​ช่อง​หาย​ไป​จาก​สาย

บท​ที่ 1 จบ​ด้วย​ออ​เด​อร์​ที่ customer เป็น wira และ quantity เป็น 7 ซึ่ง Go เขียน​ออก​มา ได้ 8 byte บท​นี้​แก้​ค่า​เดียว​ใน​ใบ​เดิม คือ​ให้ quantity เป็น 0 แล้ว​วัด​ใหม่

ผล​ที่​ได้​คือ 6 byte สิ่ง​ที่​หาย​ไป​ไม่ใช่​แค่​ค่า แต่​เป็น​ทั้ง​ช่อง tag ของ field หมายเลข 2 ก็​ไม่​ได้​ขึ้น​สาย​ไป​ด้วย

📦 orderctl — ของ​ชิ้น​เดียวกัน​กับ​สาม​คอร์ส​ภาษา​ใน​เสา​นี้

orderctl คือ​เครื่อง​มือ​ประมวล​ผล​ออ​เด​อร์​ที่​สาม​คอร์ส​ภาษา​ใน​เสา​นี้​สร้าง​ซ้ำ​ที​ละ​ภาษา repo ตัวอย่าง กำลัง​จัด​ทำ ระหว่าง​นี้​ทุก​ตัวเลข​ใน​คอร์ส​นี้​รัน​เอง​ได้ สั่ง scripts/wire-contract/up.sh เพื่อ​ดึง​เครื่อง​มือ​ที่​ตรึง version ไว้ แล้ว​ตาม​ด้วย python3 scripts/wire-contract/probe.py

schema ไม่​เปลี่ยน​เลย​สัก​บรรทัด เปลี่ยน​แค่​ค่าที่​ใส่ แล้ว​ขนาด​ไล่​จาก 8 ลง​มา​ถึง 0 byte

Order ทรง​ตั้ง​ต้น​ของ​คอร์ส​ยัง​เป็น​ทรง​เดิม​ทุก​ตัว​อักษร

order.proto — ทรง​เดิม​ของ​บท​ที่ 1
syntax = "proto3";
package order.v1;
message Order {
string customer = 1;
int32 quantity = 2;
}

Go เขียน​สี่​ใบ​จาก schema ใบ​นี้ ต่าง​กัน​ที่​ค่าที่​ใส่​ลง​ไป​อย่าง​เดียว

ค่าที่​ใส่bytehex
customer = wira และ quantity = 780a04776972611007
customer = wira และ quantity = 060a0477697261
customer ว่าง และ quantity = 721007
ค่า​เริ่ม​ต้น​ทั้ง​คู่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
0a047769726110077n/a
0a04776972610 ที่​ผู้​อ่าน​เติม​เองn/a

n/a ไม่​ได้​แปล​ว่า probe วัด​ไม่​ได้ มัน​แปล​ว่า​ผู้​อ่าน​ทรง​นี้​ไม่มี​คำ​ตอบ​ให้ ทรง​ที่ protoc generate ออก​มา​สำหรับ field แบบ​นี้ ไม่มี​ทาง​ถาม​เรื่อง presence เลย ทั้ง​ฝั่ง Go และ​ฝั่ง Java

ผล​คือ​ผู้​อ่าน​ที่​ได้ 0a0477697261 มา เห็น quantity เป็น 0 เท่านั้น มัน​แยก​ไม่​ออก​ว่า อีก​ฝั่ง​ตั้ง​ค่า​เป็น​ศูนย์​จริง หรือ​ไม่​เคย​แตะ​ช่อง​นั้น​เลย

ความ​ต่าง​สอง​อย่าง​นี้​สำคัญ​กับ​ออ​เด​อร์​จริง “สั่ง 0 จาน” กับ “ยัง​ไม่​ได้​กรอก​จำนวน” เป็น​คนละ​เรื่อง​กัน​สำหรับ​คน​ที่​รับ​ออ​เด​อร์​ไป​ทำ แต่​บน​สาย​มัน​คือ byte ชุด​เดียวกัน

เติม​คำ​ว่า optional หน้า field เดียว แล้ว quantity = 0 เขียน​ออก​มา​เป็น 8 byte

ทรง​ที่​สอง​ต่าง​จาก​ทรง​แรก​ที่​คำ​เดียว หมายเลข​เท่า​เดิม ชนิด​เท่า​เดิม และ​ชื่อ​เท่า​เดิม

ขอ explicit presence ให้ field เดียว — เฉพาะ​ส่วน message
message Order {
string customer = 1;
optional int32 quantity = 2;
}

optional ใน proto3 คือ​คำขอ explicit presenceexplicit presenceสิ่ง​ที่ optional คืน​กลับ​มา​ให้ · จำนวน​ที่​เป็น​ศูนย์​ขึ้น​สาย​จริง​เป็น 1000 และ​ผู้​อ่าน​ตอบ presence ได้​ว่า true หรือ false · ราคา​คือ​แปด byte แทน​หก byte ผู้​เขียน​จะ​เขียน field นั้น​ลง​สาย เมื่อ​มัน​ถูก​ตั้ง​ค่า ไม่​ว่า​ค่า​นั้น​จะ​เท่ากับ​ค่า​เริ่ม​ต้น​หรือ​ไม่

ทรงค่าที่​ใส่bytehex
proto3 ธรรมดาquantity = 060a0477697261
optionalตั้ง quantity เป็น 080a04776972611000
optionalไม่​ตั้ง quantity เลย60a0477697261

แถว​กลาง​คือ​ของ​ใหม่​ของ​บท​นี้ ช่อง​จำนวน​กลับ​ขึ้น​มา​อยู่​บน​สาย​แล้ว ทั้ง​ที่​ค่า​ของ​มัน​เป็น​ศูนย์

byteค่าความ​หมาย
10atag ของ field หมายเลข 1 ชนิด length-delimited
204ความ​ยาว​ของ​ค่า คือ 4 byte
3–677 69 72 61ตัว​อักษร w i r a
710tag ของ field หมายเลข 2 ชนิด​ตัวเลข​แบบ varintvarintการ​เข้า​รหัส​จำนวนเต็ม​ที่​ใช้ byte เท่า​ที่​ค่า​นั้น​ต้องการ · เลข 7 กิน byte เดียว ส่วน int32 ที่​เป็น −1 กิน​สิบ byte
800ค่า​คือ 0

สอง byte ท้าย​คือ 1000 ซึ่ง​เป็น tag ตัว​เดิม​ของ​บท​ที่ 1 แล้ว​ตาม​ด้วย​ค่า​ศูนย์ varint ของ​เลข 0 คือ byte เดียว​ที่​มี​ค่า​เป็น​ศูนย์ ไม่ใช่​การ​ไม่มี byte

แถว​ล่าง​สุด​ของ​ตาราง​บอก​ว่า optional ไม่​ได้​บังคับ​ให้​ทุก​ใบ​ยาว​ขึ้น ทรง optional ที่​ไม่​ตั้ง​ค่า เขียน​ออก​มา​ได้ 0a0477697261 ซึ่ง​เท่ากับ​ที่​ทรงธรรม​ดา​เขียน​ตอน​จำนวน​เป็น​ศูนย์ ทุก​ตัว​อักษร

byte ชุด​เดียวกัน​ได้​คำ​ตอบ false ส่วน byte ของ​ตัว​เอง​ได้ true ทั้ง Go และ Java

ตรง​นี้​คือ​สิ่ง​ที่ optional ซื้อ​มา​จริงๆ ผู้​อ่าน​ทรง​นี้​ตอบ​คำถาม presence ได้ และ​คำ​ตอบ​ของ​มัน ไม่​ได้​เหมือน​กัน​ทุก​ครั้ง

ผู้​อ่าน​ถือ​ทรงอ่าน byte ชุด​ไหนได้ quantity เป็น​อะไรpresence
proto3 ธรรมดา0a04776972610 ที่​ผู้​อ่าน​เติม​เองn/a
optional0a04776972610 ที่​ผู้​อ่าน​เติม​เองfalse
optional0a047769726110000 ที่มา​จาก 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

ใบ​ที่​จำนวน​เป็น​ศูนย์​และ​ถูก​ตั้ง​ค่า​ไว้ จ่าย 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 / 3

schema ทรงเดิมของบทที่ 1 ถูกใส่ค่า `customer = wira` และ `quantity = 0` แล้ว Go เขียนออกมาได้กี่ byte