Go 语言 encoding/json 标准库深度解析:从 Tag 反射到流式处理

适用版本:Go 1.16 ~ 1.24(文中标注各版本行为差异)

1. 背景

1.1 为什么 JSON 是 Go 生态的"头号公民"数据格式

在 Go 生态中,JSON 的使用频率远超其他序列化格式,几乎每一个网络服务、配置加载、数据交换场景都离不开它。原因有三:

  • HTTP/REST 生态绑定:Go 标准库 net/http(见第 93 篇)以 JSON 作为天然的数据交换格式,encoding/json 与 net/http 同属标准库,开箱即用、零第三方依赖。
  • Web 框架统一心智:Gin(第 97 篇)的 c.ShouldBindJSON、c.JSON,FastAPI(第 96 篇)的 response_model,底层都是同一套 JSON 编解码语义。
  • 结构体优先的静态类型哲学 :Go 是强类型语言,encoding/json 的设计核心是结构体 Tag 驱动的反射映射,与 Python 的 dict 自由字典、C++ 的手写序列化完全不同------它是"编译期类型 + 运行时反射"的折中产物。

1.2 Go JSON 设计的三个关键决策

决策 具体表现 带来的影响
Tag 声明式映射 json:"name,omitempty" 字段名与 JSON 键解耦,声明式可读性强
反射驱动 Marshal/Unmarshal 通过 reflect 遍历结构体 通用性强但性能弱于代码生成方案
接口扩展点 Marshaler / Unmarshaler / TextMarshaler 自定义类型可完全接管自己的序列化逻辑

1.3 为什么单独写这一篇

本系列已有 reflect 篇(第 119 篇)从反射 API 角度剖析动态类型,GORM 篇(第 102 篇)从 ORM 角度看 Tag 应用,zap 篇(第 109 篇)从日志角度谈强类型字段。但 encoding/json 作为 Go 标准库中反射的最大消费者、Tag 约定的标准制定者、以及每个 Go 工程师每天都会触碰的包,其 API 全景、底层映射规则、流式处理模式与高频坑点值得一次系统性的专门梳理。本篇就是这条主线。


2. 核心概念

2.1 JSON 数据类型与 Go 类型映射表

JSON 类型 Go 接收类型(Unmarshal 时) Go 输出类型(Marshal 时)
null nil(指针/接口/map/slice 置零) nil 指针/接口/map/slice
true / false bool bool
数字 float64(默认)/ json.Number / 具体整型浮点型 int/float64 等所有数值类型
字符串 string / \[\]byte string
数组 \[\]T / NT slice / array
对象 mapstringT / struct map / struct

2.2 结构体 Tag 语法

Go 复制代码
type Field struct {
    Name string `json:"name"`                 // 键名映射
    Skip string `json:"-"`                    // 完全忽略
    Opt  string `json:"opt,omitempty"`        // 零值时省略
    Str  string `json:"str,string"`           // 强制编码为 JSON 字符串(引号包裹)
    NoEscape string `json:"noescape"`         // 默认键名 = 字段名
}

Tag 完整语法:json:"<键名>,<选项1>,<选项2>"

选项 作用 注意
键名 指定 JSON 中的键名 空串则使用字段名;- 表示忽略
omitempty 零值(false/0/""/nil/空 slice/map/长度为 0 的数组)时省略字段 自定义 struct 零值不省略,除非实现 IsZero() 或指针
string 强制把该字段编码为 JSON 字符串(如 "42") 只对数字、bool、string 有效;解码时同样接受字符串形式的数字

2.3 字段选取规则(Marshal/Unmarshal 共用)

  1. 只处理导出字段(首字母大写)。
  2. 字段名大小写不敏感匹配:Name 可匹配 "name" / "Name" / "NAME",优先精确匹配。
  3. Tag 键名精确匹配优先于字段名模糊匹配。
  4. 匿名嵌入字段(embedded struct)默认平铺展开其字段;除非该嵌入字段自身有 Tag 或实现了 Marshaler。
  5. 同一层级多个字段匹配同一键时,深度优先、Tag 优先 ;同级字段声明靠后者胜出(后写覆盖先写)。

