nil切片序列化为null,空切片[]int{}序列化为[],因前者ptr为nil(未设置),后者ptr非nil(明确为空);解析时前者得nil切片,后者得len=0非nil切片。

json.Marshal 会把 nil 切片输出为 null,而 []int{} 这类 empty 切片输出为 [] —— 这是 API 兼容性翻车最频繁的点。
为什么 JSON 输出不同?看底层指针状态
Go 的切片本质是三元组:ptr、len、cap。差异全在 ptr:
-
var s []int:ptr 是nil,整个结构体是零值 →json.Marshal认为“未设置”,输出null -
s := []int{}或make([]int, 0):ptr 非nil(指向共享的 zerobase),只是长度为 0 →json.Marshal认为“明确为空”,输出[]
前端传 {"items": null} 和 {"items": []} 在 Go 里解析结果不同
如果你的结构体字段定义为 Items []string,那么:
- JSON 中
"items": null→json.Unmarshal将其赋值为nil切片 - JSON 中
"items": []→ 解析为非nil的 empty 切片([]string{}) -
len(s) == 0对两者都成立,但s == nil只对前者为真
别用 len(s) == 0 判断是否“未传值”
业务逻辑中若需区分“客户端没传该字段”和“客户端传了空数组”,必须显式比较 s == nil:
- 错误写法:
if len(items) == 0 { /* 处理空或未传 */ }—— 语义模糊,无法分离两种情况 - 正确写法:
if items == nil { /* 客户端未提供 items 字段 */ };else if len(items) == 0 { /* 客户端显式传了 [] */ } - 泛型或 interface{} 场景下,
s == nil失效(因接口包装后非 nil),此时需用reflect.ValueOf(s).Kind() == reflect.Slice && reflect.ValueOf(s).IsNil()
最容易被忽略的是:这个差异不会在本地测试轻易暴露——只有当真实请求混杂 null 和 [] 时,业务分支才开始错乱。上线前务必用两种 payload 覆盖测试。











