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

JSON บรรทัด​เดียว กับ​ปลาย​ทาง​สี่​แห่ง

gateway ของ​ระบบ​ออ​เด​อร์​รับ body ใบ​หนึ่ง​เข้า​มา​ทาง HTTP ตรวจ​สิทธิ์ แล้ว​ส่ง​ต่อ​ให้ service ข้าง​หลัง มัน​ไม่​ได้​มีหน้า​ที่​แก้​อะไร​ใน body นั้น​เลย​สัก​หลัก

body ใบ​นั้น​มี​สอง​ช่อง​ที่​บท​นี้​ตาม​ไป​ดู คือ orderId กับ total ซึ่ง​เป็น​ยอด​ที่​เก็บ​เป็น​จำนวนเต็ม ของ​หน่วย​ย่อย ทั้ง​สอง​ช่อง​เป็น​ตัวเลข​ล้วน ไม่​ได้​ครอบ​ด้วย​เครื่องหมาย​คำ​พูด

body ที่​ทุก​ปลาย​ทางใน​บท​นี้​รับ​ไป​เหมือน​กัน​หมด
{"orderId": 9007199254740993, "total": 12345678901234567890}

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

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

orderctl คือ​เครื่อง​มือ​ประมวล​ผล​ออ​เด​อร์​ที่​คอร์ส​ภาษา​ใน​เสา​นี้​สร้าง​ซ้ำ​ที​ละ​ภาษา repo ตัวอย่าง กำลัง​จัด​ทำ ระหว่าง​นี้​ทุก​ตัวเลข​ใน​บท​นี้​รัน​เอง​ได้​ด้วย npm run verify:integer-semantics ซึ่ง​เรียก python3 scripts/integer-semantics/probe.py · ไม่​ต้อง​มี Docker และ​ไม่​ต้อง​ต่อ​เน็ต

แปด​แถว​นี้​มา​จาก body บรรทัด​เดียวกัน และ​ความ​ต่าง​ทั้งหมด​เกิด​หลัง​จาก​ที่ body เข้าไป​ใน​โปรแกรม​แล้ว

สเปก​ของ JSON ไม่​ได้​กำหนด​ความ​กว้าง​ให้​ตัวเลข​ไว้​เลย JSON numberJSON numberชนิด​ตัวเลข​ของ JSON ที่​สเปก​ไม่​ได้​กำหนด​ความ​กว้าง​ไว้ · ค่าที่​ได้​จริง​จึง​ถูก​ตัดสิน​โดย​ชนิด​ปลาย​ทาง​ของ​ฝั่ง​ที่​อ่าน ไม่ใช่​โดย​ตัว​ข้อความ​ที่​ส่ง​มา จึง​เป็น​เพียง ลำดับ​ของ​หลัก ส่วน​ค่าที่​ได้​จริง​มา​จาก​ชนิด​ที่​ฝั่ง​อ่าน​เตรียม​ไว้​รับ​มัน

ปลาย​ทางorderIdtotal
Node JSON.parse900719925474099212345678901234567000
Node เขียน​กลับ​ด้วย JSON.stringify{"orderId":9007199254740992,"total":12345678901234567000}—
Python json.loads9007199254740993 ชนิด int12345678901234567890
Python json.dumps{"orderId": 9007199254740993, "total": 12345678901234567890}—
Go map[string]any9.007199254740992e+15 ชนิด float641.2345678901234567e+19
Go map[string]any เขียน​กลับ​ด้วย Marshal{"orderId":9007199254740992,"total":12345678901234567000}—
Go struct{ OrderId, Total int64 }90071992547409930 และ​มี error
Go Decoder ที่​ตั้ง UseNumber()900719925474099312345678901234567890

สาม​แถว​ที่​ช่อง​ขวา​เป็น — คือ​แถว​ของ​การ​เขียน​กลับ ค่าที่​วัด​ได้​ของ​แถว​พวก​นั้น​เป็น body ทั้ง​ใบ จึง​อยู่​ใน​ช่อง​เดียว ไม่​ได้​แยก​เป็น​สอง​ช่อง​เหมือน​แถว​ที่​เหลือ

