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

งาน​อ่าน 15 งาน สอง​ภาษา​ตอบ​ต่าง​กัน​ช่อง​เดียว

หกบท​ที่​ผ่าน​มา​เล่า​ผล​ของ​งาน​อ่าน​ที​ละ​งาน โดย​ไม่​ค่อย​บอก​ว่า​ใคร​เป็น​คน​อ่าน คำ​ตอบ​คือ​มี​คน​อ่าน สอง​คน​เสมอ ทุก​งาน​ถูก​วัด​สอง​รอบ รอบ​หนึ่ง​ด้วย Go และ​อีก​รอบ​ด้วย Java

บท​นี้​เอา​ผล​ทั้ง​สอง​ชุด​มา​วาง​เทียบ​กัน​ที​ละ​ช่อง เพื่อ​ตอบ​คำถาม​ที่​ค้าง​อยู่​ตลอด​คอร์ส​ว่า สิ่ง​ที่​คุณ​เห็น​ใน​หกบท​ที่​แล้ว เป็น​พฤติกรรม​ของ protobuf หรือ​เป็น​พฤติกรรม​ของ​ภาษา​ที่​บังเอิญ​วัด

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

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

สิบ​ห้า​งาน​อ่าน parse ผ่าน​ทั้งหมด และ​สอง​ภาษา​ตอบ​ตรง​กัน​ทุก​งาน ยกเว้น​ค่าที่​แสดง​ใน​งาน​ที่ 11

ทุก​แถว​ใน​ตาราง​นี้​คือ byte ชุด​หนึ่ง​ที่ Go เขียน​ไว้ ส่ง​ให้​ผู้​อ่าน​ที่​ถือ schema ทรง​หนึ่ง แล้ว​วัด​สาม​อย่าง​คือ​ค่าที่​ได้ จำนวน unknown fieldunknown fieldfield ที่​ผู้​อ่าน​ไม่รู้​จัก เพราะ schema ของ​ตัว​เอง​ไม่มี​หมายเลข​นั้น · ผู้​อ่าน​เก็บ byte ดิบ​ไว้​เฉยๆ แล้ว​ส่ง​คืน​ตอน serialize กลับ และ byte ที่​ผู้​อ่าน​คน​นั้น serialize กลับ​ออก​มา

#ผู้​อ่าน​ถืออ่าน byte ของGo ตอบJava ตอบunknownserialize กลับ
1v1v1customer=wira, quantity=7customer=wira, quantity=70เท่า​เดิม
2customer_namev1customer_name=wira, quantity=7customer_name=wira, quantity=70เท่า​เดิม
3v1v2customer=wira, quantity=7customer=wira, quantity=71เท่า​เดิม
4v2v1customer=wira, quantity=7customer=wira, quantity=70เท่า​เดิม
5v1v1 ที่​จำนวน​เป็น​ศูนย์customer=wiracustomer=wira0เท่า​เดิม
6optionalv1 ที่​จำนวน​เป็น​ศูนย์customer=wira · presence falsecustomer=wira · presence false0เท่า​เดิม
7optionalของ​ตัว​เอง​ที่​ตั้ง​ค่า​แล้วcustomer=wira, quantity=0 · presence truecustomer=wira, quantity=0 · presence true0เท่า​เดิม
8string size_name = 2v1customer=wiracustomer=wira1เท่า​เดิม
9int32 seats = 2v1customer=wira, seats=7customer=wira, seats=70เท่า​เดิม
10int64v1customer=wira, quantity=7customer=wira, quantity=70เท่า​เดิม
11uint32v1 ที่​จำนวน​ติดลบcustomer=wira, quantity=4294967295customer=wira, quantity=-10ไม่​เท่า
12sint32v1customer=wira, quantity=-4customer=wira, quantity=-40เท่า​เดิม
13v1sint32 ที่​จำนวน​เป็น​เจ็ดcustomer=wira, quantity=14customer=wira, quantity=140เท่า​เดิม
14repeated string customerv1customer=wira, quantity=7customer=wira, quantity=70เท่า​เดิม
15v1repeated ที่​มี​สอง​ชื่อcustomer=somchai, quantity=7customer=somchai, quantity=70ไม่​เท่า

