nil切片与[]int{}均len为0,但nil切片底层数组指针为nil,空切片有真实底层数组;json.marshal时前者输出null,后者输出[],api兼容性易出错。

nil 切片和 []int{} 看起来一样,但 len() 和 cap() 行为不同
它们都表现为“没元素”,但底层指针状态完全不同:nil 切片的底层数组指针是 nil,而空切片(如 []int{})有真实底层数组(哪怕长度为 0)。这直接影响 len()、cap()、append() 和 JSON 序列化行为。
-
len(nilSlice)和len([]int{})都返回0,但cap(nilSlice)是0,cap([]int{})也是0—— 这里容易误以为完全等价 -
append(nilSlice, 1)能正常工作,返回新切片;append([]int{}, 1)同样可以 —— 两者在追加时表现一致,但背后分配逻辑不同 - 真正差异在
json.Marshal():nil切片序列化为null,[]int{}序列化为[]—— API 兼容性常在这里翻车
初始化时写 var s []int 还是 s := []int{}?取决于你要不要区分“未设置”和“明确为空”
这是语义选择,不是性能问题。前者声明一个 nil 切片,后者构造一个长度为 0 的非 nil 切片。Go 标准库多数函数(如 json.Unmarshal)会把 JSON 中的 null 解析为 nil 切片,把 [] 解析为空切片。
- 如果字段可选且需要区分“客户端没传”(
null)和“客户端传了空数组”([]),就用var s []int - 如果只是临时收集数据,后续必走
append,两者性能无差别 —— Go 追加时都会按需分配,nil切片第一次append也分配新底层数组 - 别用
make([]int, 0)替代[]int{}:它和后者等价,但更冗长;make([]int, 0, 10)才是有预分配容量的写法
append() 对 nil 切片和空切片的行为一致,但别依赖 cap() 做扩容判断
append 内部对 nil 切片做了特殊处理,会直接调用 mallocgc 分配新数组,所以你不需要预先 make。但如果你手动检查 cap(s) 来决定是否 <code>make,那在 nil 切片上会出错 —— 因为 cap(nil) 是 0,而 len(nil) 也是 0,条件恒成立,导致多余分配。
- 正确做法:直接
append(s, x),让 Go 自己管扩容逻辑 - 错误模式:
if cap(s) —— 对 <code>nil切片会 panic 或逻辑错乱 - 想预估容量?用
make([]int, 0, estimatedSize)初始化,而不是靠运行时检测
JSON 反序列化时,nil 和空切片的差异最易被忽略
这是线上 bug 高发区。比如 API 接收一个 []string 字段,前端有时发 {"tags": null},有时发 {"tags": []}。如果结构体字段定义为 Tags []string,Go 默认把 null 解成 nil,把 [] 解成空切片 —— 但业务代码若只检查 len(s) == 0,就会漏掉语义差异。
立即学习“go语言免费学习笔记(深入)”;
- 安全做法:需要区分时,显式用指针字段
*[]string,或自定义UnmarshalJSON方法 - 常见误判:
if s == nil检查空切片 —— 错,[]int{}不等于nil;if len(s) == 0检查两者都成立,无法区分 - 测试时务必覆盖两种 JSON 输入:
null和[],尤其涉及权限、过滤、默认值逻辑的场景
最麻烦的地方在于:它们在绝大多数操作中表现一致,只有少数边界(JSON、反射、与 C 交互、某些 ORM 映射)才暴露差异。等出问题时,往往已经混在几十层调用里了。