ห้า​แถว​ที่​เหลือ​เป็น​แถว​ของ​การ​อ่าน และ​ค่า​ของ​ทั้ง​ห้า​แถว​ตกลง​ใน​ชนิด​ปลาย​ทาง​สี่​ชนิด คือ float64 ของ map กับ number ของ Node ซึ่ง​เป็น double 64 bit ตัว​เดียวกัน · int64 ของ struct · json.Number ของ Decoder และ int ของ Python · ชื่อ​บท​นับ​สี่​ชนิด​นี้ ไม่​ได้​นับ​จำนวน​แถว

แถว​ที่ 2 กับ​แถว​ที่ 6 น่า​สนใจ​ที่สุด​เมื่อ​วาง​คู่​กัน probe เทียบ​สอง​สตริง​นั้น​ตรงๆ แล้ว​ได้​ว่า Node กับ map ของ Go เขียน body ใหม่​ออก​มา​เหมือน​กัน​ทุก​ตัว​อักษร ทั้ง​ที่​เป็น​คนละ​ภาษา

ภาษา​เดียว ตัว decoder ตัว​เดียว body ใบ​เดียว ต่าง​กัน​ที่​ชนิด​ที่​รับ​ค่า​ไว้​เท่านั้น

สาม​ทรง​ข้าง​ล่าง​นี้​อยู่​ใน​โปรแกรม​เดียวกัน รัน​ต่อ​กัน​ใน​การ​เรียก​ครั้ง​เดียว และ​รับ body ตัวแปร เดียวกัน​ทั้ง​สาม​ทรง

scripts/integer-semantics/runners/main.go
type Order struct {
OrderId int64 `json:"orderId"`
Total int64 `json:"total"`
}

ผล​ที่​ออก​มา​ต่าง​กัน​สาม​แบบ ส่วน​ค่า​ของ orderId ที่​ได้​จาก​สาม​ทรง​นี้​ต่าง​กัน​สอง​แบบ เพราะ​สอง​ทรง​หลัง ให้​ค่า​เดียวกัน​ตรง​ทุก​หลัก

เอกสาร​ของ encoding/json เขียน​กฎ​นี้​ไว้​ตรงๆ คือ​เมื่อ​ปลาย​ทาง​เป็น interface value ตัว Unmarshal จะ​เก็บ float64 ลง​ไป​ให้​ทุก​ตัวเลข​ของ JSON · json.Unmarshal ไม่มี​สวิตช์​ให้ เปลี่ยน​ข้อ​นี้ สวิตช์​อยู่​บน Decoder ซึ่ง​เป็น​ทรง​ที่​สาม

scripts/integer-semantics/runners/main.go
var m map[string]any
out["mapErr"] = errText(json.Unmarshal(body, &m))
out["mapOrderId"] = fmt.Sprintf("%v", m["orderId"])
out["mapTotal"] = fmt.Sprintf("%v", m["total"])
out["mapOrderIdType"] = fmt.Sprintf("%T", m["orderId"])
re, _ := json.Marshal(m)
out["mapRemarshal"] = string(re)

%T ตอบ​ว่า float64 และ %v ตอบ​ว่า 9.007199254740992e+15 ซึ่ง​เป็น​ค่าที่​เสีย​หลัก​ไป​แล้ว ตั้งแต่​ตอน​ที่ Unmarshal คืน​ค่า ส่วน err ที่ Unmarshal คืน​มา​เป็น​สตริง​ว่าง

total เป็น​รูป​เดียวกัน %v ตอบ​ว่า 1.2345678901234567e+19 ซึ่ง​ไม่ใช่​ยอด​ที่​ต้นทาง​ส่ง​มา และ​ไม่มี​อะไร​ใน​โปรแกรม​บอก​ว่า​มัน​เคย​เป็น​ค่า​อื่น

scripts/integer-semantics/runners/main.go
var o Order
out["structErr"] = errText(json.Unmarshal(body, &o))
out["structOrderId"] = fmt.Sprintf("%d", o.OrderId)
out["structTotal"] = fmt.Sprintf("%d", o.Total)

OrderId ได้ 9007199254740993 ตรง​ทุก​หลัก เพราะ int64 กว้าง​พอ​สำหรับ​ค่า​นี้ ส่วน Total ได้ 0 เพราะ 12345678901234567890 เกิน​ช่วง​ของ int64 และ err ไม่ใช่​ค่า​ว่าง

ข้อความ error ที่ probe บันทึก​ไว้​ทั้ง​บรรทัด
json: cannot unmarshal number 12345678901234567890 into Go struct field Order.total of type int64

