深色模式
Go 读取 JSON
概述
Go 里读 JSON 的路子不止一条。很多文章一上来就讲“怎么把字符串转成结构体”,这当然没错,但一到真实项目里,很快又会碰到另外几类需求:请求体很大、只想拿几个字段、数组要边读边处理、某些字段结构还不固定。
所以这篇文章不把 JSON 解析写成单一技巧,而是按常见场景拆开:简单结构体、流式读取、部分字段提取,以及更偏底层的手动 pull 解析。
大多数场景先解到结构体
如果 JSON 结构稳定,优先解到结构体。代码最直观,也最容易维护。
go
package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
data := []byte(`{"id":1,"name":"Tom","age":18}`)
var user User
if err := json.Unmarshal(data, &user); err != nil {
panic(err)
}
fmt.Printf("%+v\n", user)
}这种方式最适合:
- 接口返回结构稳定
- 需要完整读取数据
- 更看重可读性而不是极致控制
如果字段很多,但当前逻辑只关心其中几个,也可以只定义自己真正需要的字段,不必为了“完整”把整个响应都铺出来。
请求体和大文件更适合 json.Decoder
当数据来自 io.Reader,例如 HTTP 请求体、文件流、长连接数据流,直接用 json.NewDecoder 会更顺手。
go
func decodeRequestBody(r io.Reader) (CreateUserRequest, error) {
var req CreateUserRequest
dec := json.NewDecoder(r)
dec.DisallowUnknownFields()
if err := dec.Decode(&req); err != nil {
return CreateUserRequest{}, err
}
return req, nil
}这里有两个细节很实用:
Decoder直接面对流,不需要先把整个内容读到内存里。DisallowUnknownFields能提前拦住多余字段,接口校验会更严格。
数组数据可以边解边处理
如果 JSON 顶层是数组,而且元素很多,没必要一次性全塞进切片。可以流式逐个解码。
go
package main
import (
"encoding/json"
"fmt"
"os"
)
type Item struct {
Name string `json:"name"`
Price float64 `json:"price"`
}
func readItems(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
dec := json.NewDecoder(f)
token, err := dec.Token()
if err != nil {
return err
}
if token != json.Delim('[') {
return fmt.Errorf("expected array")
}
for dec.More() {
var item Item
if err := dec.Decode(&item); err != nil {
return err
}
// 在这里逐条处理 item
}
_, err = dec.Token()
return err
}这种写法比“全部读完再统一处理”更适合大文件或批量数据。
只关心部分字段时,不一定非要上 map[string]any
很多人一看到“字段不固定”,第一反应就是反序列化到 map[string]any。这能用,但通常不是第一选择。
更稳的顺序通常是:
- 能用结构体就先用结构体。
- 某些字段结构不固定时,再引入
json.RawMessage。 - 只有在字段名本身也不固定时,再考虑
map[string]any。
例如:
go
type Event struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"`
}先把外层结构解出来,再根据 Type 决定怎么解 Payload,这通常比满篇类型断言干净得多。
什么时候才需要手动 pull 解析
只有在下面这些场景里,才值得往更底层走:
- JSON 很大,只想拿极少数字段
- 需要边扫边跳过无关字段
- 结构半稳定,但不想把所有层级都定义成结构体
这时可以考虑 jsoniter 一类库的手动读取方式。
go
package main
import (
"fmt"
jsoniter "github.com/json-iterator/go"
)
func main() {
iter := jsoniter.ParseString(jsoniter.ConfigDefault, `
{
"name": "Cake",
"price": 18.8,
"weight": 150
}
`)
for field := iter.ReadObject(); field != ""; field = iter.ReadObject() {
switch field {
case "name":
fmt.Println("name:", iter.ReadString())
case "price":
fmt.Println("price:", iter.ReadFloat64())
default:
iter.Skip()
}
}
}这种写法的特点很明确:
- 控制力强
- 只读自己关心的字段
- 代码可读性比结构体解码差一些
所以它更像性能和控制需求下的专用方案,而不是默认方案。
选择路线时可以这样判断
- 接口结构稳定:优先
struct + json.Unmarshal - 数据来自流:优先
json.Decoder - 只想延后解析某个字段:优先
json.RawMessage - 文档很大且只取局部:再考虑手动 pull 解析
先把简单路径走通,通常比一上来就追求“最灵活”更稳。JSON 解析的复杂度,往往不是输在库能力,而是输在把简单场景也做成了动态结构。