สอง​งาน​ที่​มี unknown field คือ​งาน​ที่ 3 กับ​งาน​ที่ 8 และ​ทั้ง​สอง​ภาษา​ถือ byte ชุด​เดียวกัน​ไว้ ใน​นั้น คือ 1a086272616e63682d62 กับ 1007 ตาม​ลำดับ

สอง​งาน​ที่ serialize กลับ​แล้ว​ไม่​เท่า​เดิม​คือ​งาน​ที่ 11 กับ​งาน​ที่ 15 และ​ตรง​นี้​ก็​เหมือน​กัน​อีก Go กับ Java คืน 0a047769726110ffffffff0f และ 0a07736f6d636861691007 ออก​มา​ตัว​ต่อ​ตัว

ช่อง​ที่​คน​คาด​ว่า​จะ​แตก​ต่าง​มาก​ที่สุด​คือ​ช่อง​ที่​ตรง​กัน​หมด ไม่​ว่า​จะ​เป็น​จำนวน unknown field ที่​ผู้​อ่าน​เก็บ​ไว้ หรือ byte ที่​มัน​คายก​ลับ​ออก​มา ทั้ง​สอง​อย่าง​นี้​ไม่​ขึ้น​กับ​ภาษา

งาน​ที่ 11 คือ uint32 อ่าน byte ของ int32 ที่​เป็น −1 Go ตอบ 4294967295 Java ตอบ -1

ฝั่ง​เขียน​ถือ int32 quantity = 2; แล้ว​ใส่​ค่า −1 ผล​ที่​ออก​มา​คือ 17 byte

0a047769726110ffffffffffffffffff01

ฝั่ง​อ่าน​ถือ schema ที่​เปลี่ยน​ช่อง​เดียวกัน​เป็น uint32 quantity = 2; ทั้ง​สอง​ภาษา parse ผ่าน ทั้ง​คู่​ได้ unknown field เป็น​ศูนย์ และ​ทั้ง​คู่ serialize กลับ​ได้ 0a047769726110ffffffff0f เหมือน​กัน

ค่าที่​แสดง​เท่านั้น​ที่​ไม่​ตรง​กัน

runtimeค่า​ของ quantity ที่​ได้
Go4294967295
Java-1

Java ไม่มี​ชนิด​จำนวนเต็ม 32 bit ที่​ไม่มี​เครื่องหมาย protobuf จึง map uint32 ลง​เป็น int ของ Java แล้ว​เก็บ bit บน​สุด​ไว้​ใน​ตำแหน่ง​บิต​เครื่องหมาย กติกา​ข้อ​นี้​อยู่​ใน​ตาราง​ชนิด​ข้อมูล ของ protobuf.dev

bit ทั้ง 32 ตัว​จึง​เหมือน​กัน​ทุก​ตัว​ใน​สอง​ภาษา ต่าง​กัน​ที่​วิธี​พิมพ์​ออก​มา​เป็น​ตัว​หนังสือ Go พิมพ์ uint32 แบบ​ไม่มี​เครื่องหมาย Java พิมพ์ int แบบ​มี​เครื่องหมาย

ข้อ​นี้​ไม่ใช่​บั๊ก และ​ไม่ใช่​ความ​ต่าง​บน​สาย

byte ที่​วิ่ง​เข้า​มา​ชุด​เดียวกัน byte ที่​คายก​ลับ​ออก​ไป​ชุด​เดียวกัน จำนวน unknown field เท่า​กัน สิ่ง​ที่​ต่าง​คือ​ชนิด​ที่​ภาษา​ปลาย​ทาง​มี​ให้​ใช้ ถ้า Java มี​ชนิด 32 bit ที่​ไม่มี เครื่องหมาย ตาราง​ข้าง​บน​จะ​ไม่มี​แถว​ไหน​ต่าง​กัน​เลย

ราย​ละเอียด​ว่า​ทำไม −1 ถึง​กิน 10 byte และ​ทำไม serialize กลับ​แล้ว​มัน​หด อยู่​ที่ บท​ที่ 5

byte คือ​สัญญา ส่วน​ค่าที่​คุณ​เห็น​คือ​การ​แสดง​ผล​ของ​ภาษา​นั้น