นี่​คือ partial unmarshalpartial unmarshalการ​ที่​ตัว​ถอด​ค่า​เติม struct ได้​บาง​ช่อง แล้ว​คืน error ออก​มา​พร้อม​กัน · ผู้​เรียก​ที่​บันทึก error แล้วไป​ต่อ จึง​ถือ struct ที่​เต็ม​ครึ่ง​เดียว​อยู่​ใน​มือ​โดย​ไม่รู้​ตัว · เมื่อ Unmarshal คืน​ค่า​มา err ไม่​เป็น nil แต่ OrderId ถูก​เติม​ไป​เรียบร้อย​แล้ว struct ที่​อยู่​ใน​มือ​จึง​เต็ม​ครึ่ง​เดียว

เอกสาร​ของ encoding/json บอก​ไว้​ว่า​เมื่อ​ตัวเลข​ของ JSON ล้น​ชนิด​ปลาย​ทาง Unmarshal จะ​ข้าม​ช่อง นั้น​แล้ว​ถอด​ค่า​ต่อ​ให้​ได้​มาก​ที่สุด​เท่า​ที่​ทำได้

ประโยค​ถัด​ไป​มี​เงื่อนไข​นำ​หน้า​อยู่ คือ If no more serious errors are encountered แล้ว​จึง​บอก​ว่า มัน​คืน UnmarshalTypeError ของ​ความ​ผิด​พลาด​ที่​เกิด​ก่อน​ที่สุด

UnmarshalTypeError จึง​ไม่ใช่​สิ่ง​ที่​รับ​ประกัน​ว่า​จะ​ได้​คืน​มา​ทุก​ครั้ง มัน​เป็น​สิ่ง​ที่​ได้​คืน​มา​เมื่อ ไม่มี​ความ​ผิด​พลาด​ที่​หนัก​กว่า​นั้น​เกิด​ขึ้น

มัน​ยัง​เขียน​ต่อ​ด้วย​ว่า​ไม่มี​การ​รับ​ประกัน​ว่า​ช่อง​ที่​เหลือ​ซึ่ง​ตาม​หลัง​ช่อง​ที่​มี​ปัญหา​จะ​ถูก​ถอด​ค่า​ลง ปลาย​ทาง​ครบ ประโยค​นั้น​ถอน​การ​รับ​ประกัน​ออก มัน​ไม่​ได้​บอก​ว่า​ช่อง​ที่​ตาม​หลัง​จะ​ว่าง

สถานะ​ของ struct ที่​เหลือ​จึง​เป็น​สิ่ง​ที่​ต้อง​วัด ไม่ใช่​สิ่ง​ที่​อ่าน​ออก​มา​จาก​เอกสาร​ได้ · probe ของ คอร์ส​นี้​วัด​ไว้​ทรง​เดียว คือ body ใบ​นี้​ที่ orderId มา​ก่อน total

ผู้​เรียก​ที่​เขียน if err != nil { slog.Warn("unmarshal", "err", err) } แล้ว​เดิน​ต่อ จึง​ถือ​ออ​เด​อร์​ที่​มี id ถูก​ต้อง คู่​กับ​ยอด​เป็น 0 ซึ่ง​เป็น​คู่​ที่​หน้าตา​สม​เหตุ​สม​ผลก​ว่าการ​ไม่มี​ข้อมูล​เลย

scripts/integer-semantics/runners/main.go
var n map[string]any
dec := json.NewDecoder(bytes.NewReader(body))
dec.UseNumber()
out["numberErr"] = errText(dec.Decode(&n))
out["numberOrderId"] = fmt.Sprintf("%v", n["orderId"])
out["numberTotal"] = fmt.Sprintf("%v", n["total"])
out["numberOrderIdType"] = fmt.Sprintf("%T", n["orderId"])

%T ตอบ​ว่า json.Number และ​ทั้ง​สอง​ช่อง​ออก​มา​ตรง​ทุก​หลัก คือ 9007199254740993 กับ 12345678901234567890 ส่วน err ที่ Decode คืน​มา​เป็น​สตริง​ว่าง

