Skip to content
Go
encoding/json — JSON 序列化详解

encoding/json — JSON 序列化详解

encoding/json 是 Go 中最常用的编码包。虽然 API 看起来简单,但有许多细节值得深入理解。


1. 基础序列化与反序列化

1.1 结构体标签 (Struct Tags)

type User struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email,omitempty"`      // 零值时忽略
    Password  string    `json:"-"`                     // 永远忽略
    CreatedAt time.Time `json:"created_at,omitempty"`
    Age       int       `json:"age,string"`            // 编码为字符串 "30"
}

🔬 深入原理json:"age,string" 是 Go JSON 标签的一个特殊功能——将数字序列化为 JSON 字符串。这对于 API 返回大整数(超过 JavaScript 安全范围)非常有用。

1.2 Marshal vs MarshalIndent

// 紧凑输出
data, _ := json.Marshal(user) // {"id":1,"name":"Alice"}

// 格式化输出(带缩进)
data, _ := json.MarshalIndent(user, "", "  ")
// {
//   "id": 1,
//   "name": "Alice"
// }

// 更高效的格式化:前缀 + 缩进
data, _ := json.MarshalIndent(user, "> ", "  ")

2. 🔬 json.RawMessage — 延迟解析

json.RawMessage[]byte 的别名,在 JSON 解码时跳过解析,保持原始 JSON 字节。这是实现动态类型接口的核心手段。

type Message struct {
    Type    string          `json:"type"`
    Payload json.RawMessage `json:"payload"` // 暂不解析
}

type UserPayload struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

type OrderPayload struct {
    OrderID int     `json:"order_id"`
    Amount  float64 `json:"amount"`
}

func handleMessage(data []byte) error {
    var msg Message
    if err := json.Unmarshal(data, &msg); err != nil {
        return err
    }

    switch msg.Type {
    case "user":
        var user UserPayload
        return json.Unmarshal(msg.Payload, &user)
    case "order":
        var order OrderPayload
        return json.Unmarshal(msg.Payload, &order)
    default:
        return fmt.Errorf("unknown type: %s", msg.Type)
    }
}

3. json.Number — 避免精度丢失

当 JSON 中的数字不做解析保留为字符串时使用。这是处理大整数(如金融金额)的标准做法。

// 默认行为:大数字会丢失精度
jsonStr := `{"amount": 12345678901234567890}`

// 使用 json.Number
type Transaction struct {
    Amount json.Number `json:"amount"`
}

// 需要配合 UseNumber
decoder := json.NewDecoder(strings.NewReader(jsonStr))
decoder.UseNumber() // ← 关键:让 decoder 把数字保留为 json.Number

var tx Transaction
decoder.Decode(&tx)

// 按需转换
if intVal, err := tx.Amount.Int64(); err == nil {
    fmt.Println(intVal)
} else if floatVal, err := tx.Amount.Float64(); err == nil {
    fmt.Println(floatVal) // fallback
}
// 或保持字符串形式
fmt.Println(tx.Amount.String()) // "12345678901234567890"

🚨 陷阱json.Unmarshal 将 JSON 数字解析为 float64,大于 2^53 的整数会丢失精度。对于 API 响应中的 ID、金额等大整数,务必使用 json.Numberjson:"field,string"


4. 自定义 MarshalJSON / UnmarshalJSON

4.1 自定义时间格式

type MyTime struct {
    time.Time
}

const myTimeFormat = "2006-01-02 15:04:05"

func (t MyTime) MarshalJSON() ([]byte, error) {
    s := t.Format(myTimeFormat)
    return []byte(`"` + s + `"`), nil // 注意:输出必须是带引号的 JSON 字符串
}

func (t *MyTime) UnmarshalJSON(b []byte) error {
    s := strings.Trim(string(b), `"`)
    parsed, err := time.Parse(myTimeFormat, s)
    if err != nil {
        return err
    }
    t.Time = parsed
    return nil
}

🚨 关键细节MarshalJSON 返回的是 合法的 JSON 片段。对于字符串值,返回的字节必须包含双引号。可以用 json.Marshal(s) 简化。

4.2 自定义枚举

type Status int

const (
    StatusActive   Status = iota // 0
    StatusInactive               // 1
    StatusBanned                 // 2
)

func (s Status) MarshalJSON() ([]byte, error) {
    var str string
    switch s {
    case StatusActive:
        str = "active"
    case StatusInactive:
        str = "inactive"
    case StatusBanned:
        str = "banned"
    default:
        str = "unknown"
    }
    return json.Marshal(str)
}

func (s *Status) UnmarshalJSON(b []byte) error {
    var str string
    json.Unmarshal(b, &str)
    switch str {
    case "active":
        *s = StatusActive
    case "inactive":
        *s = StatusInactive
    case "banned":
        *s = StatusBanned
    }
    return nil
}

5. 流式编解码(处理大文件)

5.1 逐条解码 JSON 数组

func decodeLargeFile(filename string) error {
    file, _ := os.Open(filename)
    defer file.Close()

    decoder := json.NewDecoder(file)

    // 读取数组开始的 '['
    if t, err := decoder.Token(); err != nil || t != json.Delim('[') {
        return fmt.Errorf("expected JSON array")
    }

    // 逐个处理元素
    for decoder.More() {
        var item Item
        if err := decoder.Decode(&item); err != nil {
            return err
        }
        process(item)
    }

    // 读取数组结束的 ']'
    if t, err := decoder.Token(); err != nil || t != json.Delim(']') {
        return fmt.Errorf("expected end of array")
    }
    return nil
}