2.4 零值 vs 缺省:解码到结构体的填充语义

Unmarshal 不会把目标结构体清零,而是增量填充:JSON 中出现的键覆盖对应字段,未出现的键保留目标原有值。这在复用缓冲结构体时是特性(可做局部更新),也常是坑(见 §6.15)。


3. API 说明

3.1 顶层函数(一次性编解码)

函数 签名 说明
Marshal func Marshal(v any) (\[\]byte, error) 编码为紧凑 JSON(无缩进、无换行)
MarshalIndent func MarshalIndent(v any, prefix, indent string) (\[\]byte, error) 编码并美化缩进
Unmarshal func Unmarshal(data \[\]byte, v any) error 解码到 v(必须为指针)
Valid func Valid(data \[\]byte) bool 校验是否为合法 JSON,不做解码
Compact func Compact(dst *bytes.Buffer, src \[\]byte) error 压缩去空白
Indent func Indent(dst *bytes.Buffer, src \[\]byte, prefix, indent string) error 美化缩进
HTMLEscape func HTMLEscape(dst *bytes.Buffer, src \[\]byte) 转义 <、>、&、U+2028、U+2029

3.2 流式 API(大文件/网络流)

类型 核心方法 适用场景
json.Encoder NewEncoder(w io.Writer) / Encode(v) / SetIndent / SetEscapeHTML 逐条输出到 http.ResponseWriter、文件、网络连接
json.Decoder NewDecoder(r io.Reader) / Decode(v) / More() / Token() / UseNumber() / DisallowUnknownFields() 逐条从流中读取,处理 NDJSON / 未知字段严格模式

3.3 特殊类型与接口

类型/接口 作用
json.Number 字符串形式的数字,避免 float64 精度丢失;需配合 Decoder.UseNumber() 或结构体字段声明
json.RawMessage 延迟解析:先保留原始 JSON 字节,后续再决定如何处理
json.Marshaler 接口 MarshalJSON() (\[\]byte, error),自定义编码
json.Unmarshaler 接口 UnmarshalJSON(\[\]byte) error,自定义解码
encoding.TextMarshaler / TextUnmarshaler 若类型未实现 JSON 接口,则退化尝试文本接口(如 time.Time)
json.MarshalerError Marshal 过程中的包装错误
json.SyntaxError 语法错误,含 Offset 字段
json.UnmarshalTypeError 类型不匹配错误,含 Field/Value/Offset 字段
json.InvalidUnmarshalError 传给 Unmarshal 的非指针或 nil 参数

4. 详细使用说明

示例 1:基础 Marshal / Unmarshal(结构体 + Tag)

Go 复制代码
package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type Device struct {
    ID       int      `json:"id"`
    Name     string   `json:"name"`
    Addrs    []string `json:"addrs,omitempty"`
    Active   bool     `json:"active"`
    Secret   string   `json:"-"`
    Firmware string   `json:"firmware,string"` // 数字以字符串形式传输
}

func main() {
    d := Device{
        ID: 101, Name: "CNC-Lathe-01", Active: true,
        Secret: "do-not-serialize", Firmware: "3.2.1",
    }
    b, err := json.Marshal(d)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(b))
    // {"active":true,"firmware":"3.2.1","id":101,"name":"CNC-Lathe-01"}
    // 注意:Secret 被忽略,Addrs 为空被 omitempty 省略,字段按字典序输出

    var got Device
    if err := json.Unmarshal(b, &got); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%+v\n", got)
}

要点 :Marshal 输出按字典序排列字段;omitempty 对空 slice 生效;- 直接忽略;string 选项把数值包成字符串。

示例 2:map 与 slice 的编解码

Go 复制代码
func mapDemo() {
    m := map[string]any{
        "points": []int{1, 2, 3},
        "meta":   map[string]string{"k": "v"},
    }
    b, _ := json.Marshal(m)
    fmt.Println(string(b)) // {"meta":{"k":"v"},"points":[1,2,3]}

    var back map[string]any
    _ = json.Unmarshal(b, &back)
    // 注意:back 中的数字是 float64,points 是 []any
    fmt.Printf("%T %v\n", back["points"], back["points"]) // []interface {}
}