หัวข้อ​ที่​มีชื่อ​ว่า “byte คือ​สัญญา ส่วน​ค่าที่​คุณ​เห็น​คือ​การ​แสดง​ผล​ของ​ภาษา​นั้น”

เส้น​แบ่ง​ของ​คอร์ส​นี้​อยู่​ตรง​นี้ ข้อ​อ้าง​เรื่อง​สัญญา​ต้อง​พูด​ถึง byte ไม่ใช่​พูด​ถึง​ค่าที่​ภาษา​หนึ่ง​พิมพ์

งาน​ที่ 11 แยก​ของ​สอง​อย่าง​ที่​คน​มัก​รวม​เป็น​อย่าง​เดียว​ออก​จาก​กัน อย่าง​แรก​คือ byte ที่​เดิน​ทาง อย่าง​ที่​สอง​คือ​ค่าที่​ภาษา​ปลาย​ทาง​แสดง​ให้​คุณ​ดู

wire compatibilitywire compatibilityการ​แก้ schema ที่​ทั้ง​สอง​ฝั่ง​ยัง​อ่าน byte ของ​กัน​ได้​ตาม​ที่​ตั้งใจ · parse ผ่าน​ไม่ใช่​หลัก​ฐาน​ของ​ข้อ​นี้ เพราะ​ค่าที่​ไป​นั่ง​ผิด​ช่อง​ก็ parse ผ่าน​เหมือน​กัน เป็น​ข้อ​อ้าง​เกี่ยว​กับ​อย่าง​แรก ส่วน​ตัวเลข​ที่​คุณ​เห็น​ใน log หรือ​ใน เทสต์ เป็น​อย่าง​ที่​สอง สอง​อย่าง​นี้​ตรง​กัน​เกือบ​ตลอด และ​งาน​ที่ 11 คือ​จุด​ที่​มัน​แยก​ออก​จาก​กัน

ผล​ที่​ตาม​มา​มี​สอง​ข้อ เอา​ไป​ใช้ได้​ทันที

  • เทสต์​ที่​เทียบ​ตัวเลข​ที่​พิมพ์​ออก​มา วัด​การ​แสดง​ผล​ของ​ภาษา​ตัว​เอง ไม่​ได้​วัด​สัญญา
  • เทสต์​ที่​เทียบ hex ของ byte ที่ serialize ออก​มา วัด​สัญญา​ตรงๆ และ​ให้​คำ​ตอบ​เดียวกัน​ทั้ง​สอง​ภาษา

protoc อ่าน .proto แล้ว​เขียน code ให้​ภาษา​ปลาย​ทาง ส่วน​ของ​ที่​ถือ​ชื่อ field ไว้ อยู่​ข้าง code ไม่​ได้​อยู่​บน​สาย

code generationcode generationขั้น​ที่ protoc แปล .proto เป็น code ของ​ภาษา​ปลาย​ทาง · Go กับ Java ได้ generator คนละ​ตัว จึง​เป็น​จุด​ที่​สอง​ภาษา​ตอบ​ไม่​เท่า​กัน​ได้ คือ​ขั้น​ที่ protoc อ่าน order.proto แล้ว​เขียน code ของ​ภาษา​ปลาย​ทาง ออก​มา ฝั่ง Go ต้อง​ลง protoc-gen-go เป็น plugin แยก​ต่าง​หาก​ก่อน ส่วน​ฝั่ง Java ใช้ generator ที่​ติด​มา​กับ protoc เอง ส่วน protobuf-java คือ runtime ที่ code ฝั่ง​นั้น​เรียก​ใช้

descriptordescriptorของ​ที่​ถือ​ชื่อ หมายเลข และ​ชนิด​ของ​ทุก field ไว้​ครบ · มัน​อยู่​ข้าง code ที่ generate ออก​มา และ​ไม่​ได้​เดิน​ทาง​ไป​กับ message คือ​ของ​ที่​ถือ​ชื่อ หมายเลข และ​ชนิด​ของ​ทุก field ไว้​ครบ มัน​ถูก​วาง​ไว้​ข้าง code ที่ generate ออก​มา​ใน​แต่ละ​ภาษา ผู้​อ่าน​ใช้​มัน​เพื่อ​แปล​หมายเลข​บน​สาย​กลับ​เป็น​ชื่อ​ที่​คน​อ่าน​รู้​เรื่อง