เอกสาร​บอก​ว่า UseNumber ทำให้ Decoder ถอด​ตัวเลข​ลง interface value เป็น Number แทนที่​จะ เป็น float64 ปลาย​ทาง​จึง​เปลี่ยน​ไป​ทั้ง​ที่ body ไม่​ได้​เปลี่ยน

`json.Number` ยัง​ไม่ใช่​จำนวนเต็ม​ของ​ภาษา

json.Number คือ string ที่​เก็บ​หลัก​ไว้​ครบ ไม่ใช่​ชนิด​จำนวนเต็ม การ​แปลง​มัน​ลง int64 เป็น​ขั้น ที่​คุณ​ต้อง​เรียก​เอง และ probe ของ​คอร์ส​นี้​ไม่​ได้​วัด​ขั้น​นั้น

สิ่ง​ที่ UseNumber ให้​คือ​การ​เลื่อน​พรมแดน​ออก​ไป จาก​จุด​ที่ decoder ตัดสิน​ใจ​แทนคุณ มา​อยู่​ที่​จุด​ที่ คุณ​เขียน​การ​แปลง​เอง ราคา​ของ​มัน​คือ​ทุก​ที่​ที่​เคย​อ่าน​ค่า​ออก​มา​ใช้ได้​เลย ต้อง​เติม​ขั้น​แปลง​เข้าไป

การ​เขียน​กลับ​ที่​เงียบ และ​ยัง​เป็น JSON ที่​ถูก​ต้อง

หัวข้อ​ที่​มีชื่อ​ว่า “การ​เขียน​กลับ​ที่​เงียบ และ​ยัง​เป็น JSON ที่​ถูก​ต้อง”

ความ​เสีย​หาย​ไม่​ได้​อยู่​ที่​การ​อ่าน แต่​อยู่​ที่​การ​เขียน​ค่าที่​อ่าน​มา​แล้ว​ส่ง​ต่อ​ไป​ข้าง​หน้า

gateway ที่​อ่าน body ลง map[string]any แล้ว Marshal กลับ​ออก​ไป คือ​รูป​ที่​ไม่​ต้อง​ประกาศ struct ให้​ทุก body ที่​ผ่าน​มือ และ​เป็น​ทรง​ที่​หนึ่ง​ของ​สาม​ทรง​ที่​หัวข้อ​ข้าง​บน​วัด​ไว้

body ที่​ออก​จาก gateway
{"orderId":9007199254740992,"total":12345678901234567000}

สิ่ง​ที่​ออก​มา​เป็น JSON ที่​ถูก​ต้อง​ทุก​ประการ ทั้ง​สอง​ช่อง​ยัง​เป็น​ตัวเลข ยัง​มีชื่อ​คีย์​เดิม และ​ยัง​ส่ง เข้า service ข้าง​หลัง​ได้​โดย​ไม่มี​ใคร​ปฏิเสธ​มัน

แต่ orderId ที่​ออก​มา​น้อย​กว่า​ที่​รับ​เข้า​มา​อยู่​หนึ่ง และ total ที่​ออก​มา​ลงท้าย​ด้วย​ศูนย์​สาม​ตัว แทน​หลัก​สาม​หลัก​สุดท้าย​ที่​รับ​เข้า​มา ทั้ง​สอง​ค่า​เปลี่ยน​โดยที่ err ของ Unmarshal เป็น​สตริง​ว่าง

err ตัว​นั้น​คือ error ตัว​เดียว​ที่ probe บันทึก​ไว้​ใน​เส้น​ทาง​นี้ เพราะ runner เขียน​ขั้น​เขียน​กลับ เป็น re, _ := json.Marshal(m) ซึ่ง​ทิ้ง error ของ​ขั้น​นั้น​ไป สิ่ง​ที่​วัด​ได้​ของ​ขั้น​นั้น​จึง​เป็น ไบต์​ที่​มัน​คืน​มา คือ body เต็ม​ใบ ไม่ใช่​ค่า​ว่าง​ที่ Marshal คืน​เมื่อ​มัน​ล้ม​เหลว

Number.isSafeInteger(9007199254740993) ของ Node ตอบ​ว่า false ซึ่ง​เป็น​คำ​ตอบ​ที่​ตรง​คำถาม​นี้ ที่สุด แต่​ไม่มี runtime ไหน​เรียก​มัน​ให้​เอง มัน​เป็น​ด่าน​ที่​ทำงาน​ก็​ต่อ​เมื่อ​มี​คน​เรียก