要点:解码到 mapstringany 时所有数字变 float64、嵌套对象变 mapstringany------这是新手最常踩的精度坑(见 §6.1)。

示例 3:自定义 MarshalJSON / UnmarshalJSON

Go 复制代码
type SafeString string

func (s SafeString) MarshalJSON() ([]byte, error) {
    // 示例:对内容做 Base64 包装(实际业务常做脱敏/加密)
    return json.Marshal("base64:" + string(s))
}

func (s *SafeString) UnmarshalJSON(b []byte) error {
    var raw string
    if err := json.Unmarshal(b, &raw); err != nil {
        return err
    }
    *s = SafeString(raw) // 实际应做解码
    return nil
}

type Cmd struct {
    Payload SafeString `json:"payload"`
}

func customDemo() {
    c := Cmd{Payload: "M03 S1200"}
    b, _ := json.Marshal(c)
    fmt.Println(string(b)) // {"payload":"base64:M03 S1200"}

    var c2 Cmd
    _ = json.Unmarshal(b, &c2)
    fmt.Println(c2.Payload) // M03 S1200
}

要点:MarshalJSON 返回值必须仍是合法 JSON(通常内部再调 json.Marshal 包装);UnmarshalJSON 接收的是该字段的原始 JSON 片段。

示例 4:流式 Decoder 逐条读取 NDJSON(工业采集场景)

Go 复制代码
func streamDemo(r io.Reader) error {
    dec := json.NewDecoder(r)
    dec.UseNumber() // 保留数字精度
    for dec.More() {
        var record map[string]any
        if err := dec.Decode(&record); err != nil {
            return err
        }
        // 处理单条记录...
        _ = record
    }
    return nil
}

// 配合 http 读取
// resp, _ := http.Get(url)
// defer resp.Body.Close()
// _ = streamDemo(resp.Body)

要点:dec.More() 判断数组内是否还有元素(顶层数组流);UseNumber() 让数字以 json.Number 呈现,避免 float64 精度损失。

示例 5:Encoder 流式写出 + SetIndent 美化

Go 复制代码
func writeNDJSON(w io.Writer, items []Device) error {
    enc := json.NewEncoder(w)
    enc.SetEscapeHTML(false) // 不转义 < > &,省体积
    for _, it := range items {
        if err := enc.Encode(it); err != nil {
            return err
        }
    }
    return nil
}

要点:Encoder.Encode 自带尾部换行,天然适合 NDJSON/日志流;SetEscapeHTML(false) 可提升体积与性能(默认转义 </>/& 以防 XSS)。

示例 6:RawMessage 延迟解析与分发

Go 复制代码
type Message struct {
    Type string          `json:"type"`
    Data json.RawMessage `json:"data"` // 先不解析
}

func rawDemo(b []byte) {
    var m Message
    _ = json.Unmarshal(b, &m)
    switch m.Type {
    case "kafka":
        var k struct{ Topic string `json:"topic"` }
        _ = json.Unmarshal(m.Data, &k)
        fmt.Println("kafka topic:", k.Topic)
    case "mqtt":
        var q struct{ Qos int `json:"qos"` }
        _ = json.Unmarshal(m.Data, &q)
        fmt.Println("mqtt qos:", q.Qos)
    }
}

要点:RawMessage 是"先整体收下、后按需解析"的利器,适合异构消息体/多态分发。

示例 7:json.Number 保精度

Go 复制代码
func numberDemo() {
    data := []byte(`{"val": 12345678901234567890}`)
    var withNum struct {
        Val json.Number `json:"val"`
    }
    _ = json.Unmarshal(data, &withNum)
    fmt.Println(withNum.Val.String()) // 12345678901234567890(无精度丢失)

    var withF float64
    _ = json.Unmarshal(data, &withF) // 只能配合 map/interface,struct 字段是 float64 会精度丢失
    fmt.Println(withF)               // 1.2345678901234567e+19
}

要点:超过 2^53 的整数(如 Kafka offset、设备 ID、时间戳)用 float64 必丢精度;字段声明 json.Number 或调用 UseNumber() 是正解。

