nil切片序列化为null,空切片序列化为[];json.Marshal依据底层指针是否为nil判断,而非len();反序列化时null→nil切片、[]→非nil空切片,需用s==nil而非len(s)==0区分字段是否传入。
json.Marshal 会把 nil 切片转成 null,empty 切片转成 []
这是最直接、最常踩坑的差异。go 的 json.marshal 不看 len(),它看切片底层的 array 指针是否为 nil:指针为 nil → 输出 null;指针非 nil(哪怕指向零长数组)→ 输出 []。
示例:
type Resp struct {
Tags []string `json:"tags"`
}
json.Marshal(Resp{Tags: nil}) // {"tags":null}
json.Marshal(Resp{Tags: []string{}}) // {"tags":[]}
- 前端收到
{"tags":null}通常会当作字段缺失或未初始化,可能触发默认逻辑或报错 - 前端收到
{"tags":[]}明确知道“有这个字段,但值为空数组”,更利于一致性处理 - API 版本升级时若旧客户端不处理
null,而新逻辑返回了nil切片,就容易崩在前端 JSON 解析阶段
json.Unmarshal 反向行为:null → nil,[] → empty
反序列化时,json.Unmarshal 严格按 JSON 值还原语义:null 被赋给结构体字段后是 nil 切片;[] 被赋值后是长度为 0 的非 nil 切片。
这意味着你不能只靠 len(s) == 0 判断“客户端有没有传这个字段”:
-
len(tags) == 0对nil和[]都成立 —— 无法区分“没传”和“传了空数组” - 正确做法是先判断
tags == nil,再结合业务决定是否允许该字段缺失 - 如果结构体字段是
*[]string,那null会解成nil指针,[]会解成非nil指针指向 empty 切片,语义更清晰但用得少
接口类型或泛型参数中,nil 切片会“消失”
当你把 nil 切片赋给 interface{} 或传入泛型函数时,v == nil 会恒为 false —— 因为接口值本身非 nil(它包装了一个 nil 切片)。
这时候想安全判断,必须用反射:
func IsNilSlice(v interface{}) bool {
rv := reflect.ValueOf(v)
return rv.Kind() == reflect.Slice && rv.IsNil()
}
- 直接写
v == nil在泛型函数里编译失败(类型不确定) - 用
reflect.ValueOf(v).IsNil()前必须确认Kind()是reflect.Slice,否则 panic - 这种反射判断开销小,但别在 hot path 上高频调用
初始化选择:var s []T 还是 s := []T{}?
取决于你要不要保留“未设置”这个语义层级:
- 用
var s []T:适合 API 输入字段,需区分null(客户端省略)和[](客户端显式传空) - 用
s := []T{}或make([]T, 0):适合内部临时容器,后续必走append,且不希望下游看到null - 别用
make([]T, 0, N)来“优化” nil 切片——append对nil切片同样高效,它内部会直接分配,手动预分配反而可能浪费内存
真正容易被忽略的是:JSON 序列化行为不是配置项,也不是可开关的特性,它是语言运行时对切片三元组(ptr/len/cap)的硬编码解释。一旦定义了结构体字段类型为 []T,你就已经绑定了这个语义契约。