flowchart TD
  B["body เดียว · orderId 9007199254740993 · total 12345678901234567890"] --> G["Go"]
  B --> N["Node · JSON.parse"]
  B --> P["Python · json.loads"]
  G --> M["ปลายทาง map[string]any"]
  G --> S["ปลายทาง struct ของ int64"]
  G --> U["ปลายทาง Decoder ที่ตั้ง UseNumber"]
  M --> M2["float64 · เขียนกลับได้ orderId 9007199254740992 · Unmarshal ไม่คืน error"]
  S --> S2["orderId ตรง · total เป็น 0 · มี error พร้อม struct ที่เติมครึ่งใบ"]
  U --> U2["json.Number · ตรงทั้งสองช่อง · ไม่มี error"]
  N --> N2["number ที่เป็น double · เขียนกลับได้ orderId 9007199254740992"]
  P --> P2["int · เขียนกลับได้ body เดิมทุกหลัก"]

คำ​บรรยาย​ภาพ: กิ่ง​ของ Go แตก​เป็น​สาม​กิ่ง​ย่อย​ก่อน​ถึง​ผลลัพธ์ จุด​ที่​แตก​คือ​ชนิด​ปลาย​ทาง ไม่ใช่​ชื่อ ภาษา · สอง​ช่อง​ที่​เขียน​กลับ​ออก​มา​ได้ orderId เป็น 9007199254740992 คือ​สอง​ช่อง​ที่​ค่า​เปลี่ยน​ไป โดย​ไม่มี​ใคร​รู้ตัว

สอง​ภาษา​นี้​รับ body ใบ​เดียวกัน และ​ทั้ง​คู่​มี​ขั้น​เขียน​กลับ​เหมือน​กัน แต่​สิ่ง​ที่​เขียน​ออก​มา​ไม่​เท่า​กัน

JSON.parse ของ Node คืน 9007199254740992 ให้ orderId และ 12345678901234567000 ให้ total เพราะ​มัน​ถอด​ตัวเลข​ของ JSON ลง Number ซึ่ง​เป็น double ตัว​เดียว​กับ​ที่ map ของ Go ได้​มา

JSON.stringify ที่​ตาม​มา​จึง​เขียน​ค่าที่​เปลี่ยน​ไป​แล้ว​ออก​ไป ไม่​ได้​เขียน​ข้อความ​ที่​รับ​เข้า​มา ส่วน​ที่​ผล​ของ​มัน​ตรง​กับ map ของ Go ทุก​ตัว​อักษร ไม่ใช่​เรื่อง​บังเอิญ ทั้ง​สอง​ทาง​ถือ double ค่า​เดียวกัน แล้ว​เขียน​มัน​กลับ​ออก​มา​เป็น​หลัก​ล้วน​เหมือน​กัน จุด​ที่​ตรง​กัน​คือ​ชนิด​ที่​รับ​ค่า​ไว้ ไม่ใช่​ภาษา

json.loads ของ Python คืน 9007199254740993 ชนิด int และ​คืน 12345678901234567890 ตรง​ทุก​หลัก แล้ว json.dumps ก็​เขียน body เดิม​กลับ​ออก​มา​ไม่​เปลี่ยน​สัก​หลัก

เอกสาร​ของ Python เขียน​ตาราง​การ​แปลง​ไว้​ว่า JSON number ที่​เป็น​จำนวนเต็ม​ไป​เป็น int และ​ระบุ​ว่า ค่า​ตั้ง​ต้น​ของ parse_int เท่ากับ​การ​เรียก int(num_str) กับ​สตริง​ของ​ทุก​จำนวนเต็ม​ที่​ถอด​ค่า

int ของ Python ไม่มี​ความ​กว้าง ซึ่ง​เป็น​ข้อ​ที่​บท​ที่ 2 วัด​ไว้​แล้ว ช่อง​นี้​จึง​ตรง​เพราะ​สอง​ข้อ​ประกอบ​กัน คือ​ตัว​ถอด​ค่า​ส่งตัว​เลขจำนวน​เต็ม​ลง int ตาม​ตาราง​ข้าง​บน และ int ไม่มี​ขอบ​ให้​ค่า​ชน​กับ​มัน ทั้ง​สอง​ข้อ​เป็น​เรื่อง​ของ​ชนิด​ปลาย​ทาง ไม่ใช่​เรื่อง​ที่ parser ของ​มัน​ระวัง​กว่า