示例 8:time.Time 与自定义时间格式

Go 复制代码
type Event struct {
    At time.Time `json:"at"`
}

// time.Time 实现了 MarshalJSON/UnmarshalJSON(RFC3339)
// 若需要自定义格式,可包一层:
type CustomTime time.Time

func (t CustomTime) MarshalJSON() ([]byte, error) {
    return json.Marshal(time.Time(t).Format("2006-01-02 15:04:05"))
}

func (t *CustomTime) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return err
    }
    tt, err := time.Parse("2006-01-02 15:04:05", s)
    if err != nil {
        return err
    }
    *t = CustomTime(tt)
    return nil
}

要点:time.Time 默认 RFC3339(2026-09-28T10:00:00Z);工业场景常用自定义格式,用类型别名 + 自定义 JSON 接口即可。

示例 9:Decoder 严格模式(DisallowUnknownFields)

Go 复制代码
func strictDemo(b []byte) error {
    dec := json.NewDecoder(bytes.NewReader(b))
    dec.DisallowUnknownFields() // JSON 中出现结构体不认识的键 → 报错
    var cfg struct {
        Host string `json:"host"`
        Port int    `json:"port"`
    }
    if err := dec.Decode(&cfg); err != nil {
        return fmt.Errorf("未知配置项: %w", err)
    }
    return nil
}

要点:配置热加载、API 契约校验场景强烈建议开启,能提前暴露"客户端发错字段名"的问题。

示例 10:省略缺失字段与默认值合并

Go 复制代码
type Config struct {
    Host string `json:"host"`
    Port int    `json:"port"`
    Mode string `json:"mode"`
}

func mergeConfig(defaultCfg Config, patch []byte) (Config, error) {
    // 先填默认值,再增量 Unmarshal:未出现的键保持默认
    cfg := defaultCfg
    if err := json.Unmarshal(patch, &cfg); err != nil {
        return cfg, err
    }
    return cfg, nil
}

要点:利用"Unmarshal 不清零、只覆盖出现的键"的特性做配置补丁合并,比先反序列化成 map 再合并优雅得多。


5. 性能优化

5.1 性能瓶颈在哪里

encoding/json 的性能开销主要来自反射:

  • Marshal 时通过 reflect 遍历结构体字段、读取 Tag、装箱基本类型;
  • Unmarshal 时按 JSON 键名在结构体字段中做线性查找(1.16 之前是纯线性,1.17+ 有轻微优化但仍非哈希索引);
  • 每次调用分配大量临时对象。

官方 benchmark(Go 1.21 前后):Marshal 约 100200MB/s,Unmarshal 约 60120MB/s------远慢于 protobuf/flatbuffers(见第 29/63 篇),但对绝大多数服务足够。

5.2 实用优化清单

手段 做法 收益
复用 Encoder/Decoder 长生命周期对象持有 json.Encoder,避免重复创建 减少分配
SetEscapeHTML(false) 不需要 HTML 转义时关闭 编码更快更小
结构体字段按热度排序 高频 JSON 键对应的字段尽量靠前声明(线性查找收益) Unmarshal 微优化
避免 mapstringany 强类型结构体 + 明确的字段类型,避免 float64 装箱与再断言 类型安全 + 性能
使用 json.Number 仅对精度敏感字段 全开会引入字符串解析开销 精度与性能平衡
大对象拆批 流式 Encoder/Decoder 逐条处理,避免一次性大内存 内存峰值下降
预分配 slice make(\[\]T, 0, n) 减少扩容 减少 realloc
终极手段:代码生成 easyjson / ffjson / sonic 见第 7 节

5.3 池化复用模式

Go 复制代码
var bufPool = sync.Pool{New: func() any { return &bytes.Buffer{} }}

func encodeToJSON(v any) ([]byte, error) {
    buf := bufPool.Get().(*bytes.Buffer)
    buf.Reset()
    defer bufPool.Put(buf)
    if err := json.NewEncoder(buf).Encode(v); err != nil {
        return nil, err
    }
    // 去掉 Encode 附加的换行
    return bytes.TrimRight(buf.Bytes(), "\n"), nil
}

