1 · สองทิศทาง: Marshal และ Unmarshal
JSON Go struct
{ type Product struct {
"id": 101, ──Unmarshal()──▶ ID int
"name": "…", Name string
"price": 2590 Price int
} ◀──Marshal()─── }
| ฟังก์ชัน | ทิศทาง | จำง่าย ๆ |
|---|---|---|
json.Marshal(v) | Go → JSON ([]byte) | ส่งออก เช่นตอบ API |
json.Unmarshal(data, &v) | JSON → Go | รับเข้า เช่นอ่าน request body |
ของที่วางอยู่บนชั้นในบ้าน จะส่งไปรษณีย์ตรง ๆ ไม่ได้ ต้องเอาลงกล่อง ปิดเทป เขียนหน้ากล่องก่อน พอปลายทางได้กล่อง เขาก็แกะกล่อง เอาของไปวางบนชั้นของเขา
· json.Marshal = แพ็คของลงกล่อง —
เอา struct ในเครื่องเรา ทำให้เป็นข้อความที่ส่งข้ามเครื่องได้
· json.Unmarshal = แกะกล่องเอาของขึ้นชั้น —
เอาข้อความ JSON ที่รับมา เทกลับเข้า struct ของเรา
ที่ไม่เหมือนกัน: ส่งพัสดุคือย้ายของไป ต้นทางไม่เหลือแล้ว แต่ Marshal คือคัดลอก — struct ตัวเดิมยังอยู่ครบ และของบางชิ้นจะไม่ถูกใส่ลงกล่องโดยไม่มีใครบอก ซึ่งคือเรื่องของทั้งบทนี้
- marshal / unmarshal
- คำที่ Go ใช้เรียก “แพ็คลงกล่อง” กับ “แกะกล่อง” · เวลาถามให้พูดว่า “ตอน marshal มันหายไป” หรือ “unmarshal แล้วได้ค่าว่าง” จะสื่อกว่าพูดว่า “แปลง JSON ไม่ได้” เพราะอีกฝ่ายรู้ทันทีว่าเป็นทิศทางไหน
- serialize / deserialize
- คำกลางที่ใช้กันทุกภาษา · ในบริบท Go ใช้แทน marshal/unmarshal ได้ ·
เจอคำไหนก็นึกถึงกล่องพัสดุใบเดิม
ระวัง: ใน Go คำว่าEncoder/Decoderหมายถึงตัวที่ทำงานกับสตรีมโดยเฉพาะ (เจอในหัวข้อ 8) ไม่ใช่คำพ้องของMarshal/Unmarshalตรง ๆ - payload
- “ของที่อยู่ในกล่อง” = ตัวเนื้อข้อมูลที่ส่งไปมาระหว่างระบบ · “ขอดู payload หน่อย” แปลว่า ขอดูก้อน JSON จริงที่ส่งไป
2 · Struct Tag คือคำสั่งที่แปะไว้ท้าย field
type User struct {
UserID int `json:"userId"`
FullName string `json:"fullName"`
Email string `json:"email"`
}
// json.Marshal ได้:
{
"userId": 101,
"fullName": "Alice Smith",
"email": "alice@email.com"
}
ถ้าไม่ใส่ tag เลย Go ใช้ชื่อ field ตรง ๆ:
// ผลรันจริงเมื่อไม่มี tag:
{"UserID":101,"FullName":"Alice Smith","Email":"alice@email.com"}
ซึ่งผิดธรรมเนียมของฝั่ง JSON — Google JSON Style Guide
บอกว่า property name ควรเป็น camelCase ส่วน Go บังคับให้ field
ที่จะ export ต้องขึ้นต้นตัวใหญ่ (PascalCase)
struct tag คือสะพานเชื่อมสองธรรมเนียมนี้
- field
- “ช่องเก็บของ” หนึ่งช่องใน struct เช่น
UserID int· พูดว่า “field ชื่อ Email” ดีกว่าพูดว่า “ตัวแปร Email” เพราะ dev จะรู้ทันทีว่าอยู่ใน struct - struct tag
- ข้อความในเครื่องหมาย
`…`ที่ต่อท้าย field · ถามให้ตรงว่า “field นี้ struct tag เขียนว่าอะไร” - camelCase / PascalCase
- รูปแบบการตั้งชื่อ ·
userId= camelCase ·UserID= PascalCase · ประโยคที่ใช้จริงคือ “JSON key ฝั่งนั้นเป็น camelCase หรือ snake_case” — ถามคำนี้ก่อนเขียน tag จะไม่ต้องมาแก้ทีหลัง
tag ใส่ได้หลาย package พร้อมกัน คั่นด้วยเว้นวรรค — แต่ละ library อ่านเฉพาะส่วนของตัวเอง:
UserID int `json:"userId" xml:"userId" validate:"required"`
3 · กับดักที่ 1: field ตัวเล็กหายไปเงียบ ๆ
ก่อนไปต่อ: กฎตัวพิมพ์ใหญ่-เล็กของ Go
Go ไม่มีคำว่า public/private — มันใช้
ตัวอักษรตัวแรกของชื่อ ตัดสินแทน:
| ชื่อขึ้นต้นด้วย | เรียกว่า | ใครเห็นได้ | ตัวอย่าง |
|---|---|---|---|
ตัวใหญ่ Name | exported | ทุก package | fmt.Println, json.Marshal |
ตัวเล็ก name | unexported | เฉพาะ package ตัวเอง | ตัวช่วยภายในไฟล์เดียวกัน |
ทำไมเรื่องนี้เกี่ยวกับ JSON: โค้ดที่แปลง struct เป็น JSON
ไม่ได้อยู่ในไฟล์คุณ — มันอยู่ใน package encoding/json ของ Go
ดังนั้นมันเห็นได้เฉพาะ field ที่เป็น exported เท่านั้น
field ตัวเล็กจึงไม่มีทางเข้าหรือออก JSON ได้เลย ต่อให้แปะ tag ไว้
วันย้ายบ้าน คุณจ้างบริษัทขนของมาช่วย
เขาเก็บของเฉพาะห้องที่เปิดประตูไว้
ห้องไหนล็อกกุญแจ เขาเดินผ่านไปเฉย ๆ — ของในห้องนั้นไม่ได้ขึ้นรถ
ขากลับก็เหมือนกัน ของที่ควรไปวางในห้องล็อก เขาเอาเข้าไม่ได้
ห้องนั้นเลยไม่ถูกแตะเลย — ของเดิมที่มีอยู่ก็ยังอยู่อย่างนั้น
ถ้าไม่มีอะไรอยู่ตั้งแต่แรกถึงจะว่างเปล่า (ตัวอย่างข้างล่างคือกรณีหลัง เพราะเป็น struct ตัวใหม่เอี่ยม)
· field ชื่อตัวใหญ่ = ห้องที่เปิดประตูไว้ ·
field ชื่อตัวเล็ก = ห้องที่ล็อกกุญแจ
· encoding/json = บริษัทขนของที่คลำหาห้องเอาเอง
(กลไกชื่อ reflection — ไม่ได้รู้จักชื่อห้องมาก่อน)
กติกาของ Go คือคนที่คลำหาแบบนี้ห้ามเปิดห้องตัวเล็กเด็ดขาด ต่อให้อยู่บ้านเดียวกันก็ตาม
· tag ที่แปะไว้บนห้องล็อก ไม่ช่วยอะไรเลย เพราะเขาเปิดประตูไม่ได้ตั้งแต่แรก
ที่ไม่เหมือนกัน: บริษัทขนของจริงยังโทรมาบอกว่า “ห้องนี้เข้าไม่ได้นะครับ”
แต่ encoding/json ไม่บอกอะไรเลย —
ไม่มี error ไม่มี warning เงียบสนิท และนี่คือจุดที่ทำให้คนพลาดกันมาแล้วจริง
type User struct {
ID int `json:"id"`
Name string `json:"name"`
password string `json:"password"` // ← p ตัวเล็ก
}
user := User{ID: 101, Name: "Alice", password: "secret123"}
data, _ := json.Marshal(user)
fmt.Println(string(data))
พิมพ์อะไรออกมา?
{"id":101,"name":"Alice"} — ผมรันทดสอบแล้ว
password ขึ้นต้นด้วยตัวเล็ก = unexported field
→ package encoding/json ซึ่งอยู่คนละ package
มองไม่เห็นเลย ต่อให้แปะ tag ไว้ก็ไม่มีผล
ไม่มี error ไม่มี warning — เงียบสนิท
ด้านกลับ: อันตรายกว่ามาก
type LoginRequest struct {
Email string `json:"email"`
password string `json:"password"` // ← ตัวเล็ก
}
// รับ JSON: {"email":"peter@email.com","password":"123456"}
// หลัง json.Unmarshal:
req.Email // "peter@email.com"
req.password // "" ← ว่างเปล่า! รหัสผ่านที่ผู้ใช้ส่งมาหายไปหมด
Unmarshal ไม่คืน error — มันมองว่า field นั้นไม่มี
แล้วปล่อยเป็น zero value (ค่าเริ่มต้นของชนิดข้อมูลนั้น เช่น
string ได้ข้อความว่าง int ได้ 0)
ผลคือรหัสผ่านที่ระบบเห็นเป็นค่าว่างทุกครั้ง
ถ้าโค้ดตรวจรหัสเขียนไว้หลวม ๆ ประตูเปิดพอดี
กฎ: field ที่ต้องเข้า/ออก JSON ต้องขึ้นต้นด้วยตัวใหญ่เสมอ
- exported / unexported
- “ห้องที่เปิด” กับ “ห้องที่ล็อก” · ถามให้ตรงว่า “field นี้ exported หรือเปล่า” จะได้คำตอบเร็วกว่าถามว่า “ทำไมมันไม่ออก JSON” เพราะคำนี้ชี้ไปที่สาเหตุตรง ๆ เลย
- zero value
- ค่าเริ่มต้นที่ Go ใส่ให้เองเมื่อไม่มีใครกำหนดค่า ·
0·false·""·nil· ประโยคที่ใช้จริง “field นี้ได้ zero value เพราะ JSON ไม่ได้ส่งมา” - silent failure
- “พังแบบเงียบ ๆ” — ทำงานผิดโดยไม่มี error โผล่มาเลย · เวลาเล่าให้ senior ฟังให้พูดว่า “มันเป็น silent failure ไม่มี error เลย” เขาจะรู้ทันทีว่าต้องไปไล่หาที่ไหน
4 · omitempty — และเหตุผลที่มันหลอกคน
type User struct {
ID int `json:"id"`
Nickname string `json:"nickname,omitempty"`
}
user := User{ID: 101, Nickname: ""}
// ได้: {"id":101} ← nickname หายไปเพราะว่าง
ดูสมเหตุสมผล จนกระทั่งเจอกรณีนี้:
type Item struct {
Price int `json:"price,omitempty"`
Active bool `json:"active,omitempty"`
}
item := Item{Price: 0, Active: false}
data, _ := json.Marshal(item)
fmt.Println(string(data))
ได้ JSON หน้าตายังไง?
{} — ว่างเปล่าทั้งก้อน ผมรันทดสอบแล้ว
omitempty ตัดทิ้งเมื่อค่าเป็น zero value —
ซึ่งรวมถึง 0, false, "",
nil, slice/map ที่ว่าง
แต่ "สินค้าราคา 0 บาท" กับ "ปิดใช้งาน" เป็นข้อมูลที่มีความหมาย!
ฝั่งรับจะแยกไม่ออกว่า "ไม่ได้ส่งมา" กับ "ส่งมาว่าเป็น 0"
ใบสมัครมีช่อง “จำนวนบุตร” ·
เว้นว่างไว้ = ยังไม่ได้ตอบ ·
เขียนเลข 0 ลงไป = ตอบแล้วว่าไม่มีลูก
สองอย่างนี้คนละความหมายกันชัด ๆ
ทีนี้ถ้าเจ้าหน้าที่ตั้งกฎว่า “ช่องไหนเป็น 0 ให้ถือว่าไม่ได้กรอก แล้วลบทิ้ง”
คำตอบ “ไม่มีลูก” จะหายไปทันที — นี่คือสิ่งที่ omitempty ทำ
ส่วน pointer คือการเพิ่มช่องติ๊ก “ไม่ได้กรอก” แยกออกมาต่างหาก
ตอนกรอกส่งออกไปจึงแยกได้ครบทั้ง 3 สถานะ
ที่ไม่เหมือนกัน: ฟอร์มกระดาษที่เว้นว่าง อย่างน้อยยังเห็นช่องอยู่
แต่ JSON ที่โดน omitempty ตัด key นั้นหายไปจากก้อนข้อมูลเลย
ฝั่งรับไม่เห็นแม้แต่ชื่อช่อง จึงไม่มีทางรู้ว่าเคยมี field นี้อยู่
ข้อควรระวัง — ขาเข้าไม่ได้ 3 สถานะ:
ตอน ขาออก (Marshal) pointer แยกได้ 3 สถานะจริง
แต่ตอน ขาเข้า (Unmarshal) nil หมายได้ทั้ง
“ไม่ได้ส่ง key มาเลย” และ “ส่ง key มาเป็น null”
— ผมรันทดสอบแล้ว ทั้ง {} และ {"p":null} ได้ P == nil เท่ากัน
ถ้าต้องแยกสองอันนี้จริง ๆ ต้องไปอ่าน JSON ดิบเอง เช่นรับเป็น
map[string]json.RawMessage แล้วเช็กว่ามี key นั้นอยู่ไหม
ทางแก้: ใช้ pointer
type Item struct {
Price *int `json:"price,omitempty"`
Active *bool `json:"active,omitempty"`
}
// ไม่ต้องการส่งจริง ๆ — ปล่อยเป็น nil:
Item{}
// ได้: {}
// ต้องการส่งค่า zero จริง ๆ:
var price int
var active bool
Item{Price: &price, Active: &active}
// ได้: {"price":0,"active":false} ← ส่งออกไปครบ
pointer แยกได้ 3 สถานะ ในขณะที่ค่าธรรมดาแยกได้แค่ 2:
nil = "ไม่ได้ส่ง (หรือส่ง null มา)" · &0 = "ส่งมาว่า 0" ·
&5 = "ส่งมาว่า 5" — สำคัญมากตอนทำ PATCH API
(API แบบ "แก้เฉพาะ field ที่ส่งมา ที่เหลือปล่อยไว้เหมือนเดิม")
ที่ต้องแยก "ไม่แก้ field นี้" กับ "แก้เป็นศูนย์"
ย้ำอีกรอบ: ตอนรับเข้ามา nil ยังแยกไม่ออกระหว่าง
"ไม่ได้ส่ง key มา" กับ "ส่ง null มาเพื่อสั่งล้างค่า" —
ถ้าธุรกิจต้องแยกสองอันนี้ ต้องอ่าน JSON ดิบเอง
- omitempty
- ตัวเลือกใน tag ที่แปลว่า “ถ้าค่าว่างให้ตัด key นี้ทิ้ง” ·
เวลาโดนถามว่าทำไมข้อมูลหาย ให้ตอบว่า
“field นั้นติด omitempty อยู่ พอค่าเป็นค่าว่างเลยไม่ออกไป”
“ค่าว่าง” ที่ว่านี้คือรายการเจาะจง:0·false·""·nil· slice/map ที่ว่าง เท่านั้น
ระวัง: struct ว่าง กับtime.Timeที่ยังไม่ได้ตั้งค่า ไม่โดนตัด — ผมรันทดสอบแล้วInnerว่างยังออกมาเป็น{"s":{"A":0}}อยู่ดี ถ้าอยากให้มันหายจริงต้องเปลี่ยนเป็น pointer - optional field
- field ที่ “มีก็ได้ ไม่มีก็ได้” ในข้อตกลงระหว่างสองระบบ · ประโยคที่ใช้ตอนออกแบบ API คือ “field นี้เป็น optional หรือ required” — ถามคำนี้ก่อนเขียนโค้ดจะลดปัญหาไปเยอะ
- nullable
- “ค่านี้เป็น
nullได้” — แค่นั้น คือ 2 สถานะ (มีค่า / เป็น null) ไม่ได้แปลว่า 3 สถานะ · เวลาขอความเห็น senior ให้พูดว่า “field นี้ nullable ไหม ถ้าใช่ ผมจะเปลี่ยนเป็น *int” เขาจะเข้าใจทันที - absent vs null
- การแยกระหว่าง “ไม่ได้ส่ง key นั้นมาเลย” (absent)
กับ “ส่ง key มาแต่ค่าเป็น
null” · เป็นคำที่คนทำPATCHAPI พูดกันจริง (มาตรฐานที่อ้างกันคือ JSON Merge Patch — RFC 7386) · ประโยคที่ใช้คือ “อันนี้ต้องแยก absent กับ null ไหม” · ถ้าต้องแยกจริง*intไม่พอ เพราะทั้งสองกรณีได้nilเหมือนกัน
5 · json:"-" — ห้าม field นี้เข้าออก JSON
type User struct {
ID int `json:"id"`
Email string `json:"email"`
PasswordHash string `json:"-"` // ← ไม่ออกไปแน่นอน
}
// ผลรันจริง: {"id":101,"email":"alice@example.com"}
json:"-" ใช้ได้ แต่วันไหนมีคนเผลอลบ tag ออก
ข้อมูลลับหลุดทันทีโดยไม่มีอะไรเตือน
ทีมส่วนใหญ่จึงแยกเป็นคนละ struct ไปเลย — แล้วตั้งกฎว่า
struct ฝั่งซ้ายจะไม่ถูกส่งเข้า json.Marshal เด็ดขาด
ตัวที่ handler (ฟังก์ชันที่รับ request เข้ามาแล้วตอบกลับ)
หยิบไปตอบผู้เรียกใช้ มีแค่ตัวขวาเท่านั้น
ใช้ในระบบเท่านั้น — ห้ามส่งเข้า Marshal
type UserModel struct {
ID int
Email string
PasswordHash string
}
ตัวเดียวที่ส่งออกไปข้างนอก
type UserResponse struct {
ID int `json:"id"`
Email string `json:"email"`
}
อย่าเข้าใจผิดว่า "ไม่มี tag = ไม่ออก JSON" —
จากหัวข้อ 2 ถ้าไม่มี tag Go จะใช้ชื่อ field ตรง ๆ ดังนั้น
json.Marshal(userModel) ยังได้
{"ID":1,"Email":"...","PasswordHash":"..."} ครบทุก field
(ผมรันแล้ว) สิ่งที่กันข้อมูลหลุดคือ "ไม่มีใครเอา UserModel ไป Marshal"
ไม่ใช่การไม่ใส่ tag
6 · กฎของ json.Unmarshal() ที่ต้องรู้ครบ
6.1 ลืม & = ไม่ทำงาน แต่ได้ error บอก
json.Unmarshal(data, &product) // ✅
json.Unmarshal(data, product) // ❌
// error จริงที่ได้:
// json: Unmarshal(non-pointer main.ProductPrice)
เหตุผลย้อนไปที่ บทเรียน 07:
Go ส่งค่าแบบ pass by value 100% —
ถ้าไม่ส่ง pointer ไป Unmarshal จะแก้ได้แค่ copy
อันนี้ยังดีที่มี error เตือน ไม่เหมือนกับดักอื่นในบทนี้
6.2 field ที่ JSON ไม่ได้ส่งมา → zero value
// JSON: {"name":"Alice"}
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Active bool `json:"active"`
}
// ผลรันจริง: {Name:Alice Age:0 Active:false} — ไม่มี error
6.3 field ส่วนเกินใน JSON → ถูกทิ้งเงียบ ๆ
// JSON: {"email":"alice@example.com","admin":true}
type LoginRequest struct {
Email string `json:"email"`
}
// ผลรันจริง: err = <nil> ← ผ่านฉลุย "admin" หายไปเฉย ๆ
6.4 type ไม่ตรง → อันนี้มี error
// Struct ต้องการ int แต่ JSON ส่ง string มา
type Product struct {
Price int `json:"price"`
}
// JSON: {"price":"2590"}
// error จริง:
// json: cannot unmarshal string into Go struct field
// ProductPrice.price of type int
7 · เรื่องที่สไลด์ไม่ได้บอก: การจับคู่ key ไม่ สนตัวพิมพ์
type StrictUser struct {
ID string `json:"id"`
Name string `json:"name"`
}
// JSON ส่งมาเป็นตัวพิมพ์ใหญ่:
json.Unmarshal([]byte(`{"ID":"P001","NAME":"Keyboard"}`), &u)
ได้ค่าเข้า struct ไหม หรือได้ค่าว่าง?
ได้ครบ — {ID:P001 Name:Keyboard} ผมรันทดสอบแล้ว
encoding/json จับคู่แบบ case-insensitive เมื่อหา
exact match ไม่เจอ ข้อดีคือทนต่อ API ที่ตั้งชื่อไม่สม่ำเสมอ
ข้อเสียคือมันปิดบังบั๊ก — โค้ดทำงานได้ทั้งที่ contract
ระหว่างสองระบบไม่ตรงกันจริง ๆ วันหนึ่งอีกฝั่งเปลี่ยนไปใช้ชื่ออื่น
คุณจะเพิ่งรู้ว่าไม่เคยตกลงกันชัดเจน
8 · json.NewDecoder() — เมื่อ Unmarshal ไม่พอ
Unmarshal ต้องได้ข้อมูลครบทั้งก้อนก่อนถึงจะเริ่มแกะได้
ส่วน NewDecoder อ่านทีละส่วนไปแกะไป — วิธีหลังนี้เรียกว่าอ่านแบบ
stream
ดูหนังออนไลน์ไม่ต้องรอโหลดจบทั้งเรื่องก่อนถึงจะกดเล่นได้
เครื่องเล่นดึงข้อมูลมาทีละก้อนเรื่อย ๆ จึงไม่ต้องเก็บไฟล์ทั้งเรื่องไว้ในเครื่องพร้อมกัน
· os.ReadFile + Unmarshal = โหลดหนังทั้งเรื่องลงเครื่องก่อน แล้วค่อยเปิดดู
· json.NewDecoder = ดึงมาทีละก้อนจากสายที่ต่ออยู่
ที่ไม่เหมือนกัน: ดูหนังคือเห็นฉากแรกได้เลยทั้งที่ยังโหลดไม่จบ
แต่ Decode(&v) กับ JSON ก้อนเดียว
ยังต้องอ่านจนจบก้อนนั้นก่อน ถึงจะได้ struct ที่เติมค่าครบให้เอาไปใช้ —
ห้ามคิดว่าอ่าน field แรก ๆ ออกมาใช้ก่อนได้
ที่ประหยัดจริงคือ ไม่ต้องแบก []byte ทั้งก้อนไว้ในแรม
และ อ่าน JSON หลายก้อนต่อกันในสายเดียวได้ — ผมรันทดสอบแล้ว
ป้อน {"Pub":1}{"Pub":2}{"Pub":3} เข้าไป เรียก Decode ซ้ำ ๆ ได้ทีละก้อนจริง
os.ReadFile + Unmarshal | json.NewDecoder | |
|---|---|---|
| อ่านข้อมูล | โหลดทั้งไฟล์เข้า RAM ก่อน | อ่านทีละส่วนแบบ stream |
| ไฟล์ 2 GB | กิน RAM 2 GB | กิน RAM คงที่ |
| รับจาก network | ต้องรออ่านให้ครบก่อน | เริ่มแกะได้เลย |
| กัน field แปลกปลอม | ❌ ไม่มีทางเลือก | ✅ DisallowUnknownFields() |
file, err := os.Open("product.json")
if err != nil {
return err
}
defer file.Close() // ← บทเรียน 08
var product Product
decoder := json.NewDecoder(file)
if err := decoder.Decode(&product); err != nil {
return err
}
ปิดช่องโหว่ 6.3 ด้วย DisallowUnknownFields()
type User struct {
ID string `json:"id"`
Name string `json:"name"`
}
raw := `{"name":"admin","isAdmin":true}`
decoder := json.NewDecoder(strings.NewReader(raw))
decoder.DisallowUnknownFields() // ← เปิดโหมดเข้มงวด
if err := decoder.Decode(&user); err != nil {
fmt.Println("error", err)
return
}
// ผลรันจริง: error json: unknown field "isAdmin"
เปิดเสมอสำหรับ config file และ request body ที่รับจากภายนอก —
ผู้ใช้พิมพ์ "tiemout" แทน "timeout" ใน config
ถ้าไม่เปิดโหมดนี้ ระบบจะใช้ค่า default เงียบ ๆ แล้วคุณจะหาสาเหตุไม่เจอ
ส่วนตอนรับ response จาก API ภายนอก ไม่ควรเปิด
เพราะเขาอาจเพิ่ม field ใหม่เมื่อไหร่ก็ได้ แล้วระบบคุณจะพังทันที
9 · ทดสอบความจำ
1. field ชื่อ password (ตัวเล็ก) ที่มี json:"password" จะเป็นยังไง?
field ตัวเล็ก = unexported → encoding/json ซึ่งอยู่คนละ package
เข้าถึงไม่ได้ ไม่มี error ไม่มี warning ขา Marshal ก็หาย
ขา Unmarshal ก็ได้ค่าว่าง — เคยเป็นสาเหตุของช่องโหว่ login มาแล้วจริง
2. Price int + omitempty เมื่อค่าเป็น 0 จะได้ JSON แบบไหน?
omitempty ตัดทิ้งทุกอย่างที่เป็น zero value —
0, false, "", nil
ทั้งที่บางครั้งค่าเหล่านี้มีความหมาย
ทางแก้: เปลี่ยนเป็น *int ซึ่งแยก "ไม่ส่ง"
(nil) ออกจาก "ส่งค่า 0" ได้
3. JSON มี field เกินกว่าที่ struct ประกาศไว้ — Unmarshal ทำยังไง?
เป็นค่าเริ่มต้นของ encoding/json — ออกแบบมาให้ทนต่อ API
ที่เพิ่ม field ใหม่ ถ้าต้องการเข้มงวด ต้องใช้
json.NewDecoder(...) แล้วเรียก DisallowUnknownFields()
ซึ่งจะได้ json: unknown field "isAdmin"
4. ทำไมถึงควรใช้ json.NewDecoder แทน ReadFile+Unmarshal?
เหตุผลหลักคือ stream — อ่านทีละส่วน ไม่กิน RAM ตามขนาดไฟล์
เหมาะกับ HTTP request body และไฟล์ใหญ่ ของแถมคือได้
DisallowUnknownFields() มาด้วย
(Decode ยังต้องส่ง & เหมือนเดิมนะ)
10 · ลงมือทำ
ภารกิจ: ค้นหาโปรไฟล์ผู้เล่นและแปลงเป็น JSON (workshop จากสไลด์)
- สร้าง
type Player structที่มีID,UserName,NickName,Password - เขียน
findPlayer(id int) (Player, error)— ไม่พบให้คืนErrPlayerNotFound(sentinel จาก บทเรียน 12) - ถ้าพบ ให้แปลงเป็น JSON โดยมีเงื่อนไข:
- key ต้องเป็น camelCase (
playerId,userName) Passwordห้ามหลุดออกไปใน JSONNickNameถ้าไม่มีค่า ไม่ต้องแสดง
- key ต้องเป็น camelCase (
- ผลลัพธ์ที่ต้องได้:
findPlayer(101) → {"playerId":101,"userName":"go_master"} findPlayer(999) → error: player not found - ตรวจงานตัวเอง: ทำข้อ "Password ห้ามหลุด" ด้วย
json:"-"ไปแล้วใช่ไหม — ลองทำอีกวิธี ด้วยการแยกPlayerResponseออกมา แล้วตอบตัวเองว่าแบบไหนที่เพื่อนร่วมทีมเผลอทำพังยากกว่า - ท้าทาย 1: ทำให้
findPlayer(101)ที่มี nickname กับไม่มี nickname ให้ JSON ต่างกันจริง — พิสูจน์ว่าomitemptyทำงาน - ท้าทาย 2: เขียนขากลับ — รับ JSON
{"playerId":101,"userName":"go_master","isAdmin":true}แล้วUnmarshalกลับเป็นPlayer- ใช้
json.Unmarshalธรรมดา → เกิดอะไรกับisAdmin? - เปลี่ยนไปใช้
json.NewDecoder+DisallowUnknownFields()→ ต่างกันยังไง?
- ใช้
- ท้าทาย 3: ลอง
Unmarshal{"PLAYERID":999}(ตัวพิมพ์ใหญ่หมด) เข้าPlayer— เข้าไหม? ถ้าเข้า อธิบายว่าทำไม (ย้อนดูหัวข้อ 7)
อ่านต่อจากต้นทาง
แหล่งหลักที่แนะนำ:
JSON and Go — บทความทางการที่อธิบาย
ทั้ง Marshal/Unmarshal/Decoder และเคสที่ไม่รู้โครงสร้างล่วงหน้า
(map[string]any) ประมาณ 15 นาที
ตอนเขียนโค้ดจริงให้เปิด
pkg.go.dev — json.Marshal
ไว้ข้าง ๆ เพราะกฎการแปลงทุกข้อ (รวม omitempty และ
-) อยู่ในหน้านั้นครบ
💬 ติดตรงไหนถามได้เลย — ถ้าเจอ JSON ที่โครงสร้าง
ไม่แน่นอน (บางครั้งเป็น object บางครั้งเป็น array)
บอกมาได้ มีวิธีจัดการที่ยังไม่ได้สอนในบทนี้
(json.RawMessage และ custom UnmarshalJSON)
อ้างอิง
- The Go Blog — JSON and Go — บทความทางการ ครอบคลุมทั้ง encode/decode/stream
- pkg.go.dev — json.Marshal — กฎการแปลงทุกข้อ รวม tag options
- pkg.go.dev — DisallowUnknownFields
- Google JSON Style Guide — ที่มาของกฎ camelCase
- Go spec — Struct types — นิยามของ struct tag