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.Number或json:"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 是否生效 boolfalse✅ 生效 int/float0✅ 生效 string""✅ 生效 *T指针nil✅ 生效 []Tslicenil✅ 生效 map[K]Vnil✅ 生效 structStruct{}❌ 不生效(struct 不可为 nil) time.Timetime.Time{}⚠️ 不生效( time.Time是 struct!)
🚨 特别注意
time.Time:它是 struct,所以json:"t,omitempty"对零值time.Time{}无效。需要用*time.Time或time.Time+IsZero()+ 自定义 MarshalJSON。