注意 :buf.Bytes() 返回的切片在 Put 后会被复用,调用方必须立即拷贝或约定同步消费(典型 use-after-free 陷阱)。


6. 常错点/坑

6.1 数字精度丢失(最高频)

json.Unmarshal 到 mapstringany / any 时,数字一律为 float64,超过 2^53 的整数精度丢失。Kafka offset、雪花 ID、设备序列号、毫秒时间戳全中招。

解法:结构体字段用 int64/uint64/json.Number,或 dec.UseNumber()。

6.2 未导出字段静默忽略

结构体中小写字段(id、name)在 Marshal 时被静默跳过、Unmarshal 时静默不填,不报错。排查半天找不到原因。

解法:字段必须导出;需要 JSON 键名不同就用 Tag。

6.3 Tag 键名拼写错误

json:"namee" 与 json:"name" 写错一个字母,Unmarshal 不报错,字段默默为空。

解法:契约测试 + DisallowUnknownFields + 单测断言关键字段。

6.4 零值被 omitempty 误杀

omitempty 对 false、0、"" 都生效。业务上"显式传了 0 表示关闭"的场景,omitempty 会把它吞掉,导致语义改变。

解法:区分"缺省"与"显式零值"用指针字段(*int),指针非 nil 才序列化。

6.5 自定义 struct 的 omitempty 不生效

Go 复制代码
type Point struct{ X, Y int }
type Shape struct {
    P Point `json:"p,omitempty"` // Point{} 是零值但不省略!
}

omitempty 只认基础零值、nil 指针/接口、空 slice/map/array。struct 零值不触发省略,除非实现 func (p Point) IsZero() bool(Go 1.13+ 支持)。

6.6 MarshalJSON 递归死循环

Go 复制代码
func (t T) MarshalJSON() ([]byte, error) {
    return json.Marshal(t) // 无限递归!Marshal(t) 又调 t.MarshalJSON
}

解法:用类型别名绕开接口:

Go 复制代码
type alias T
func (t T) MarshalJSON() ([]byte, error) {
    return json.Marshal(alias(t))
}

6.7 Unmarshal 传非指针

json.Unmarshal(b, m) 其中 m 是 map(非指针)→ 返回 json.InvalidUnmarshalError;传结构体值(非指针)同样报错。忘记 & 是高频失误。

6.8 Decoder.Decode 忽略结尾多余 JSON

Go 复制代码
dec := json.NewDecoder(r)
dec.Decode(&v) // 只读第一条,后面的 {"x":1} {"y":2} 不管

流里有多条 JSON 时 Decode 只消费一条,循环要用 More() 或持续 Decode 直到 io.EOF。

6.9 Encoder.Encode 自带换行导致拼接错乱

Encode 输出末尾有 \n。多个 Encoder 输出拼接或用 Compact 时会多出换行,bytes.TrimSpace 处理。

6.10 MarshalIndent 的 prefix 参数误解

json.MarshalIndent(v, "", " ") 的第二个参数是每行前缀(通常空串),第三个才是缩进。把缩进写进 prefix 会出现奇怪缩进。

6.11 HTMLEscape 默认转义 < > &

Marshal/Encode 默认把 <、>、& 转成 \u003c 等(防 XSS)。如果目标是存储/传输而非浏览器渲染,会白涨体积。Encoder.SetEscapeHTML(false) 可关。

6.12 大 JSON 一次性 Unmarshal OOM

几十 MB 到 GB 级 JSON(如采集日志、离线数据)直接 json.Unmarshal 会瞬间吃满内存。用 Decoder 流式逐条处理。

6.13 嵌套 mapstringany 的类型断言链

Go 复制代码
m := parsed["data"].(map[string]any)["list"].([]any)[0].(map[string]any)["id"].(float64)

每层都要断言,链式断言一个不对就 panic。解法:能结构体就结构体,不能就用 RawMessage 分层解析。

6.14 结构体字段大小写匹配的隐性规则

Unmarshal 时 "NAME" 也能匹配 Name 字段(大小写不敏感)。这通常无害,但存在歧义字段时(ID 与 Id 同时存在)可能填错字段。

6.15 增量填充导致脏数据

