今天在逛技术论坛的时候,发现有位小伙伴在评论区对我之前写的文章做了一个补充,我觉得他补充的这一点非常好!非常贴合实战经验,因此,今天写一篇文章也聊一聊他补充的这个点。
非常感谢这位小伙伴的补充!也希望大家都能够在评论区一起聊一聊,讨论讨论。不错的点,我看到了之后,都会再写文章一起聊一聊的。

不知道大家在初学Go的时候,有没有过这种"强迫症"时刻:写代码时总琢磨,我声明一个空切片,到底用 var 好,还是用 := 加花括号好?
go
// 写法一
var arr []int
// 写法二
arr := []int{}
如果你去搜,网上大多告诉你是"nil切片"和"空切片"的区别。但这就完了吗?如果你真把这俩当一回事儿,线上项目早晚给你炸个"坑"出来。
今天这篇文章,就不扯啥枯燥的计算机底层原理,咱们直接贴代码、看现象、聊实战。
花5分钟看完,以后你在项目里怎么选,心里就有谱了。
一、先看本质:内存里到底长啥样?
Go语言里的切片([]int)其实是一个"盒子",里面装着三样东西:指向底层数组的指针(地址) 、长度(Len) 和 容量(Cap)。
我们来做个简单的内存窥探:
go
package main
import "fmt"
func main() {
// 写法一:只声明,不初始化(零值初始化)
var a []int
// 写法二:字面量初始化,里面啥也没有
b := []int{}
// 看看它们是不是 nil
fmt.Printf("a 是否为 nil? %v\n", a == nil) // 输出:true
fmt.Printf("b 是否为 nil? %v\n", b == nil) // 输出:false
// 虽然长度都是0,但指针地址完全不同
fmt.Printf("a 底层指针指向:%p\n", a) // 指向 0x0 (空地址)
fmt.Printf("b 底层指针指向:%p\n", b) // 指向一个固定的非零地址(Go的zerobase)
}
解释一下:
var a []int:你只是喊了一声"我要个盒子",但没去拿货 。这时候盒子上的收货地址是空的(nil)。b := []int{}:你喊了一声"我要个盒子",并且跑去货架上拿了个空纸箱 回来。这个纸箱是空的,但它真实存在 (非nil)。
结论:
var是 空指针 ,{}是 指向空对象的指针。
二、惊天大坑:对接 JSON 时
假设你的接口要返回一个用户列表,前端拿到这个字段需要循环渲染。我们看看两种写法返回给前端的结果:
go
package main
import (
"encoding/json"
"fmt"
)
type Response struct {
Users []int `json:"users"`
}
func main() {
// 场景1:结构体中的 Users 字段没赋值,默认是 nil
// 即 var users []int
res1 := Response{}
// 场景2:显式赋值为空切片
res2 := Response{
Users: []int{}, // 明确初始化为非 nil 空切片
}
data1, _ := json.Marshal(res1)
data2, _ := json.Marshal(res2)
fmt.Printf("var 声明的 JSON:%s\n", string(data1))
// 输出:{"users":null}
fmt.Printf("{} 声明的 JSON:%s\n", string(data2))
// 输出:{"users":[]}
}
- 用
var返回的是null。 - 用
{}返回的是[](空数组)。
那么,前端同学看到 null 会怎样?
如果是严谨的JS代码,null 不是数组,无法调用 map 方法,页面直接白屏报错 Cannot read property 'map' of null!
所以,只要是返回给前端的列表字段,请务必使用 []int{} 或 make([]int, 0) 来初始化,确保序列化后是 [] 而不是 null
当然了,有经验的前端童鞋也许会使用
?.语法糖来避免这种情况,但归根结底我们作为后端对返回数据格式尽可能统一会更好。
三、arr := []int{} 和 var arr []int 二者之间的联系是啥?
这时候有细心的小伙伴要问了:arr := []int{} 是不是就等价于两步操作 var arr []int arr = []int{} 呢?
是的,最终效果 100% 一致!
我们可以写段代码来验证一下:
go
package main
import "fmt"
func main() {
// 方式一:一步到位
a := []int{}
// 方式二:拆解成两步(先声明,再赋值)
var b []int
b = []int{} // 此时 b 被覆盖,不再是 nil
// 比较最终效果 ------ 完全一样!
fmt.Printf("a == nil? %v\n", a == nil) // false
fmt.Printf("b == nil? %v\n", b == nil) // false
// 比较底层指针,两者指向同一个 zerobase 占位地址
fmt.Printf("a 的指针: %p, len: %d\n", a, len(a))
fmt.Printf("b 的指针: %p, len: %d\n", b, len(b))
// 输出结果:完全一致
}
那编译器到底做了什么?
虽然"最终结果"一样,但编译器和CPU执行时有个细微的时间差:
-
对于
arr := []int{}:编译器在变量诞生的那一刻,直接把它初始化为非nil的空切片。全程没有nil中间态。 -
对于
var arr []int; arr = []int{}:执行顺序是:- 先执行
var arr []int:此时arr被赋值为零值,也就是nil。 - 再执行
arr = []int{}:右侧构建了一个新的空切片,直接覆盖掉变量里的nil。
- 先执行
关键点来了 :虽然在第一步的瞬间,arr 曾经是 nil,但在第二步执行完后,nil 就被彻底丢弃了。对于日常业务代码(特别是调用函数时),只关心赋值完成后的最终状态,所以你可以放心地认为两者完全等价。
那为什么我推荐你直接用 arr := []int{}?
既然等价,那 var arr []int; arr = []int{} 两步走不好吗?
不好!原因有三:
- 多此一举:创建了"中间态(nil)",不仅多敲一行代码,还容易让人困惑。
- 可读性差 :代码审查时,别人看到
var arr []int第一反应是"哦,这是个 nil 切片",结果下一行你就把它覆盖了,容易造成阅读上的思维跳跃。 - 不够优雅:Go语言推崇简洁,一步到位永远比两步更清晰。
但是! 这个等价关系成立有个前提 :必须在函数内部 。因为 := 是短变量声明,不能在全局作用域使用。
go
// 包级别(全局)
// 编译错误!非声明语句不能在函数外
// var globalArr []int
// globalArr = []int{}
// ✅ 正确写法一:一步到位用 var
var globalArr = []int{}
// ✅ 正确写法二:显式声明类型(效果等同上面)
var globalArr2 []int = []int{}
func main() {
// 函数内部,两种写法都可以
localA := []int{} // 简洁优雅
var localB []int
localB = []int{} // 不推荐,多此一举
}
四、语法局限:全局变量和函数内的"潜规则"
这个地方初学者很容易写错,你要记住::= 是"短变量声明",它是局部的"特权阶级"。
- 在函数内部:两种写法随便用,看心情。
- 在函数外部(包级别) :只能用
var或者var ... = ...,不能用:=。
go
// 包级别(全局)
var globalArr []int // ✅ 正确,这是Go的标准写法
// globalArr2 := []int{} // ❌ 编译错误!非局部声明不能用 :=
var globalArr3 = []int{} // ✅ 正确,显式赋值
func main() {
// 局部范围内随便玩
localVar := []int{} // ✅ 正确
var localVar2 []int // ✅ 正确
}
实战建议:在全局定义切片变量,用
var居多;如果一定要全局给个空数组(防止包外调用者误判nil),用var xxx = []int{}。
五、追加元素(append)时表现一样,但性能有一点微差
很多小白担心"nil切片 append 会不会报错?会不会空指针异常?"
放心,Go语言设计得很巧妙:无论 nil 切片还是空切片,直接 append 都完全没问题。
go
func main() {
var a []int
b := []int{}
// 无论如何,都能正常追加
a = append(a, 1, 2, 3) // 正常工作,Go会自动分配内存
b = append(b, 1, 2, 3) // 正常工作
fmt.Println(a) // [1 2 3]
fmt.Println(b) // [1 2 3]
}
性能上有细微差别吗?
var a []int:在append首次触发扩容时,Go会分配一块全新的底层数组。因为是零成本启动,不占用任何内存空间,极其轻量。b := []int{}:它已经指向了一个"空内存占位符",虽然现代编译器对此做了优化(指向zerobase),但在极高频的内存分配场景下,var写法少一次指向非零地址的赋值,CPU寄存器开销微乎其微。
所以,如果只是定义一个待填充的容器,就直接用 var 这种写法把。
六、实战选型
为了帮你彻底治好选择困难症,大家伙儿可以看看如下的场景
| 场景 | 推荐写法 | 理由 |
|---|---|---|
| 函数内部待会要 append 填充数据 | var arr []int |
零成本、符合Go社区习惯 |
| 结构体字段、返回给前端的JSON | arr := []int{} 或 make([]int, 0) |
防止序列化出现 null 导致前端报错 |
| 初始化时就有具体默认值 | arr := []int{1, 2, 3} |
废话不多说,直接用 |
| 已知数据量很大(如数据库批量查询) | arr := make([]int, 0, 1000) |
预分配容量,避免频繁扩容,性能拉满 |
| 在数据库ORM中查询(比如 GORM) | var arr []Model |
查不到数据返回 nil 不影响业务;若业务强制非空则转用 {} |
七、最后一个"隐形坑":判断"有没有数据"
有些新手喜欢这么写:
go
func GetUsers() []int {
var users []int
// ... 一些逻辑
return users
}
// 调用方判断
if GetUsers() == nil {
fmt.Println("没有数据")
}
或者习惯用 len(arr) == 0 来判断是否为空。
请注意:
len(nil切片)和len(空切片)都是 0。- 但
nil切片 != 空切片。
最佳实践: 在Go中,判断切片是否为空,永远使用 len(arr) == 0,不要去判断 arr == nil 。因为无论是 var 还是 {},长度都是0,这样写才具备通用性,不会被底层声明方式所束缚!
八、总结
其实 var arr []int 和 arr := []int{} 就像 "手里什么都没有" 与 "手里拿着一张白纸" 的区别。
- 写业务接口、返回JSON,你就是得拿那张"白纸"给前端(
{}),这叫规矩。 - 写内部逻辑、临时容器,两手空空最省力(
var),这叫效率。
而 arr := []int{} 本质上就是 var arr []int; arr = []int{} 的"合成糖",最终效果毫无差别,但前者代码更优雅、更安全(避免误读 nil 中间态),日常开发请优先使用前者。
留个作业: 去看看你项目里的代码,把那些返回 null 导致前端报错的接口,统统改成 []int{} 试试!
希望这篇文章能帮你在Go的路上少踩一个坑。
我是 Alex,我们下期见!如果觉得有用,点个在看,转发给身边写Go的朋友,少一个人踩坑,多一个人请你吃饭。😄