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

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

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

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

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

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

相关文章
|
3月前
|
jenkins 持续交付 网络安全
手摸手教你玩 Jenkins,一次搞懂 CI/CD!(第二章:发布之sshPublisher)
本文详解如何用Jenkins通过SSH执行远程部署脚本实现项目发布:涵盖插件安装(Git Parameter、Publish Over SSH)、目标服务器配置(专用用户、免密sudo)、SSH凭据与Git凭据设置,并提供完整Jenkinsfile示例,支持参数化构建与多分支发布。(239字)
284 0
手摸手教你玩 Jenkins,一次搞懂 CI/CD!(第二章:发布之sshPublisher)
|
3月前
|
人工智能 文字识别 监控
taobao.item.search.img(拍立淘图片搜索 API)全业务场景手册
拍立淘按图搜商品(taobao.item.search.img)是阿里视觉AI接口,支持图片URL或Base64输入,精准返回同款/相似商品ID、标题、价格、销量等结构化数据,广泛应用于跨境铺货、比价导购、竞品监控、品牌维权、智能选品等六大场景。(239字)
|
5月前
|
Rust Linux 开发者
再见 pip!Rust 写的 uv 正在把 Python 包管理按在地上摩擦
Python开发者最头疼的依赖管理与环境配置难题,终于有解!uv——Rust编写的超快包管理器,安装、解析、虚拟环境创建速度达pip的10-100倍。支持一键装Python版本、运行脚本、编译依赖,正重塑Python开发工作流。(239字)
468 1
|
5月前
|
人工智能 开发工具 git
90%的人都不知道:Docker 容器 apt 报错 404 的幕后黑手竟是它!
Debian 10(Buster)已于2024年6月30日终止支持,软件源已迁移至archive.debian.org。若容器中执行apt报404错误,只需将sources.list中的deb.debian.org/security.debian.org替换为archive.debian.org,并删除-buster-updates相关行即可恢复安装能力。(239字)
433 4
90%的人都不知道:Docker 容器 apt 报错 404 的幕后黑手竟是它!
|
6月前
|
SQL JSON 缓存
别再用过时的地区数据了!闸北区都消失了,教你一次性搞定省市区同步更新!(附实战源码)
本文记录了更新地区表至最新行政区划的完整实践:从权威数据源获取2024年省市区数据,通过Python脚本实现新旧表(省-市-区三级)精准比对,支持新增、软删除及层级关系维护,并附详细代码与分步缓存策略,兼顾准确性、安全性和可追溯性。(239字)
396 5
|
6月前
|
人工智能 小程序 API
AI开发实战5、手摸手教学:如何用AI+go-zero,从数据库设计开始构建API
本文是AI开发实战系列第5篇,聚焦用Claude 3.5/Gemini等模型高效构建Go微服务后端。以开源记账小程序「时光账记」为例,详解如何通过AI辅助完成数据库建模、API契约定义与业务代码生成,强调“框架自建+AI填空”模式,兼顾效率与代码一致性。(239字)
470 1
AI开发实战5、手摸手教学:如何用AI+go-zero,从数据库设计开始构建API
|
5月前
|
NoSQL Unix Linux
别再死记硬背了!一文扒光 I/O 多路复用的底裤(Epoll/Select/Poll)
本文深入剖析I/O多路复用本质:以“一个服务员服务百桌客人”类比,讲清select/poll/epoll核心差异;揭秘红黑树+就绪队列如何实现O(1)高效通知;厘清LT/ET触发模式与同步/异步误区;直击高并发内存、上下文切换、锁竞争三大痛点。助你面试不背书,架构有底气!(239字)
398 3
|
5月前
|
存储 安全 算法
密码泄露了?别慌!GitHub、微软、Google都在用的“虚拟MFA”,到底有多强?
密码易泄露?虚拟MFA(多因素认证)用手机动态验证码构筑第二道防线,即使密码失窃,攻击者也无法登录。基于TOTP算法,兼容Google/Microsoft Authenticator等应用,实现简单、成本低廉、安全高效。(239字)
600 1
|
4月前
|
编解码 NoSQL 安全
Celery 太重了?这可能是你一直在找的 asyncio 任务队列
arq是Python原生异步任务队列,基于asyncio与Redis Streams构建,轻量(仅依赖redis-py)、高性能、开箱即用。专为FastAPI等异步框架设计,告别Celery的复杂配置,实现高并发I/O任务的优雅调度。(239字)
532 0
|
4月前
|
缓存 Rust Linux
Python 统一大业:uv 如何整合 Pip、Pyenv 和 Venv?
Python包管理长期饱受速度慢、依赖冲突之苦。Rust编写的`uv`横空出世——集Python版本管理、虚拟环境创建、依赖安装与锁定于一体,速度比pip快10–100倍,真正实现“All-in-One”极速开发体验。(239字)
695 0