重复 Unmarshal 到同一个结构体时,上次残留的字段值不会被清掉。解法:每次用新结构体,或先清零(v = T{})。

6.16 匿名嵌入字段意外平铺

Go 复制代码
type Base struct{ ID int `json:"id"` }
type Wrap struct {
    Base        // 平铺:JSON 是 {"id":1} 而非 {"base":{"id":1}}
    Name string `json:"name"`
}

想保留嵌套层级需要显式字段名:Base Base \json:"base"``。

6.17 time.Time 解析失败

time.Time 只认 RFC3339。"2026-09-28 10:00:00" 这种格式 Unmarshal 直接报错。自定义格式见示例 8。

6.18 数字字符串混传

JSON 里 "port": "8080"(字符串)但 Go 字段是 int → UnmarshalTypeError。对方接口不规范时用 json.Number + 手动转换,或让对方修契约。

6.19 RawMessage 为 null

json:"data" 的值为 null 时,RawMessage 是 "null" 四个字节,不是 nil。直接 Unmarshal 会得到 nil/零值,注意判空:

Go 复制代码
if len(m.Data) == 0 || string(m.Data) == "null" { /* 处理缺省 */ }

6.20 并发 Marshal/Unmarshal 安全性

encoding/json 的函数级 API 是并发安全的(无共享全局状态),但:

  • 同一个 Encoder/Decoder 不能并发使用(内部有缓冲状态);
  • 共享 sync.Pool 中的 buffer 取用后要立即消费;
  • 结构体 Tag 反射缓存(sync.Map 内部实现)并发安全,无需担心。

6.21 Unmarshal 到接口的 nil 陷阱

Go 复制代码
var v any
json.Unmarshal([]byte("null"), &v) // v 为 nil
json.Unmarshal([]byte(`{}`), &v)   // v 为 map[string]interface{}

空对象与 null 结果不同,v == nil 判断要小心。

6.22 Marshal 循环引用(map/slice 自引用)

map 或 slice 内部引用自身时(m"self" = m),Marshal 直接死循环崩溃。结构体指针环同样问题(a.Next = a)。

解法:业务上避免自引用数据结构进 JSON;必要时手动打断(深度限制)。

6.23 错误处理不检查

Marshal/Unmarshal 的错误在大部分"看起来能过"的场景不会出现,但一旦出现(如 channel/func 字段无法序列化、JSON 语法错误),不检查就会静默失败。生产代码必须检查 err。

6.24 func/channel/complex 类型无法序列化

Marshal 遇到 func、chan、complex 类型字段返回错误(json: unsupported type)。结构体里藏回调函数时尤其隐蔽。

6.25 大整数转字符串再转回

json:"firmware,string" 把 int64 包成字符串;但 -0、超大整数、特殊浮点(NaN/Inf)在 string 模式下会报错或行为异常。

6.26 JSON 键重复

JSON 中同一个键出现两次({"a":1,"a":2}),Unmarshal 以最后一个为准。这在多来源拼接的数据里是隐蔽 bug。


7. 与第三方 JSON 库的选型对照

库 特点 性能 适用场景
encoding/json(标准库) 零依赖、API 稳定、生态兼容 基准 绝大多数业务、库的默认选择
jsoniter(json-iterator/go) 兼容标准库 API、优化迭代器 快 1~2 倍 想无痛提速的存量项目
easyjson 代码生成(模板化),生成专用编解码 快 3~5 倍 高吞吐网关、核心热路径,愿意接受生成代码进仓库
ffjson 代码生成,动态生成反序列化代码 快 2~3 倍 同 easyjson 但维护热度较低
sonic(字节跳动) JIT 汇编优化,零拷贝 快 3~10 倍 极致性能需求,需注意平台兼容(x86-64/arm64)与 Go 版本绑定
gjson 只解析不反序列化,路径查询 快 只读单字段提取(如从大 JSON 取一个值),不做类型映射

选型建议:

  • 追求稳定与生态:标准库足够,结合 §5.2 的优化手段;
  • 热路径吞吐瓶颈:先 profile 确认瓶颈在 JSON,再上 easyjson(生成代码可审查、无运行时魔法);
  • 只读提取:gjson.Get(json, "data.items.0.id") 免反序列化。