descriptor ไม่​ได้​เดิน​ทาง​ไป​กับ message หน้า Encoding ของ protobuf.dev อธิบาย​ว่า ฝั่ง​ถอดรหัส​รู้​ชื่อ​และ​ชนิด​ของ field ได้​จาก​นิยาม​ใน .proto ของ​ตัว​เอง ไม่ใช่​จาก byte ที่​รับ​มา มัน​เดิน​ทาง​ได้​ก็​ต่อ​เมื่อ​มี​คน​ตั้งใจ​ส่ง​มัน​เป็น message อีก​ใบ​ต่าง​หาก ซึ่ง​คอร์ส​นี้​ไม่​แตะ

flowchart LR
  P["order.proto ใบเดียว"] --> C["protoc"]
  C --> G["code กับ descriptor ฝั่ง Go"]
  C --> J["code กับ descriptor ฝั่ง Java"]
  G --> W["byte บนสาย"]
  J --> W
  W --> RG["ค่าที่ Go แสดง"]
  W --> RJ["ค่าที่ Java แสดง"]

คำ​บรรยาย​ภาพ: schema ใบ​เดียว​ผ่าน protoc ออก​มา​เป็น​ของ​สอง​ชุด สอง​ชุด​นั้น​เขียน​และ​อ่าน byte ชุด​เดียวกัน ส่วน​ค่าที่​คน​เห็น เป็น​สิ่ง​ที่​แต่ละ​ภาษา​แสดง​จาก byte ชุด​นั้น​อีก​ที​หนึ่ง

นี่​คือ​เหตุผล​ที่​บท​ที่ 1 วัด​ได้​ว่า​เปลี่ยน​ชื่อ field แล้ว byte ไม่​ขยับ ชื่อ​อยู่​ใน descriptor ของ​แต่ละ​ฝั่ง ไม่​ได้​อยู่​ใน​ของ​ที่​ส่ง

กฎ​ที่​เอา​ไป​ใช้ได้ วัด​ฝั่ง​ที่​คุณ​ไม่​ได้​เขียน

หัวข้อ​ที่​มีชื่อ​ว่า “กฎ​ที่​เอา​ไป​ใช้ได้ วัด​ฝั่ง​ที่​คุณ​ไม่​ได้​เขียน”

ข้อ​อ้าง​ที่​วัด​จาก runtime เดียว เป็น​ข้อ​อ้าง​เรื่อง runtime นั้น ไม่ใช่​ข้อ​อ้าง​เรื่อง format

เทสต์​ที่​เขียน​ง่าย​ที่สุด​คือ​เทสต์​แบบ​นี้ เขียน​ออก​มา​แล้ว​อ่าน​กลับ​เข้าไป​ใน​ภาษา​เดียวกัน มัน​ผ่าน​เสมอ และ​มัน​ไม่​ได้​แตะ​ฝั่ง​ที่​จะ​พัง

❌ version ดิบ — เทสต์​ที่​วัด​เฉพาะ​ฝั่ง​ที่​ตัว​เอง​เขียน
b, _ := proto.Marshal(&orderv1.Order{Customer: "wira", Quantity: 7})
var got orderv1.Order
_ = proto.Unmarshal(b, &got)
// ผ่านทุกครั้ง · แต่ฝั่งที่ deploy จริงคือ Java และเทสต์นี้ไม่เคยเรียกมันเลย

ทาง​แก้​ไม่ใช่​การ​เขียน​เทสต์​ให้​เยอะ​ขึ้น​ใน​ภาษา​เดิม แต่​คือ​การ​ย้าย​เส้น​ที่​วัด ให้​ฝั่ง​เขียน​กับ ฝั่ง​อ่าน​เป็น​คนละ runtime กัน เหมือน​ที่​ของ​จริง​เป็น