🔁 เส้น​แบ่ง​กับ​คอร์ส Go จาก​ศูนย์

API ของ encoding/json เป็น​ของ​บท​ที่ 3 ใน​คอร์ส Go จาก​ศูนย์ คือ struct tag การ​จับ​คู่​คีย์ Decoder เทียบ Unmarshal DisallowUnknownFields และ omitempty

ตาราง​ของ​บท​นั้น​มี​แถว​หนึ่ง​ที่​บอก​ว่า​ตั้ง UseNumber() แล้ว​ได้ json.Number แทน float64 เมื่อ​อ่าน​ลง any · แถว​นั้น​บอก​ว่า​สวิตช์​มี​อยู่ แต่​ไม่​ได้​วัด​ค่า​ไหน​เลย

บท​นี้​ไม่​ได้​ทัวร์ API ซ้ำ บท​นี้​เป็น​เจ้าของ​ค่าที่​หาย​ไป​ระหว่าง​ทาง คือ​ตัวเลข​ที่​เข้าไป​กับ​ตัวเลข​ที่ ออก​มา​ไม่​เท่า​กัน และ​เป็น​เจ้าของ​คำถาม​ว่า​ใคร​เป็น​คน​ตัดสิน​ค่า​นั้น

gateway ไม่​ได้​คำนวณ​อะไร​สัก​ครั้ง มัน​แค่​อ่าน body แล้ว​เขียน​กลับ และ id ที่​ออก​ไป​ไม่ใช่ id ที่​เข้า​มา

บท​ที่ 5 วัด​ค่าที่​เปลี่ยน​ตอน​โปรแกรม​เขียน​การ​แปลง​ลง​ไป​เอง ส่วน​ที่​นี่​ไม่มี​ใคร​เขียน​การ​แปลง​สัก​บรรทัด orderId ที่​ออก​จาก gateway เป็น 9007199254740992 ทั้ง​ที่​ทุก​บรรทัด​ใน gateway อ่าน​แล้ว​ส่ง​ต่อ

service ข้าง​หลัง​จึง​ได้ id ที่​ไม่​ตรง​กับ​ที่​ลูกค้า​กด​มา และ​คำ​สั่ง​ที่​ตาม​มา​ก็​ทำงาน​กับ id ใบ​นั้น ตาม​ปกติ ไม่มี​ขั้น​ไหน​ใน​เส้น​ทาง​นี้​คืน error ให้​ใคร​เห็น

ยอด​เป็น​รูป​เดียวกัน​แต่​ตาม​รอย​ยาก​กว่า เพราะ 12345678901234567000 ยัง​อ่าน​ดู​เหมือน​ยอด​ปกติ ไม่มี​หลัก​ไหน​หาย​ไป​ทั้ง​ก้อน มี​แต่​หลัก​ท้าย​ที่​กลาย​เป็น​ศูนย์

การ​ไล่​หา​สาเหตุ​จะ​เริ่ม​ที่ service ข้าง​หลัง​ก่อน​เสมอ เพราะ​ที่​นั่น​คือ​ที่​ที่​อาการ​โผล่ ส่วน​จุด​ที่​ค่า เปลี่ยน​อยู่​ที่ gateway ซึ่ง​เป็น​ที่​เดียว​ใน​เส้น​ทาง​ที่​ไม่มี​ใคร​สงสัย มัน​แค่​ส่ง​ต่อ

ราคา​ของ​การ​ไม่​ให้​ค่า​เดิน​ผ่าน​ชนิด​ที่​แคบ​กว่า

