encoding/json
JSON ใน Go
หัวข้อที่มีชื่อว่า “JSON ใน Go”Package encoding/json แปลงค่า Go เป็น JSON text (marshaling) และแปลง JSON text กลับเป็นค่า Go (unmarshaling) function หลักสองตัวคือ:
data, err := json.Marshal(v) // Go value → []byte (JSON)err = json.Unmarshal(data, &v) // []byte (JSON) → Go valueMarshal รับค่าใดก็ได้และคืน []byte Unmarshal เขียนลงในค่าที่ชี้โดย argument ที่สอง — ส่ง pointer เสมอเพื่อให้ function แก้ไขตัวแปรได้
Struct tags
หัวข้อที่มีชื่อว่า “Struct tags”Struct tag คือ raw-string literal บน field ของ struct ที่ให้ metadata Package encoding/json อ่าน tag json:"name" เพื่อควบคุมการ serialize:
type User struct { Name string `json:"name"` Email string `json:"email,omitempty"` Age int `json:"age"`}syntax ของ tag คือ `json:"<name>[,options]"` option ที่พบบ่อย:
| Tag | ผล |
|---|---|
json:"name" | ใช้ name เป็น JSON key แทนชื่อ field |
json:"name,omitempty" | ละเว้น field นี้เมื่อเป็น false, 0, nil pointer/interface หรือ empty string/slice/map/array — ไม่รวม zero struct หรือ zero time.Time |
json:"name,omitzero" (Go 1.24) | ละเว้น field นี้เมื่อเป็น zero value ของ type นั้น รวมถึง struct และ time.Time — เรียก method IsZero() bool ถ้า type มี method นี้ |
json:"-" | ละเว้น field นี้เสมอ |
omitempty เช็กแค่ความ “ว่าง” ระดับภาษา (zero value ของ basic type หรือความยาวเป็น 0 สำหรับ slice/map) struct — แม้ทุก field ข้างในจะเป็น zero — ไม่ถือว่า “ว่าง” ตามกฎนี้ omitempty จึงไม่มีวันละเว้น struct time.Time เองก็เป็น struct ผลจึงเหมือนกัน: time.Time{} ที่เป็น zero ยัง marshal ออกมาเป็น "0001-01-01T00:00:00Z"
type Location struct { City string}
type Event struct { Name string `json:"name"` Location Location `json:"location,omitempty"` // struct — NOT omitted when zero When time.Time `json:"when,omitempty"` // time.Time — NOT omitted when zero}
e := Event{Name: "party"} // Location and When left at their zero valuesdata, _ := json.Marshal(e)fmt.Println(string(data))// {"name":"party","location":{"City":""},"when":"0001-01-01T00:00:00Z"}omitzero (Go 1.24) แก้ปัญหานี้ตรงๆ: มันละเว้น field ทุกครั้งที่ค่าเท่ากับ zero value ของ type นั้น รวมถึง struct ด้วย และเรียก IsZero() บน field เมื่อ type นั้นนิยาม method ไว้ — ซึ่งเป็นเหตุผลที่มันถือว่า time.Time{} เป็นค่าว่างได้ถูกต้อง
type Event2 struct { Name string `json:"name"` Location Location `json:"location,omitzero"` // omits zero structs too When time.Time `json:"when,omitzero"` // uses time.Time.IsZero()}
e2 := Event2{Name: "party"}data2, _ := json.Marshal(e2)fmt.Println(string(data2))// {"name":"party"}Nested structs
หัวข้อที่มีชื่อว่า “Nested structs”Field ของ struct สามารถเป็น struct ได้เช่นกัน json.Marshal จะ recurse เข้าไปโดยอัตโนมัติ:
type Address struct { City string `json:"city"` Country string `json:"country"`}
type User struct { Name string `json:"name"` Age int `json:"age"` Address Address `json:"address"`}JSON ที่ได้จะฝัง address object ไว้ภายใต้ key "address"
map[string]any — การ unmarshal แบบยืดหยุ่น
หัวข้อที่มีชื่อว่า “map[string]any — การ unmarshal แบบยืดหยุ่น”เมื่อยังไม่มี struct type ล่วงหน้า ให้ unmarshal ลงใน map[string]any แต่ละค่า JSON จะกลายเป็น Go counterpart ที่เป็นธรรมชาติ: string, float64, bool, []any, map[string]any หรือ nil
var m map[string]anyjson.Unmarshal([]byte(`{"lang":"Go","version":1.22}`), &m)fmt.Println(m["lang"]) // Gofmt.Println(m["version"]) // 1.22 (float64)package main
import ( "encoding/json" "fmt")
type Address struct { City string `json:"city"` Country string `json:"country"`}
type User struct { Name string `json:"name"` Age int `json:"age"` Email string `json:"email,omitempty"` Address Address `json:"address"`}
func main() { // Marshal a struct — Email is set, so it appears u := User{ Name: "Alice", Age: 30, Address: Address{ City: "Bangkok", Country: "Thailand", }, } data, err := json.Marshal(u) if err != nil { fmt.Println("marshal error:", err) return } fmt.Println(string(data))
// omitempty: Email is empty, so it is omitted u2 := User{Name: "Bob", Age: 25, Address: Address{City: "London", Country: "UK"}} data2, _ := json.Marshal(u2) fmt.Println(string(data2))
// Unmarshal back into a struct var result User err = json.Unmarshal(data, &result) if err != nil { fmt.Println("unmarshal error:", err) return } fmt.Printf("Name: %s, City: %s\n", result.Name, result.Address.City)
// map[string]any — flexible unmarshaling raw := `{"lang":"Go","version":1.22,"stable":true}` var m map[string]any _ = json.Unmarshal([]byte(raw), &m) fmt.Println(m["lang"], m["version"], m["stable"])} Unexported field มองไม่เห็นเลย ไม่ใช่แค่ถูกละเว้น
หัวข้อที่มีชื่อว่า “Unexported field มองไม่เห็นเลย ไม่ใช่แค่ถูกละเว้น”field ที่ unexported (ขึ้นต้นด้วยตัวพิมพ์เล็ก) ไม่ได้แค่ถูกข้ามด้วย semantics ของ omitempty — encoding/json มองไม่เห็นมันเลยไม่ว่าจะผ่าน reflection หรือทางไหนก็ตาม ใส่ tag ยังไงก็ไม่ช่วย:
type Account struct { Name string `json:"name"` secret string // never marshaled, never populated by Unmarshal, no matter what}json.Marshal(Account{Name: "root", secret: "swordfish"}) จะได้ {"name":"root"} — field secret ไม่มีตัวตนเลยเท่าที่ package นี้เกี่ยวข้อง นี่คือสาเหตุที่พบบ่อยของความสับสน “ทำไม field ของฉันไม่ขึ้น” เมื่อโค้ดที่ AI เขียน copy ชื่อ field ตัวพิมพ์เล็กมาจากที่อื่นในโค้ดเบส
decode เป็น any: float64 เสียความละเอียด และ json.Number
หัวข้อที่มีชื่อว่า “decode เป็น any: float64 เสียความละเอียด และ json.Number”เมื่อไม่มี struct type เป้าหมาย Unmarshal/Decode จะแปลงตัวเลข JSON ทุกตัวเป็น float64 ของ Go — รวมถึงจำนวนเต็มด้วย float64 มี mantissa แค่ 53 bit ดังนั้นจำนวนเต็มที่มากกว่า 2^53 (9007199254740992) จะเสียความละเอียดเงียบๆ ทันทีที่กลายเป็น any:
var v anyjson.Unmarshal([]byte(`{"n": 9007199254740993}`), &v)// v["n"] is 9.007199254740992e+15 — the trailing 3 became a 2json.Decoder.UseNumber() แก้ปัญหานี้: มัน decode ตัวเลข JSON เป็น json.Number ซึ่งเป็น string type ที่เก็บ text ต้นฉบับไว้ แล้วค่อยแปลงเป็นตัวเลขเองด้วย .Int64() หรือ .Float64() เมื่อต้องการ numeric type จริงๆ — field ID ที่เป็นเลขใหญ่คือกรณีคลาสสิกที่วิธีนี้ช่วยได้
Decoder เทียบกับ Unmarshal: DisallowUnknownFields และ streaming ด้วย More()
หัวข้อที่มีชื่อว่า “Decoder เทียบกับ Unmarshal: DisallowUnknownFields และ streaming ด้วย More()”json.Unmarshal(data, &v) ต้องการ payload ทั้งหมดในหน่วยความจำเป็น []byte json.NewDecoder(r).Decode(&v) อ่านตรงจาก io.Reader — ไม่ต้องมี buffer เต็มก่อน และรองรับสองอย่างที่ Unmarshal ไม่มี:
dec.DisallowUnknownFields()ปฏิเสธ JSON key ใดๆ ที่ไม่มี field ของ struct ตรงกัน แทนที่จะเพิกเฉยเงียบๆ มีประโยชน์ที่ขอบของ API ที่ field พิมพ์ผิดหรือไม่คาดคิดควรเป็น error ที่ชัดเจนdec.More()ให้ decode stream ของค่าทีละตัว — เช่น element ของ JSON array — โดยไม่ต้องเก็บ array ทั้งหมดไว้ในหน่วยความจำ
dec := json.NewDecoder(r)dec.Token() // consume the opening '['for dec.More() { var item Item dec.Decode(&item) // one element at a time}[]byte, time.Time และ nil slice เทียบกับ slice ว่าง
หัวข้อที่มีชื่อว่า “[]byte, time.Time และ nil slice เทียบกับ slice ว่าง”กฎการ encode สามข้อที่พลาดได้ง่ายเมื่อเขียน API contract เอง:
[]bytemarshal เป็น JSON string แบบ base64 (RFC 4648) ไม่ใช่ JSON array ของตัวเลขtime.Timemarshal เป็น string แบบ RFC 3339 โดย default (รูปแบบเวลาในตัวรูปแบบเดียวของjson.Marshal)- slice ที่เป็น
nilmarshal เป็นnull; slice ว่างที่ไม่ใช่ nil ([]T{}) marshal เป็น[]endpointGET /itemsที่ลืม initialize slice ผลลัพธ์จะส่ง"items":nullให้ client ที่คาดหวัง"items":[]— เป็น bug ที่กระทบ API-compatibility จริงๆ ไม่ใช่แค่เรื่องความสวยงาม
Custom MarshalJSON และ alias trick
หัวข้อที่มีชื่อว่า “Custom MarshalJSON และ alias trick”type สามารถควบคุมการ encode JSON ของตัวเองได้โดย implement MarshalJSON() ([]byte, error) กับดักคือ: การเรียก json.Marshal บน receiver ข้างใน method นั้นจะเรียก MarshalJSON ซ้ำอีกครั้ง วนซ้ำไม่รู้จบ วิธีแก้คือ alias trick — นิยาม type ใหม่ที่มี field เดิมทุกตัวแต่ไม่มี method ของ type เดิมเลย แล้ว marshal type ใหม่นั้นแทน:
type Node struct { Value int `json:"value"` Children []Node `json:"children"`}
func (n Node) MarshalJSON() ([]byte, error) { type alias Node // same fields, no methods — breaks the recursive call return json.Marshal(struct { Total int `json:"total"` alias }{ Total: n.Value + len(n.Children), alias: alias(n), })}type alias Node สร้าง named type ใหม่ที่มี field layout ตรงกับ Node เป๊ะ แต่มี method set ว่างเปล่าใหม่ ดังนั้น json.Marshal(alias(n)) จึงไปใช้ default struct encoder แทนที่จะเรียก MarshalJSON ซ้ำอีกครั้ง
package main
import ( "bytes" "encoding/json" "fmt" "strings" "time")
// secret is unexported — encoding/json can never see it, tag or no tagtype Account struct { Name string `json:"name"` secret string}
// Node uses a MarshalJSON with the "alias trick" to avoid infinite recursion:// aliasing Node to a new type with the same fields drops the MarshalJSON method,// so encoding it with the alias type calls the default struct encoder, not itself.type Node struct { Value int `json:"value"` Children []Node `json:"children"`}
func (n Node) MarshalJSON() ([]byte, error) { type alias Node // same fields, no methods — breaks the recursive call return json.Marshal(struct { Total int `json:"total"` alias }{ Total: n.Value + len(n.Children), alias: alias(n), })}
type Blob struct { Raw []byte `json:"raw"` When time.Time `json:"when"` Tags []string `json:"tags"` Empty []string `json:"empty"`}
func main() { // 1. unexported field is invisible to encoding/json a := Account{Name: "root", secret: "swordfish"} data, _ := json.Marshal(a) fmt.Println("unexported dropped:", string(data))
// 2. decoding into any: numbers become float64, large ints lose precision var any1 any json.Unmarshal([]byte(`{"n": 9007199254740993}`), &any1) m := any1.(map[string]any) fmt.Printf("as any: %v (%T)\n", m["n"], m["n"])
// json.Number keeps the exact text instead dec := json.NewDecoder(strings.NewReader(`{"n": 9007199254740993}`)) dec.UseNumber() var any2 any dec.Decode(&any2) m2 := any2.(map[string]any) num := m2["n"].(json.Number) fmt.Printf("as json.Number: %v (%T) -> Int64: %v\n", num, num, func() string { i, err := num.Int64() return fmt.Sprintf("%d err=%v", i, err) }())
// 3. DisallowUnknownFields rejects fields the struct doesn't declare type Strict struct { Name string `json:"name"` } var s Strict dec2 := json.NewDecoder(strings.NewReader(`{"name":"Ada","extra":1}`)) dec2.DisallowUnknownFields() err := dec2.Decode(&s) fmt.Println("DisallowUnknownFields error:", err)
// 4. Decoder streaming with dec.More() over a JSON array, one element at a time dec3 := json.NewDecoder(strings.NewReader(`[1,2,3]`)) dec3.Token() // consume the opening '[' var nums []int for dec3.More() { var n int dec3.Decode(&n) nums = append(nums, n) } fmt.Println("streamed with dec.More():", nums)
// 5. []byte marshals as base64, time.Time as RFC3339 b := Blob{ Raw: []byte("hi"), When: time.Date(2026, 9, 13, 12, 0, 0, 0, time.UTC), Tags: []string{"a"}, Empty: []string{}, } var nilTags []string b.Tags = nilTags // nil slice data2, _ := json.Marshal(b) fmt.Println("[]byte + time.Time:", string(data2))
// 6. custom MarshalJSON with the alias trick against infinite recursion tree := Node{Value: 1, Children: []Node{{Value: 2}, {Value: 3}}} data3, _ := json.Marshal(tree) fmt.Println("custom MarshalJSON:", string(data3))
// 7. nil slice encodes as null, empty slice encodes as [] type Lists struct { Nil []int `json:"nil"` Empty []int `json:"empty"` } var buf bytes.Buffer json.NewEncoder(&buf).Encode(Lists{Nil: nil, Empty: []int{}}) fmt.Print("nil vs empty slice: ", buf.String())} output จริงจากโปรแกรมนี้บน Go 1.26.5:
unexported dropped: {"name":"root"}as any: 9.007199254740992e+15 (float64)as json.Number: 9007199254740993 (json.Number) -> Int64: 9007199254740993 err=<nil>DisallowUnknownFields error: json: unknown field "extra"streamed with dec.More(): [1 2 3][]byte + time.Time: {"raw":"aGk=","when":"2026-09-13T12:00:00Z","tags":null,"empty":[]}custom MarshalJSON: {"total":3,"value":1,"children":[{"total":2,"value":2,"children":null},{"total":3,"value":3,"children":null}]}nil vs empty slice: {"nil":null,"empty":[]}สังเกตบรรทัดสุดท้ายของ children ใน output ของ tree: {Value: 3} ไม่มีการตั้งค่า Children จึงเป็น nil slice และ marshal เป็น null — กฎเดียวกับข้อ 7 ที่ปรากฏขึ้นในผลลัพธ์ของ MarshalJSON แบบ custom ด้วย
encoding/json/v2 (Go 1.27)
หัวข้อที่มีชื่อว่า “encoding/json/v2 (Go 1.27)”Go 1.27 มาพร้อม encoding/json/v2 และ package ระดับล่างคู่กัน encoding/json/jsontext ในสถานะ stable ทุกอย่างข้างบนยังใช้ได้: API ของ v1 encoding/json ไม่เปลี่ยน แต่ตอนนี้ สร้างอยู่บน v2 โปรแกรมเดิมจึงได้ decoder ที่เร็วขึ้นฟรี ๆ (ปิดได้ด้วย GOEXPERIMENT=nojsonv2 ถ้าเจอพฤติกรรมต่างกัน)
ให้ import v2 เมื่อต้องการ API และ default ที่เข้มงวดกว่า:
package main
import ( "encoding/json/v2" "fmt" "os")
type User struct { Name string `json:"name"` Email string `json:"email,omitempty"` Age int `json:"age"`}
func main() { u := User{Name: "Ada", Age: 36}
data, err := json.Marshal(u) // same shape as v1 … fmt.Println(string(data), err)
var back User err = json.Unmarshal(data, &back) fmt.Printf("%+v %v\n", back, err)
data, _ = json.Marshal(map[string]int{"b": 2, "a": 1}, json.Deterministic(true)) // … plus variadic Options fmt.Println(string(data))
_ = json.MarshalWrite(os.Stdout, u) // straight to an io.Writer fmt.Println()
// stricter defaults: duplicate keys are rejected err = json.Unmarshal([]byte(`{"name":"A","name":"B"}`), &back) fmt.Println("duplicate key:", err)} json.UnmarshalRead(r, &u) คือด้านกลับของ MarshalWrite — decode จาก io.Reader อย่าง resp.Body ได้ตรง ๆ
สิ่งที่ต่างจาก v1:
| เรื่อง | v1 encoding/json | v2 encoding/json/v2 |
|---|---|---|
| UTF-8 ผิดรูปใน string | แทนที่เงียบ ๆ | ปฏิเสธ |
| key ซ้ำใน object | ตัวหลังชนะ | ปฏิเสธ |
| การจับคู่ชื่อ field | ไม่สนตัวพิมพ์ | ต้องตรงเป๊ะโดย default (json.MatchCaseInsensitiveNames(true) เพื่อผ่อน) |
| Options | setter ระดับ Encoder | ค่า Options ต่อการเรียก ประกอบกันได้ |
| ฝัง field ของ struct | anonymous field | tag json:",embed" (ชื่อเก่า inline ถูกเปลี่ยน) |
| ความเร็ว Unmarshal | baseline | เร็วขึ้นอย่างมีนัยสำคัญ Marshal เท่าเดิม |
การจับคู่ชื่อ field แบบเข้มงวดของ v2 พลาดได้ง่ายแบบเงียบ ๆ เมื่อ import encoding/json/v2 แล้ว unmarshal key ตัวพิมพ์เล็กลง field ที่ไม่มี tag จะคืน error เป็น nil และปล่อย field ว่างไว้:
type User struct { Name string // no tag}
var u Usererr := json.Unmarshal([]byte(`{"name":"Ada"}`), &u)// v1: err == nil, u.Name == "Ada" — matched case-insensitively// v2: err == nil, u.Name == "" — no exact match, field left alonev1 จับคู่ name กับ Name แบบไม่สนตัวพิมพ์ แต่ v2 ต้องตรงตัวพิมพ์ (หรือมี tag json:"name") เป็นค่าเริ่มต้น ดังนั้นเวลาย้าย code มาใช้ v2 ให้ใส่ tag ให้ชัดเจนเสมอ
jsontext คือชั้น streaming ที่อยู่ข้างใต้: jsontext.Decoder และ jsontext.Encoder เดินผ่าน document เป็น Token และ Value พร้อมตรวจ syntax ไปด้วย เหมาะกับ payload ขนาดใหญ่มากหรือการเขียน JSON ใหม่โดยไม่ decode เป็นค่า Go
dec := jsontext.NewDecoder(r)for { tok, err := dec.ReadToken() if err == io.EOF { break } // inspect tok.Kind(): '{', '}', '"', '0', …}หลักคิดสำหรับปี 2026: โค้ดเดิมและ wire format ที่ยอมรับ input หลวม ๆ ใช้ v1 ต่อไป service ใหม่เริ่มที่ v2 เพื่อให้ JSON ผิดรูป fail เสียงดังตั้งแต่ขอบระบบ
เส้นทาง marshal/unmarshal
หัวข้อที่มีชื่อว่า “เส้นทาง marshal/unmarshal”flowchart LR GoValue["Go struct"] -- "json.Marshal" --> Bytes["[]byte JSON"] Bytes -- "json.Unmarshal" --> GoValue2["Go struct"] GoValue -- "MarshalJSON()" --> Custom["custom encoding"] Bytes -- "json.NewDecoder(r)" --> Dec["Decoder"] Dec -- "Decode() / More()" --> Stream["one value at a time"] Bytes -- "into map[string]any" --> Any["float64 / json.Number"]
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| json.Marshal/Unmarshal | ง่าย, ไม่ต้อง schema | reflect-based — ช้ากว่า code-gen 2-5x |
| struct tags (json:“name”) | control field names, omitempty | runtime-only — ไม่มี compile-time check ว่า tag ถูกต้อง |
| json.Decoder (streaming) | decode large/stream JSON โดยไม่ load ทั้งหมดใน memory | API ซับซ้อนกว่า Unmarshal |
| omitempty | ไม่ส่ง zero value ใน JSON | 0, false, "" ถือเป็น empty — อาจ surprise |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- json.Marshal/Unmarshal thread-safe — ใช่ — แต่ json.Decoder/Encoder แต่ละ instance ไม่ thread-safe อย่าแชร์ข้าม goroutine
- unexported field ถูก marshal — ไม่ — unexported field ถูก ignore โดย encoding/json ไม่ว่าจะมี tag หรือไม่
- omitempty ทำงานกับ pointer เหมือน value — omitempty บน *T: nil → omit, non-nil → include แม้ว่า value ข้างใน pointer เป็น zero
- field ที่เป็น any ใน struct marshal เป็น null เสมอ — marshal ตาม concrete type ที่ store อยู่ใน interface ตอน runtime
💡 ตัวอย่างจากของจริง
Kubernetes API: ทุก Kubernetes resource (Pod, Deployment, Service) marshal/unmarshal ผ่าน encoding/json — struct tags ควบคุม JSON key ทั้งหมด
Go REST API: json.NewDecoder(r.Body).Decode(&req) คือ pattern มาตรฐานที่ทุก Go API server ใช้สำหรับ parse request body
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”โค้ด encoding/json ที่ AI เขียนมักดูครบถ้วนแต่แอบสมมติ schema ที่ไม่เคยตรวจสอบจริง เช็กสิ่งเหล่านี้ก่อนเชื่อ:
- field ที่ต้องปรากฏใน JSON ขึ้นต้นด้วยตัวพิมพ์ใหญ่หรือไม่? field ตัวพิมพ์เล็กจะไม่ serialize เลยเงียบๆ โดยไม่มี error จาก
MarshalหรือUnmarshal - มีการ type-assert ค่า
anyที่ decode มาตรงๆ เป็นint,int64หรือ numeric type อื่นหรือไม่? ตัวเลข JSON ทุกตัว decode เป็นfloat64เว้นแต่ target เป็น struct field ที่เจาะจงหรือตั้งUseNumber()ไว้ - tag
omitemptyอยู่บน field ที่เป็นstructหรือtime.Timeแล้วสมมติว่ามันจะถูกละเว้นเมื่อเป็น zero หรือไม่? มันจะไม่ถูกละเว้น — มีแค่omitzero(Go 1.24) เท่านั้นที่ทำแบบนั้น - field ผลลัพธ์ที่เป็น
[]Tถูกปล่อยเป็น nil เมื่อผลลัพธ์ว่างหรือไม่ ในขณะที่ผู้ใช้ API คาดหวัง[]ไม่ใช่null? MarshalJSONแบบ custom เรียกjson.Marshalบนn(receiver) ตรงๆ แทนที่จะเป็น alias type หรือไม่ — นี่คือ infinite recursion ที่จะโผล่ตอน runtime เท่านั้น?- โค้ดที่ต้องปฏิเสธ field ที่ไม่คาดคิดใช้
Unmarshalธรรมดาแทนที่จะใช้Decoderพร้อมDisallowUnknownFields()หรือไม่?
🐛 หา bug ในโค้ดนี้
func parseCount(body []byte) (int, error) { var v map[string]any if err := json.Unmarshal(body, &v); err != nil { return 0, err } count := v["count"].(int) // ??? return count, nil}เฉลย
json.Unmarshal ลงใน map[string]any จะแปลงตัวเลข JSON ทุกตัวเป็น float64 ไม่เคยเป็น int — ดังนั้น v["count"].(int) จะ panic กับ input ที่ถูกต้องทุกตัว:
recovered: interface conversion: interface {} is float64, not intแก้ได้ด้วย type assertion ที่ถูกต้องพร้อมแปลง type อย่างชัดเจน หรือหลีกเลี่ยง map ทั้งหมดโดย decode ลงใน struct ที่มี field Count int ที่กำหนด type ไว้ ให้ encoding/json แปลงตัวเลขให้เอง:
count, ok := v["count"].(float64)if !ok { return 0, fmt.Errorf("count: not a number")}return int(count), nil