Go 语言 JSON 解析怎么写?附 3 种常见方式一次讲清

编程狮(w3cschool.cn) 2026-09-01 15:58:02 浏览数 (35)
反馈

Go 做 JSON 解析,最常用的是 struct 配合 json.Unmarshal:先定义结构体,用 JSON tag 把字段和 JSON 键对应上,再一行反序列化拿到结果。本文讲清 Go 语言 JSON 解析的 3 种主流做法:结构体反序列化、map 动态解析、以及 json.Decoder 流式读取。你还会看到「字段名对不上」「omitempty 吞掉空值」「数字默认变 float64」这些真实项目里高频踩的坑怎么填。读完你不仅能把数据解析进 Go,还知道每种写法该用在哪个场景。今天编程狮就把这块讲透。

Go JSON 解析方法图

一、为什么需要 JSON 解析

Go 写服务端、调接口、读配置文件,几乎每天都和 JSON 打交道。所谓 JSON 解析,本质是「把一段文本,变成内存里结构化的 Go 值」。最常见的来源有三个:HTTP 接口返回的响应体、消息队列里的消息体,还有本地配置文件。

⚠️ 注意:Go 的标准库把「文本转 Go 值」叫 unmarshal(反序列化),把「Go 值转文本」叫 marshal(序列化)。别把方向搞反,否则编译期不报错,运行时才莫名其妙拿到空数据。

选哪种解析方式,先看你的数据「长什么样、稳不稳定」。如果字段固定、结构清晰,用结构体最省心;如果字段名、结构都不确定,比如要兼容第三方千奇百怪的返回,那就得上 map 动态解析;如果数据量很大、或者来自网络流,那就得用流式 decoder 边读边解析。下面逐个来看,并通过 Go 语言基础教程 先补一下语法基础。

二、方法一:struct + json.Unmarshal(最推荐)

适合谁:字段固定、结构清晰、要强类型校验。代价:得先定义结构体,字段多的时候有点啰嗦。

type User struct {
    Name  string `json:"name"`
    Age   int    `json:"age"`
    Email string `json:"email"`
}


func main() {
    data := []byte(`{"name":"小明","age":18,"email":"x@w3cschool.cn"}`)
    var u User
    err := json.Unmarshal(data, &u)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%+v\n", u) // {Name:小明 Age:18 Email:x@w3cschool.cn}
}

json.Unmarshal 接收字节切片和目标结构体指针,按 JSON tag 把键映射到字段。字段名对不上时,靠 json:"xxx" 这个 tag 来指定映射关系,否则 Go 会按「大小写不敏感的同名」去匹配,容易静默失败。

做 JSON 解析时,结构体的好处是类型安全:年龄直接是 int,不用自己做类型转换,编译器还会帮你拦住明显错误。当接口字段很多时,手写结构体麻烦,但可以借助工具一键生成。它和 encoding/json 这套标准库配合最稳,生产环境基本都走这条路。一个常见误用是忘记传指针——json.Unmarshal(data, u) 少写了 &,结果改的是副本,外面读到的还是零值,排查起来很费劲。

三、方法二:map + 动态解析

适合谁:字段名、结构都不确定,要先「看一眼」再决定怎么处理。代价:拿到的是 interface{},用之前得自己做类型断言。

func main() {
    data := []byte(`{"name":"小红","score":95.5,"tags":["a","b"]}`)
    var m map[string]interface{}
    json.Unmarshal(data, &m)
    fmt.Println(m["name"]) // 小红
    score := m["score"].(float64)
    fmt.Println(score) // 95.5
}

map[string]interface{} 能接住任意结构的 JSON,因为值被存成了 interface{}。但代价是:数字默认解析成 float64,不是整数 int;想拿到具体类型,得配合类型断言,断言错了会直接 panic。

动态 JSON 解析在「对接不可控的第三方接口」时很好用——你不知道对方哪天加字段、改类型,用 map 先收下来再逐步提取,比频繁改结构体灵活。但它也有天生短板:没有编译期保护,字段拼错只会得到 nil,不会报错。所以凡是结构稳定的内部接口,优先用结构体;只有结构真不确定的时候,才考虑用动态思路兜底。类型断言前最好用 v, ok := m["x"].(float64) 的写法,避免 panic 拖垮整个请求。想看它在 Web 框架里怎么落地,可参考 Gin 框架实战教程 的示例。

四、方法三:流式 decoder

适合谁:数据来自大文件、HTTP 请求体、或要逐条处理数组。代价:写法比 Unmarshal 略多几行。