สาม​ทาง​ข้าง​ล่าง​แก้​ที่ gateway ได้​ทั้งหมด และ​แต่ละ​ทาง​ย้าย​งาน​ไป​วาง​ไว้​ที่​อื่น

  • ส่ง​ต่อ body เป็น​ไบต์​ดิบ ไม่​ต้อง​ถอด​ค่า​เลย​ถ้า​ไม่​ต้อง​อ่าน​ช่อง​ไหน ราคา​คือ gateway จะ​ตรวจ หรือ​แก้​อะไร​ใน body ไม่​ได้​อีก​เลย และ​กฎ​ที่​เคย​เขียน​ไว้​ตรง​นั้น​ต้อง​ย้าย​ไป​ที่​อื่น
  • ตั้ง UseNumber() แล้ว​อ่าน​เป็น json.Number ราคา​คือ​ทุก​จุด​ที่​เคย​ใช้​ค่า​ได้​ทันที​ต้อง เติม​ขั้น​แปลง​เอง และ​ต้อง​ตัดสิน​ใจ​เอง​ว่า​ค่าที่​แปลง​ไม่​ผ่าน​จะ​ไป​ทาง​ไหน
  • ประกาศ struct ให้​ทุก body ที่​ผ่าน​มือ ราคา​คือ​ต้อง​เขียน​ชนิด​ตาม​ให้​ครบ​ทุก​ช่อง​ที่​ต้นทาง เพิ่ม​เข้า​มา และ​ต้อง​อ่าน err ทุก​ครั้ง ไม่ใช่​บันทึก​ไว้​แล้ว​เดิน​ต่อ

ข้อ​สุดท้าย​มี​กับ​ดัก​ซ้อน​อยู่ การ​ประกาศ struct ไม่​ได้​ทำให้​ค่าที่​ล้น​กลาย​เป็น​ค่าที่​ถูก มัน​เพียง​ทำให้ มี​คน​บอก​คุณ · คน​ที่​บันทึก error แล้ว​เดิน​ต่อ​จะ​ถือ struct ที่​เต็ม​ครึ่ง​เดียว​เหมือน​เดิม

จุด​ที่​ห้า​ใน​ห้า​จุด · ครึ่ง​ที่​ชนิด​ปลาย​ทาง​ตัดสิน

หัวข้อ​ที่​มีชื่อ​ว่า “จุด​ที่​ห้า​ใน​ห้า​จุด · ครึ่ง​ที่​ชนิด​ปลาย​ทาง​ตัดสิน”

บท​นี้​เปิด​จุด​ที่​ห้า และ​เป็น​จุด​ที่​ค่า​เปลี่ยน​โดย​ไม่มี​การ​คำนวณ​และ​ไม่มี​การ​แปลง​ที่​คุณ​สั่ง​เอง

ธี​สิ​ส​ของ​คอร์ส​นี้​คือ ค่า​จำนวนเต็ม​ค่า​หนึ่ง​เปลี่ยน​ได้​ห้า​จุด​ระหว่าง​ทาง​ผ่าน​ระบบ และ​ทุก​จุด​ใน​ห้า​จุด​นั้น เงียบ สี่​จุด​แรก​คือ​ความ​กว้าง​ที่​หมด ค่าที่​อยู่​นอก​ชนิด​เดิม ทิศ​ของ​การ​ปัด และ​ทศนิยม​ที่​กลืน​จำนวนเต็ม

จุด​ที่​ห้า​คือ พรมแดน​ของ​การ parseparse boundaryจุด​ที่​ค่า​จาก​ภายนอก​กลาย​เป็น​ค่า​ใน​ชนิด​ของ​ภาษา · นโยบาย​เมื่อ parse ไม่​สำเร็จ​มี​สี่​แบบ คือ​โยน คืน​ค่า​พร้อม error ตัด​ท้าย​ทิ้ง​เงียบๆ และ​ตรง​เป๊ะ ซึ่ง​เป็น​จุด​ที่​ค่า​จาก​ภายนอก​กลาย​เป็น​ค่า​ใน ชนิด​ของ​ภาษา สาม​จุด​แรก​ต้อง​มี​การ​คำนวณ​ที่​เขียน​อยู่​ใน​บรรทัด ส่วน​จุด​ที่​สี่​ยัง​มี​การ​แปลง​หรือ​ชื่อ​ของ ชนิด​ทศนิยม​ให้​เห็น จุด​นี้​ไม่มี​ทั้ง​สอง​อย่าง สิ่ง​เดียว​ที่​คุณ​เขียน​คือ​ชนิด​ของ​ตัวแปร​ที่​รับ​ค่า

สิ่ง​ที่​ตัดสิน​ค่าที่​ออก​มา​ไม่ใช่​ตัว​ข้อความ​ที่​ส่ง​มา เพราะ​ข้อความ​เดียวกัน​ให้​คำ​ตอบ​คนละ​แบบ และ​ไม่ใช่ ภาษา เพราะ​โปรแกรม Go ใบ​เดียว​ให้​ผล​สาม​แบบ สิ่ง​ที่​ตัดสิน​คือ​ชนิด​ที่​ฝั่ง​อ่าน​เตรียม​ไว้​รับ​มัน