ทำได้​สาม​ท่า เรียง​จาก​ถูก​ที่สุด​ขึ้น​ไป

  1. ตรึง hex ของ byte ที่​ผู้​เขียน​สร้าง ไว้​เป็น​ค่า​คาด​หวัง​ใน​เทสต์ แล้ว​ให้​ฝั่ง​อ่าน​ยืนยัน​กับ hex ชุด​นั้น
  2. ให้​ฝั่ง​อ่าน​จริง​อ่าน byte ชุด​นั้น แล้ว​ยืนยัน​ทั้ง​ค่าที่​ได้ จำนวน unknown field และ hex ที่ serialize กลับ
  3. ทำ​ข้อ​สอง​กับ​ทุก​ทรง​ของ schema ที่​คุณ​ตั้งใจ​จะ deploy ไม่ใช่​เฉพาะ​ทรง​ที่​แก้​ล่าสุด

ตาราง​ข้าง​บน​เป็น​หลัก​ฐาน​เรื่อง Go กับ Java ที่ version ซึ่ง​ตรึง​ไว้ คือ protoc 36.1 protoc-gen-go v1.36.12 และ protobuf-java 4.36.1 เท่านั้น

มัน​ไม่​ได้​พิสูจน์​ว่า​ทุก​ภาษา​ตอบ​เหมือน​กัน ภาษา​ที่​ไม่มี​ชนิด​จำนวนเต็ม​แบบ​ไม่มี​เครื่องหมาย มี​ข้อ​จำกัด​ข้อ​เดียว​กับ Java ส่วน​มัน​จะ​พิมพ์​ค่า​ออก​มา​เป็น​อะไร คอร์ส​นี้​ไม่​ได้​วัด และ​ภาษา​ที่​ตัวเลข​ทุก​ตัว​เป็น​ทศนิยม 64 bit มี​คำถาม​ของ​ตัว​เอง​อีก​ชุด ซึ่ง​คอร์ส​นี้​ไม่​ได้​วัด​เหมือน​กัน

ท่า​ที่​ปลอดภัย​คือ​เขียน​ว่า​อะไร​ถูก​วัด แล้ว​หยุด​ตรง​นั้น ประโยค​ที่​บอก​ว่า​ทุก runtime ทำ​แบบ​นี้ ต้อง​มี​บรรทัด​ใน protobuf.dev รองรับ ไม่ใช่​มี​แค่​ผล​จาก​สอง​ภาษา​รองรับ

หกบท​แรก​กับ​บท​นี้​มี​จุด​ร่วม​อยู่​อย่าง​หนึ่ง กับ​ดัก​ทุก​ข้อ​เงียบ​สนิท ไม่มี error ไม่มี parse ที่​พัง และ​ไม่มี​เทสต์​ฝั่ง​ไหน​แดง บท​ที่ 8 ถาม​คำถาม​สุดท้าย​ว่า มี​อะไร​ที่​เครื่อง​มือ​จับ​ได้​ก่อน​ของ​จะ​ขึ้น​บ้าง


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • Protocol Buffers — Encoding (เข้า​ถึง 2026-09-04) — ที่มา​ของ​กฎ​ว่า​ฝั่ง​ถอดรหัส​รู้​ชื่อ​และ​ชนิด​ของ field จาก​นิยาม​ของ​ตัว​เอง ไม่ใช่​จาก byte ที่​รับ​มา

  • Protocol Buffers — Language Guide (proto 3) · Scalar Value Types (เข้า​ถึง 2026-09-04) — ที่มา​ของ​การ map uint32 ลง​เป็น int ของ Java และ​ของ​ข้อ​สังเกต​ว่า Java เก็บ​จำนวนเต็ม​แบบ​ไม่มี​เครื่องหมาย​ไว้​ใน​ชนิด​ที่​มี​เครื่องหมาย

  • Protocol Buffer Basics: Go (เข้า​ถึง 2026-09-04) — ที่มา​ของ​ขั้น​ตอน​ที่ protoc เรียก protoc-gen-go เขียน code ฝั่ง Go

  • Protocol Buffer Basics: Java (เข้า​ถึง 2026-09-04) — ที่มา​ของ​ขั้น​ตอน​เดียวกัน​ฝั่ง Java ซึ่ง​สั่ง protoc ตัว​เดียว​จบ ไม่​ต้อง​ลง plugin แยก​เหมือน​ฝั่ง Go

  • ตัวเลข​ทุก​ตัว​ใน​บท​นี้​มา​จาก 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

เช็กความเข้าใจ — บทที่ 7

ข้อ 1 / 3

จากงานอ่านทั้ง 15 งาน Go กับ Java ตอบต่างกันกี่ช่อง