8. 总结

  1. Tag 是核心心智:json:"name,omitempty,string" 声明式搞定键名映射、零值省略、字符串化三类需求,是 Go JSON 与其他语言最大的区别。
  2. 精度红线:超过 2^53 的整数必须用 int64/json.Number/UseNumber(),mapstringany 默认 float64 是最大的隐性炸弹。
  3. 流式优先:大 JSON、NDJSON、网络流一律 Decoder/Encoder 流式处理,拒绝一次性 Unmarshal。
  4. 接口扩展:MarshalJSON/UnmarshalJSON/RawMessage 三件套覆盖自定义格式、异构分发、延迟解析三类高级场景。
  5. 性能有上限:标准库反射驱动,热路径要提速就上代码生成(easyjson/sonic),先 profile 再优化。
  6. 26 条坑中最高频的是:数字精度、未导出字段、omitempty 误杀、递归死循环、非指针 Unmarshal------写代码时对照检查一遍。

9. FAQ 速查表

Q1:Unmarshal 到 mapstringany 后数字变 float64,怎么保精度? dec.UseNumber() 后值为 json.Number(字符串形式),转 Int64()/Float64() 可控。

Q2:omitempty 为什么对自定义 struct 不生效? omitempty 只认基础零值/nil/空容器。实现 IsZero() bool 方法(Go 1.13+)即可让 struct 支持。

Q3:JSON 键名大小写不敏感吗? Unmarshal 时大小写不敏感匹配(NAME→Name),Marshal 时严格按 Tag/字段名输出。

Q4:如何忽略某个字段? Tag 写 json:"-"。注意 json:"-" 与 json:"-," 不同,后者键名是 "-"。

Q5:字段是 time.Time,格式不对怎么处理? time.Time 只认 RFC3339。自定义格式用类型别名 + 自定义 MarshalJSON/UnmarshalJSON(见示例 8)。

Q6:如何严格校验 JSON 不含未知字段? dec.DisallowUnknownFields()。

Q7:Marshal 结构体里嵌了 func 字段怎么办? func/chan/complex 无法序列化,会报错。用 json:"-" 忽略或自定义 Marshaler。

Q8:为什么我 Unmarshal 后字段是零值? 检查:字段是否导出、Tag 键名是否拼错、JSON 里键是否存在、是否被增量填充覆盖、UnmarshalTypeError 被忽略。

Q9:大 JSON 怎么处理? 用 Decoder 流式逐条解析,或 gjson 只读提取需要的字段。

Q10:同一个结构体能并发 Marshal 吗? 可以,函数级 API 并发安全;但同一 Encoder/Decoder 实例不能并发使用。

Q11:json.Number 和 string 选项有什么区别? string 选项是"强制字符串化"(输出 "42");json.Number 是"保留数字原文不转 float64"。二者解决的问题不同。

Q12:怎样最快判断一段文本是不是合法 JSON? json.Valid(data),不做解码。

相关推荐
JWASX6 小时前
Java 转 go 学习 - 函数(1)
学习·golang
看浪的路人8 小时前
第7讲:实时告警与自动化响应
开发语言·后端·golang
小小龙学IT8 小时前
Go 语言 database/sql 标准库深度解析:从连接池到驱动抽象
数据库·sql·golang
李游Leo9 小时前
HarmonyOS 7 + Node.js-JSON Schema:审核测试路径与版本事实的一致性门禁【鸿蒙心迹】
node.js·json·harmonyos
wdfk_prog9 小时前
Wi-Fi Direct 源码分析(09):从 P2P_CONNECT 到 GO Negotiation 完成
运维·服务器·ubuntu·golang·asp.net·p2p·wifi-direct
JWASX10 小时前
Java 转 go 学习 - map
学习·golang
福大大架构师每日一题19 小时前
ollama v0.35.0发布:决策模型正式上线,接口直接返回选择、概率与评分
golang
茉莉玫瑰花茶1 天前
GO [ 泛型 ]
golang
念何架构之路1 天前
go-grpc服务端调用
开发语言·后端·golang