4、JSON与数据序列化:JSON结构定义、Go中的json.Marshal/Unmarshal、自定义序列化
在API契约校验中,JSON是最核心的数据交换格式。本章将深入Go语言对JSON的序列化与反序列化机制,从标准库的json.Marshal/Unmarshal到自定义序列化控制,帮助你在契约校验场景中精准处理数据边界。
4.1 JSON结构定义与Go类型映射
JSON由六种基本数据类型构成:对象、数组、字符串、数字、布尔值和null。Go标准库encoding/json提供了与这些类型一一对应的映射关系:
| JSON类型 | Go类型 | 注意事项 |
|---|---|---|
| 对象 | struct / map[string]interface{} |
struct字段需导出(大写开头) |
| 数组 | []T / []interface{} |
元素类型需一致或使用空接口 |
| 字符串 | string |
默认UTF-8编码 |
| 数字 | float64 / int / int64 |
默认解析为float64,需注意精度 |
| 布尔值 | bool |
严格匹配true/false |
| null | nil / 指针 / interface{} |
零值字段默认不输出null |
契约校验要点:在定义API契约时,务必明确每个字段的JSON类型和Go类型映射。数字类型尤其容易引发校验失败——契约中声明
integer,但Go默认解析为float64,会导致类型断言错误。
4.2 json.Marshal与json.Unmarshal核心用法
json.Marshal将Go值编码为JSON字节切片,json.Unmarshal将JSON字节切片解码为Go值。以下是一个完整的序列化/反序列化示例:
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
Age int `json:"age,string"` // 数字编码为字符串
IsActive bool `json:"is_active"`
}
// 序列化
user := User{ID: 1, Name: "张三", Age: 28, IsActive: true}
data, err := json.Marshal(user)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
// 输出: {"id":1,"name":"张三","email":"","age":"28","is_active":true}
// 反序列化
jsonStr := `{"id":2,"name":"李四","age":"35","is_active":false}`
var user2 User
if err := json.Unmarshal([]byte(jsonStr), &user2); err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", user2)
// 输出: {ID:2 Name:李四 Email: Age:35 IsActive:false}
关键字段标签说明:
json:"field_name":指定JSON中的字段名omitempty:当字段值为零值时,序列化时忽略该字段string:将数字类型编码/解码为JSON字符串-:忽略该字段,不参与序列化
常见陷阱:使用
omitempty时,布尔值false、数字0、空字符串""都会被忽略。在契约校验中,如果API要求必须返回某个字段(即使为零值),不应使用omitempty。
4.3 自定义序列化:实现Marshaler/Unmarshaler接口
当标准序列化行为无法满足契约要求时(如自定义时间格式、加密字段、特殊编码),可以通过实现json.Marshaler和json.Unmarshaler接口进行精确控制。
// 自定义时间类型,契约要求格式为 "2006-01-02 15:04:05"
type CustomTime struct {
time.Time
}
const ctLayout = "2006-01-02 15:04:05"
// 实现 json.Marshaler
func (ct CustomTime) MarshalJSON() ([]byte, error) {
if ct.IsZero() {
return []byte(`""`), nil
}
formatted := ct.Format(ctLayout)
return json.Marshal(formatted)
}
// 实现 json.Unmarshaler
func (ct *CustomTime) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
if s == "" {
ct.Time = time.Time{}
return nil
}
t, err := time.Parse(ctLayout, s)
if err != nil {
return err
}
ct.Time = t
return nil
}
// 使用自定义类型
type Order struct {
ID int `json:"id"`
CreatedAt CustomTime `json:"created_at"`
}
order := Order{ID: 1001, CreatedAt: CustomTime{Time: time.Now()}}
data, _ := json.Marshal(order)
fmt.Println(string(data))
// 输出: {"id":1001,"created_at":"2025-03-21 14:30:00"}
契约校验场景:当API契约要求时间格式为
RFC3339,但上游系统返回Unix时间戳时,通过自定义UnmarshalJSON可以在反序列化阶段完成格式转换,避免在业务代码中反复处理。
4.4 JSON序列化流程与契约校验关系
下图展示了Go中JSON序列化/反序列化的完整流程,以及契约校验在其中的介入点:
4.5 契约校验中的序列化最佳实践
在API契约校验场景中,序列化操作需要遵循以下原则:
- 显式定义字段标签:每个导出字段都应使用
json:"name"标签,避免依赖Go字段名自动映射 - 统一数字类型处理:契约中明确数字是
int还是float,必要时使用json.Number避免精度丢失 - 自定义序列化控制边界:对时间、枚举、加密字段实现
Marshaler/Unmarshaler接口 - 使用
json.RawMessage延迟解析:当部分字段结构不确定时,先保留原始JSON,后续再按契约校验 - 避免
interface{}滥用:在契约校验中,interface{}会丢失类型信息,应优先使用具体结构体
// 使用 json.RawMessage 延迟解析
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data"` // 保留原始JSON
}
// 后续按契约校验时再解析
var resp Response
json.Unmarshal([]byte(rawJSON), &resp)
// 根据不同的契约版本解析 data 字段
switch resp.Code {
case 200:
var user User
json.Unmarshal(resp.Data, &user)
// 对 user 进行契约校验
case 201:
var list []User
json.Unmarshal(resp.Data, &list)
// 对 list 进行契约校验
}
性能提示:在契约校验的高频调用场景中,建议使用
json.Decoder代替json.Unmarshal,前者支持流式解析,减少内存分配。同时,预编译json.Encoder的缓冲区可以提升序列化吞吐量。
4.6 常见序列化问题与契约校验冲突
| 问题场景 | 表现 | 契约校验解决方案 |
|---|---|---|
| 数字精度丢失 | 大整数被解析为float64,精度丢失 |
使用json.Number或自定义UnmarshalJSON |
| 字段缺失 | JSON中缺少必需字段,反序列化为零值 | 反序列化后校验字段是否为零值,或使用指针类型 |
| 额外字段 | JSON包含未定义字段,默认被忽略 | 使用json.Decoder的DisallowUnknownFields() |
| 空数组 vs null | []和null在Go中都被解析为nil切片 |
使用json.RawMessage区分,或自定义解析 |
| 时间格式不一致 | 契约要求RFC3339,实际返回Unix时间戳 | 实现自定义UnmarshalJSON进行格式转换 |
契约校验红线:永远不要在生产环境中直接使用
json.Unmarshal的结果而不做校验。即使Go标准库没有报错,字段缺失、类型不匹配、值越界等问题仍可能导致业务逻辑异常。契约校验是序列化之后的第二道防线。