บท​ที่ 7 อยู่​ที่​จุด​ที่​ห้า​เหมือน​กัน แต่​เปลี่ยน​รูป​ของ​อินพุต จาก​ตัวเลข​ที่​อยู่​ใน JSON ไป​เป็น​สตริง ที่​ถูก​สั่ง​ให้​กลาย​เป็น​ตัว​เลข​ตรงๆ แล้ว​วัด​ว่า​แต่ละ​ภาษา​ทำ​อะไร​เมื่อ​มัน​แปลง​ไม่​สำเร็จ


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • Go package documentation — encoding/json (เข้า​ถึง 2026-09-13) — หัวข้อ Unmarshal ระบุ​ว่า​เมื่อ​ปลาย​ทาง​เป็น interface value ตัว Unmarshal เก็บ float64 ลง​ไป​ให้ JSON number · และ​ระบุ​ว่า​เมื่อ​ค่า​ไม่​เหมาะ​กับ​ชนิด​ปลาย​ทาง หรือ​ตัวเลข​ล้น​ชนิด​ปลาย​ทาง มัน​จะ​ข้าม​ช่อง​นั้น​แล้ว​ถอด​ค่า​ต่อ​ให้​ได้​มาก​ที่สุด​เท่า​ที่​ทำได้ · ประโยค ถัด​ไป​ขึ้น​ต้น​ด้วย​เงื่อนไข If no more serious errors are encountered แล้ว​จึง​บอก​ว่า​มัน​คืน UnmarshalTypeError ของ​ความ​ผิด​พลาด​ที่​เกิด​ก่อน​ที่สุด · และ​ระบุ​ต่อว่า​ไม่​รับ​ประกัน​ว่า​ช่อง​ที่ ตาม​หลัง​ช่อง​ที่​มี​ปัญหา​จะ​ถูก​ถอด​ค่า​ครบ · หัวข้อ Decoder.UseNumber ระบุ​ว่า​มัน​ทำให้ Decoder ถอด​ตัวเลข​ลง interface value เป็น Number แทนที่​จะ​เป็น float64

  • Number.isSafeInteger() — MDN (เข้า​ถึง 2026-09-13) — นิยาม safe integer ไว้​สอง​ข้อ คือ​แทน​ค่า​ได้​ตรง​ตัว​ใน IEEE 754 และ​การ แทน​ค่า​นั้น​ต้อง​ไม่ใช่​ผล​ของ​การ​ปัด​จำนวนเต็ม​ค่า​อื่น​มา · ยก​ตัวอย่าง​ว่า 2 ** 53 ไม่ใช่ safe integer เพราะ 2 ** 53 + 1 แทน​ตรง​ตัว​ไม่​ได้ แล้ว​ถูก​ปัด​มา​เป็น 2 ** 53

  • json — JSON encoder and decoder, Python documentation (เข้า​ถึง 2026-09-13) — ตาราง​การ​แปลง​ของ JSONDecoder จับ​คู่ JSON number ที่​เป็น​จำนวนเต็ม ไว้​กับ int ของ Python · และ​ระบุ​ว่า​เมื่อ​ไม่​ได้​ตั้ง parse_int ค่า​ตั้ง​ต้น​เทียบ​เท่ากับ​การ เรียก int(num_str) กับ​สตริง​ของ​ทุก​จำนวนเต็ม​ที่​ถอด​ค่า

  • ตัวเลข​ทุก​ตัว​ใน​บท​นี้​มา​จาก scripts/integer-semantics/measured.json ซึ่ง scripts/integer-semantics/probe.py เขียน​ขึ้น​หลัง​ยิง​ทั้ง​ชุด​สาม​รอบ​ต่อ​การ​เรียก​หนึ่ง​ครั้ง · วัด​บน go1.27.0 node v24.19.0 และ Python 3.14.6 บน darwin/arm64 เมื่อ 2026-09-12

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

ข้อ 1 / 3

โปรแกรม Go อ่าน body ใบนี้ลง `map[string]any` แล้ว `Marshal` กลับออกไป ผลที่วัดได้เป็นอย่างไร