6. 🚨 JSON 常见陷阱

陷阱 1:HTML 转义

Go 的 json.Marshal 默认会对 <>& 三个字符进行 HTML 转义,这是为了防止 JSON 被嵌入到 HTML <script> 标签时引发 XSS 攻击。

type Data struct {
    Content string `json:"content"`
}

d := Data{Content: "<script>alert('xss')</script>"}
data, _ := json.Marshal(d)
fmt.Println(string(data))
// 默认输出:{"content":"<script>alert('xss')</script>"}
//          ↑ 注意 < 和 > 被转义成了 Unicode 转义序列 < 和 >
//          字符串中实际存储的是这些转义字符,JSON 文本中没有 < 和 > 出现
//          这样做是为了安全:当 JSON 嵌在 HTML 的 <script> 标签中时,
//          "</script>" 序列不会意外关闭 script 标签

// 禁用 HTML 转义(当你确定 JSON 不会被嵌入 HTML 时)
buf := &bytes.Buffer{}
encoder := json.NewEncoder(buf)
encoder.SetEscapeHTML(false)
encoder.Encode(d)
fmt.Println(buf.String())
// 输出:{"content":"<script>alert('xss')</script>"}
//       ↑ 保留了原始的 < 和 > 字符,JSON 文本更短,但不适合嵌入 HTML

🔬 两个输出的区别

  • 默认输出<<>>&&。JSON 文本中不存在 </>/& 字面量,安全嵌入 HTML。
  • 禁用转义后:保留 <>& 原样。JSON 文本更短更易读,但嵌入 <script> 标签时可能被浏览器解析为 HTML。
  • 如果你使用 HTTPS API(Content-Type: application/json)而非嵌入 HTML,可以安全地禁用转义以减小编码体积。

💡 选择指南

  • REST API 返回 JSON → 可以禁用转义(SetEscapeHTML(false)),因为 JSON 不会被嵌入 HTML。
  • JSON 作为服务端渲染模板数据 → 保持默认转义,防止 XSS。

陷阱 2:nil slice vs empty slice

s1 := struct{ Items []string }{Items: nil}
json.Marshal(s1) // {"Items":null}

s2 := struct{ Items []string }{Items: []string{}}
json.Marshal(s2) // {"Items":[]}

// 解决方案:初始化时使用 make
Items: make([]string, 0) // → []

陷阱 3:JSON 中的数字类型

// 反序列化到 interface{} 时,JSON 数字总是 float64
jsonStr := `{"count": 42}`
var data map[string]interface{}
json.Unmarshal([]byte(jsonStr), &data)
// data["count"] 是 float64(42),不是 int

// 解决方案:使用 json.Decoder + UseNumber()

陷阱 4:omitempty 对 struct 无效

omitempty 的判断规则是:值为零值时才忽略。但 struct 类型的"零值"问题比较复杂。

type Inner struct {
    Value string `json:"value"`
}

type Outer struct {
    Inner Inner `json:"inner,omitempty"`
}

o := Outer{} // Inner 是零值 Inner{}
data, _ := json.Marshal(o)
fmt.Println(string(data))
// 输出:{"inner":{"value":""}}
// ↑ 即使 Inner 是零值,omitempty 也没有省略它!

为什么会这样?

因为 omitempty 通过反射判断"值是否为零值"。对于 struct 类型,Go 的反射中没有任何 struct 值等于零值判断reflect.Value.IsZero() 仅在 Go 1.13+ 的 reflect 包中存在,但 encoding/json 没有使用它)。

实际上 encoding/json 对 struct 类型的 omitempty 判断是:struct 不是 nil(struct 类型在 Go 中不是引用类型,永远不可能是 nil),所以 struct 永远不会被 omitempty 省略。

// ❌ omitempty 对 struct 始终不生效(struct 不可能是 nil)
type Outer struct {
    Inner InnerStruct `json:"inner,omitempty"` // 永远不会被省略
}

// ✅ 解决方法:使用指针 + omitempty
type Outer struct {
    Inner *InnerStruct `json:"inner,omitempty"` // nil 时省略!
}

// ✅ 或者实现 json.Marshaler 接口判断是否为零值
func (i Inner) IsZero() bool {
    return i == Inner{}
}

// ✅ 最佳方案:使用 `omitempty` + 指针
// 在字段可能不存在时,让字段类型是指针
type Config struct {
    Owner   *UserInfo `json:"owner,omitempty"`   // nil 时被省略
    Timeout int       `json:"timeout,omitempty"` // 0 时被省略
}

💡 omitempty 对各类型的效果总结

类型 零值 omitempty 是否生效
bool false ✅ 生效
int/float 0 ✅ 生效
string "" ✅ 生效
*T 指针 nil ✅ 生效
[]T slice nil ✅ 生效
map[K]V nil ✅ 生效
struct Struct{} 不生效(struct 不可为 nil)
time.Time time.Time{} ⚠️ 不生效time.Time 是 struct!)

🚨 特别注意 time.Time:它是 struct,所以 json:"t,omitempty" 对零值 time.Time{} 无效。需要用 *time.Timetime.Time + IsZero() + 自定义 MarshalJSON。