func main() {
    r := strings.NewReader(`{"name":"小刚","age":20}`)
    dec := json.NewDecoder(r)
    var u User
    for dec.More() {
        dec.Decode(&u)
        fmt.Printf("%+v\n", u)
    }
}

json.NewDecoder 包着一个 io.Reader,可以边读边解析,不用把整个 JSON 读进内存。这对「超大 JSON 数组、网络流、逐条消费」场景特别关键——比如一个几十 MB 的日志文件,用 Unmarshal 一次性读会吃光内存,用 Decoder 能一条条吐出来。

流式 JSON 解析还支持边读边校验:每 Decode 一次拿到一个对象,你可以在循环里直接做业务处理或落库,处理完就丢,内存占用恒定。另外 Decoder 配合 DisallowUnknownFields() 还能拒绝结构体里没定义的字段,帮你守住接口契约。当数据来自不可信的外部输入、又不想为未知字段背锅时,这一招很实用。流式解析把内存压力从「一次性」摊薄到「逐条」,是大文件场景的首选。

五、三种方式怎么选 + 高频坑

怎么选,先记住一张表的逻辑:结构固定用结构体,结构未知用 map,体量很大用 Decoder。 下面三个坑,是真实项目里翻车最多的地方。

  • 字段名对不上:JSON 里是 user_name,结构体字段叫 UserName,没写 tag 就会解析成零值。解决:老老实实写 json:"user_name"。字段名映射靠 tag 完成,别指望 Go 自动猜。
  • omitempty 吞空值:结构体字段带 json:"email,omitempty",当 email 是空字符串时,序列化输出会直接丢掉这个键。这在「前端靠字段存在与否判断」的场景会出 bug。是否用 omitempty 要想清楚,别无脑照抄。
  • 数字变 float64:map 动态解析时,所有数字默认是 float641 会变成 1.0,和前端对不上;想保留整数类型,要么用结构体指定 int,要么上 json.DecoderUseNumber()

再补两个边角坑:第一,结构体字段必须导出(首字母大写),小写字段 json.Unmarshal 会直接跳过且不报错,是新手最常见的静默失败;第二,时间字段默认走 RFC3339,格式对不上要用 time.Time 自定义解析。把这三类的边界厘清,Go 的 JSON 解析基本不会再让你半夜爬起来查 bug。

Go JSON 解析怎么选

总结

Go 做 JSON 解析,日常首选 struct + json.Unmarshal,类型安全、好维护;字段结构不确定时,用 map[string]interface{} 动态接住,再配合类型断言提取;数据体量很大或来自网络流,用 json.Decoder 流式解析,内存恒定。要点带走:

  • 字段名映射靠 json:"xxx" tag,别靠 Go 自动同名匹配;
  • omitempty 会吞掉空值字段,按需使用别滥用;
  • map 解析数字默认是 float64,要整数就用结构体或 UseNumber。

延伸学习

想把 Go 的 JSON 处理系统补齐,可以按这个顺序来:

  1. 先过一遍 FastJSON 教程速查,看看另一种 JSON 处理思路;
  2. 想看另一种讲法,参考 JSON 解析实战笔记 的踩坑总结;
  3. 写完想立刻试,在线运行 Go 代码 免安装跑片段。

常见问题

Q:字段名和 JSON 里的键不一致怎么办?

A:在结构体字段后面加 JSON tag 来指定映射,例如 Name string \json:"user_name"``。如果不写 tag,Go 只会按「大小写不敏感的同名」去匹配,键名对不上时字段会被静默置为零值,不报错但数据全空,所以和第三方接口对接时建议每个字段都显式写 tag。

Q:用 map 解析时数字为什么变成了小数?

A:因为 map[string]interface{} 里的值默认是 interface{},标准库会把所有 JSON 数字都解析成 float64,于是 1 变成 1.0。如果需要保留整数类型,优先用结构体把字段声明成 int,或者在 Decoder 上调用 UseNumber(),让数字以 json.Number 形式保留原样再自行转换。

Q:超大 JSON 文件一次性 Unmarshal 会怎样?

A:会把整个文件读进内存再解析,文件几十 MB 以上时极易触发 OOM 或让服务卡顿。json.Decoder 包着 io.Reader,可以边读边解析、逐条 Decode,内存占用保持恒定。所以面对大文件、HTTP 请求体、消息流这类场景,流式 decoder 才是稳妥选择。

0 人点赞