别再乱用了!var arr []int 和 arr := []int{} 给整懵了?不就声明个空数组,项目居然崩了?

简介: 本文深入剖析Go中`var arr []int`与`arr := []int{}`的本质区别:前者为nil切片(JSON序列化为`null`),后者为空切片(序列化为`[]`)。结合内存结构、JSON接口坑点、append行为及实战选型,帮你避开前端报错、提升代码健壮性。

今天在逛技术论坛的时候,发现有位小伙伴在评论区对我之前写的文章做了一个补充,我觉得他补充的这一点非常好!非常贴合实战经验,因此,今天写一篇文章也聊一聊他补充的这个点。

非常感谢这位小伙伴的补充!也希望大家都能够在评论区一起聊一聊,讨论讨论。不错的点,我看到了之后,都会再写文章一起聊一聊的。

不知道大家在初学Go的时候,有没有过这种“强迫症”时刻:写代码时总琢磨,我声明一个空切片,到底用 var 好,还是用 := 加花括号好?

// 写法一
var arr []int

// 写法二
arr := []int{
   }

如果你去搜,网上大多告诉你是“nil切片”和“空切片”的区别。但这就完了吗?如果你真把这俩当一回事儿,线上项目早晚给你炸个“坑”出来。

今天这篇文章,就不扯啥枯燥的计算机底层原理,咱们直接贴代码、看现象、聊实战

花5分钟看完,以后你在项目里怎么选,心里就有谱了。


一、先看本质:内存里到底长啥样?

Go语言里的切片([]int)其实是一个“盒子”,里面装着三样东西:指向底层数组的指针(地址)长度(Len)容量(Cap)

我们来做个简单的内存窥探:

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 时

假设你的接口要返回一个用户列表,前端拿到这个字段需要循环渲染。我们看看两种写法返回给前端的结果:

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% 一致!

我们可以写段代码来验证一下:

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{}:执行顺序是:

    1. 先执行 var arr []int:此时 arr 被赋值为零值,也就是 nil
    2. 再执行 arr = []int{}右侧构建了一个新的空切片,直接覆盖掉变量里的 nil

关键点来了:虽然在第一步的瞬间,arr 曾经是 nil,但在第二步执行完后,nil 就被彻底丢弃了。对于日常业务代码(特别是调用函数时),只关心赋值完成后的最终状态,所以你可以放心地认为两者完全等价。

那为什么我推荐你直接用 arr := []int{}

既然等价,那 var arr []int; arr = []int{} 两步走不好吗?

不好!原因有三:

  1. 多此一举:创建了“中间态(nil)”,不仅多敲一行代码,还容易让人困惑。
  2. 可读性差:代码审查时,别人看到 var arr []int 第一反应是“哦,这是个 nil 切片”,结果下一行你就把它覆盖了,容易造成阅读上的思维跳跃。
  3. 不够优雅: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 ... = ...,不能用 :=
// 包级别(全局)
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 都完全没问题。

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 不影响业务;若业务强制非空则转用 {}

七、最后一个“隐形坑”:判断“有没有数据”

有些新手喜欢这么写:

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 []intarr := []int{} 就像 “手里什么都没有”“手里拿着一张白纸” 的区别。

  • 写业务接口、返回JSON,你就是得拿那张“白纸”给前端({}),这叫规矩
  • 写内部逻辑、临时容器,两手空空最省力(var),这叫效率

arr := []int{} 本质上就是 var arr []int; arr = []int{} 的“合成糖”,最终效果毫无差别,但前者代码更优雅、更安全(避免误读 nil 中间态),日常开发请优先使用前者。

留个作业: 去看看你项目里的代码,把那些返回 null 导致前端报错的接口,统统改成 []int{} 试试!

希望这篇文章能帮你在Go的路上少踩一个坑。

我是 Alex,我们下期见!如果觉得有用,点个在看,转发给身边写Go的朋友,少一个人踩坑,多一个人请你吃饭。😄

相关文章
|
6天前
|
缓存 人工智能 安全
GPT-5.6 Terra与GPT-5.5性能实测:成本减半后的跑分对比与快速迁移指南
GPT-5.6 Terra 的定价为每百万 token 输入 2.50/输出 15。GPT-5.5 则是 5/ 30。Terra 的每一项费率,包括 $0.25/M 的缓存读取,都恰好是 GPT-5.5 的一半,因此在任何工作负载组合下,Terra 都固定 便宜 2.0x。以每天 10 万次请求、3K token 提示词计算,大约是 Terra 每天 2,000,GPT−5.5每天 4,000,即每月约 60,000对 120,000。问题在于:OpenAI 没有发布任何针对 Terra 的编码基准。那个著名的 91.9% Terminal-Bench 数字是 Sol 在 Ul
|
1天前
|
数据采集 机器学习/深度学习 人工智能
田间杂草定位与检测4200张YOLO智慧农业数据集分享
本数据集含4200张真实农田图像,YOLO格式,单类别(杂草)高质量标注,覆盖多作物、多光照、多生长阶段等复杂场景,专为智慧农业杂草检测与智能除草设备研发设计,支持YOLOv5/v8/v10等主流模型训练。
326 93
|
2天前
|
缓存 UED 开发者
Codex109天重置23次,明天还要再送一次
Codex近109天完成23次额度重置,7月14日将迎来第24次。Tibo高频响应用户反馈:优化GPT-5.6高消耗问题、补发失效福利、调整重置时间——形成“反馈→回应→修复→补偿”正向闭环,彰显以用户为中心的产品哲学。(239字)
488 11
|
6天前
|
人工智能 缓存 JSON
刚刚 GPT-5.6 发布,吊打 Claude 5 和 Grok 4.5?一手实测来啦!
GPT-5.6 刚发布,跑分号称超越 Fable 5,真的假的?附一手实测,让 3 个最强 AI 编程模型 GPT-5.6 Sol、Claude Fable 5、Grok 4.5 在 Cursor 里同时一把梭开发网页游戏,看看最新模型到底谁更能打。
332 0
|
6天前
|
存储 人工智能 JSON
Qwen 本地部署搭配 ComfyUI 生成 AI 漫剧完整实操指南(小白零基础可落地,零成本无限生成+角色一致性天花板)
2026全网最优本地漫剧流水线:零成本、离线运行、角色统一、低配(8G显卡)可跑。融合Qwen本地大模型+ComfyUI双引擎,实现剧本生成→分镜绘图→动态成片全自动,隐私安全、无审核限流,新手30分钟上手,日更无忧。(239字)