Menu

Golang JSON:Marshal、Unmarshal 与结构体标签

如何用 encoding/json 在 Go 中编码和解码 JSON:Marshal 和 Unmarshal,omitempty、omitzero 等结构体标签,格式化输出,解码到 map[string]any,拒绝未知字段,以及用 Decoder 处理数据流。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

Marshal 与 Unmarshal

json.Marshal 把 Go 值转换成 JSON 字节。json.Unmarshal 用 JSON 字节填充一个 Go 值。结构体标签决定 JSON 中的字段名。

第一次输出中没有 Email,因为它带有 omitempty,而值是空字符串。Unmarshal 需要一个指针(&back);直接传入值本身会返回 InvalidUnmarshalError。

结构体标签

标签的语法是 json:"name,option,option"。

标签效果
json:"user_id"使用 user_id 作为键名
json:"email,omitempty"值为 false、0、""、nil 或空切片、空 map 时省略
json:",omitzero"(Go 1.24)值为零值,或其 IsZero() 返回 true 时省略
json:"-"永远不编码或解码这个字段
json:"-,"使用字面量 - 作为键名
json:"count,string"把数字或布尔值编码为 JSON 字符串("42")
无标签键名就是 Go 字段名,如 UserID

密码永远不会离开结构体,余额带上了引号,deleted_at 消失了,而 created_at 打印出零值时间 0001-01-01T00:00:00Z。这个差别正是新增 omitzero 的原因:omitempty 从来都对结构体无效,在 time.Time 上更是长期让人意外。在 Go 1.23 及更早的版本中,可以用带 omitempty 的 *time.Time 达到同样的效果。

MarshalIndent(v, prefix, indent) 生成易读的输出。它适合用于配置文件和调试;API 通常发送紧凑的 JSON。

只处理导出字段

encoding/json 通过反射工作,只能看到导出的字段。这是 Go 中最常见的 JSON bug:

type point struct {
	x, y int // lowercase: json.Marshal(point{1, 2}) gives {}
}

没有错误,也没有警告,编码时只得到 {},解码时字段保持零值。把字段名首字母大写,再为小写键名加上标签。

解码时如何匹配字段

解码到结构体时:

  • 键会与标签名或字段名进行不区分大小写的匹配。{"NAME": "x"} 会填充标签为 json:"name" 的字段。
  • 没有对应字段的键会被悄悄忽略。
  • 没有对应键的字段会保留原来的值。Unmarshal 不会重置它们,所以解码到一个已有数据的结构体时,结果是合并进去的。
  • 类型不匹配(结构体中是 int,JSON 里却是字符串)会返回 *json.UnmarshalTypeError,但其他字段仍然会被填充。

如果要拒绝意料之外的键,比如在严格的 API 中,或者在可能有拼写错误的配置文件中,就使用带 DisallowUnknownFields 的 Decoder:

结构未知:map[string]any

如果事先不知道数据的结构,就解码到 map[string]any 或 any 中。对应关系是固定的:

JSONGo
对象map[string]any
数组[]any
字符串string
数字float64
true / falsebool
nullnil

输出中可以看到两个陷阱。所有数字都是 float64,所以 m["stock"].(int) 会失败。而且大于 2^53 的整数存成 float64 时会丢失精度:id 9007199254740993 读回来变成了 ...992。UseNumber 可以同时避免这两个问题。

如果你知道部分结构,就把这部分解码到结构体中,其余部分使用 json.RawMessage。它保存字段的原始字节,等你知道它的类型后再解码。

切片、map、指针与 nil

Go 值JSON
nil 切片(var s []int)null
空切片([]int{})[]
nil mapnull
nil 指针null
[]bytebase64 字符串
map[string]T对象,键会被排序
map[int]T对象,整数键转成字符串

对于期望得到数组的 API 客户端来说,nil 切片和空切片的区别很重要。当字段必须是 [] 时,用 []T{} 或 make([]T, 0) 初始化。

如果需要在输入中区分“缺失”和“零值”,就使用指针字段(*int、*bool)。解码后,nil 指针表示这个键不存在或为 null;指向 0 的指针表示客户端发送的就是 0。

数据流:Encoder 与 Decoder

json.Marshal 和 Unmarshal 处理的是完整的字节切片。对于 io.Reader 或 io.Writer(HTTP 请求体、文件、标准输入),使用 json.NewDecoder 和 json.NewEncoder。Decoder 还可以逐个读取一连串 JSON 值:

Encoder.Encode 在每个值后面写一个换行符。默认情况下,Marshal 和 Encoder 都会把 \u003c、\u003e 和 \u0026 转义成 \u003c、\u003e 和 \u0026,这样 JSON 可以安全地嵌入 HTML。SetEscapeHTML(false) 可以关闭这一行为。在 HTTP 处理器中,json.NewDecoder(r.Body).Decode(&v) 和 json.NewEncoder(w).Encode(v) 是常用的一对。

自定义编码

一个类型可以通过实现 json.Marshaler 和 json.Unmarshaler 来控制自己的 JSON 形式。常见的例子是一个 iota 枚举,它应该显示为名字,而不是数字:

MarshalJSON 使用值接收者,所以对值和指针都有效;UnmarshalJSON 需要指针接收者,因为它要修改值。如果类型只需要字符串形式,可以改为实现 encoding.TextMarshaler(MarshalText),这样它还能用作 map 的键。

常见错误

  • 字段名小写。 它们会被悄悄跳过。
  • 把值传给 Unmarshal。 它需要指针。
  • 忽略错误。 格式错误的 JSON 和类型不匹配只会通过错误报告出来。
  • 以为 map[string]any 中的数字是 int。 它们是 float64。
  • 指望 omitempty 省略空结构体或零值 time.Time。 在 Go 1.24 上使用 omitzero,或者使用指针。
  • 依赖 map 中的字段顺序。 map 的键在输出时会排序;结构体字段则保持声明时的顺序。

常见问题

Go 语言怎么把结构体转成 JSON?

调用 json.Marshal(v),它返回 []byte 和一个 error。只有导出的字段(名字以大写字母开头)才会被包含进去。在字段上使用 json:"name" 这样的结构体标签,可以控制它的 JSON 键名。要输出带缩进的结果,使用 json.MarshalIndent(v, "", " ")。

为什么我的结构体字段没有出现在 JSON 输出里?

encoding/json 只能看到导出的字段。名为 name(小写)的字段对它来说是不可见的,编码和解码时都是如此。把字段名首字母大写,再用 json:"name" 这样的标签设置 JSON 键名。

Go JSON 中的 omitempty 有什么作用?

当字段保存的是空值时,omitempty 会把它从输出中省略:false、0、""、nil 指针或接口,以及空的切片或 map。它不会把结构体或 time.Time 视为空。Go 1.24 新增了 omitzero,它会省略任何等于其类型零值的值(或者 IsZero() 方法返回 true 的值),包括结构体和 time.Time。

Go 语言怎么解析结构未知的 JSON?

解码到 map[string]any(或 any)中。对象会变成 map[string]any,数组变成 []any,字符串变成 string,布尔值变成 bool,而所有数字都变成 float64。用类型断言读取这些值;如果大整数必须保持精确,就使用 Decoder.UseNumber。

Coddy 编程语言插图

和 Coddy 一起学编程

开始