json.Marshal将nil切片转为null、空切片转为[],导致前端Array.isArray判断失败;反序列化时null→nil、[]→[]T,需用s==nil和len(s)==0共同判断;结构体字段须显式初始化为[]T{}。

json.Marshal 会把 nil 切片转成 null,空切片转成 []
这是最直接、最常踩的坑:前端约定 "items": [] 表示“有这个字段,但值为空”,而 "items": null 往往被当成缺失、错误或未初始化。Go 的 json.Marshal 严格按语义区分——nil 是“未设置”,[]T{} 是“明确设为空”。
常见错误现象:
- 结构体字段声明为
Items []string,但没初始化,json.Marshal输出{"Items":null} - 从 Redis 反序列化后字段变成
nil,再序列化就变成null,破坏接口契约 - 前端用
Array.isArray(data.items)判定失败(因为null不是数组)
反序列化时 null 和 [] 会分别解成 nil 切片和空切片
json.Unmarshal 不会自动把 null 转成空切片;它忠实还原 JSON 原意:null → nil,[] → []T{}。这意味着你不能只靠 len(s) == 0 来判断“有没有数据”,因为它对两者都返回 true。
正确做法:
- 用
s == nil显式判断是否为null(仅适用于具体切片类型) - 若字段是
interface{}或泛型参数,必须用reflect.ValueOf(v).Kind() == reflect.Slice && reflect.ValueOf(v).IsNil() - 业务逻辑需区分“客户端没传该字段”(
null)和“客户端传了空数组”([])时,必须同时检查== nil和len() == 0
结构体字段初始化必须写 []T{},不能只声明
声明即零值:var Items []string 等价于 Items: nil。只要没显式赋值,字段就是 nil,后续任何 json.Marshal 都输出 null。
实操建议:
- 在结构体定义处不初始化?那就必须在构造实例时显式赋值:
Items: []Address{} - 用工厂函数封装初始化逻辑,避免漏掉:
func NewCustomer() Customer { return Customer{Items: []string{}} } - 别依赖
make([]T, 0)作为“更安全”的写法——它和[]T{}在 JSON 行为上完全等价,只是多打几个字
append 对两者都安全,但 cap() 检查在 nil 切片上会误导
append(s, x) 内部已处理 nil 切片,无需预先判断。但如果你手动写扩容逻辑,比如:
if cap(s) < len(s)+1 {
s = make([]int, len(s), newCap)
}
这段代码在 s == nil 时恒成立(因为 cap(nil) 是 0),导致无谓分配甚至逻辑错乱。
真正该做的:
- 让
append自己管扩容——它足够智能 - 如果预估容量,用
make([]T, 0, estimatedSize)初始化,而不是运行时检测 - 永远别在不确定是否为
nil的切片上做cap()或len()比较来决策分配
最易被忽略的一点:JSON 反序列化后的字段状态,会直接影响下一次序列化的输出。一旦某个字段在某次 Unmarshal 后变成 nil,又没在后续逻辑中重置为空切片,它就会一路 null 到底——这个状态不会自动恢复。


















