- 示例工程
- 教程
【免费下载链接】learngo
❤️ 1000+ Hand-Crafted Go Examples, Exercises, and Quizzes. 🚀 Learn Go by fixing 1000+ tiny programs.
切片与数组的取舍,是每个 Go 开发者入门时绕不开的第一个"分水岭"问题。本文以 learngo 仓库 16-slices/questions/1-slices-vs-arrays.md 这一经典测验文档为主线,逐题拆解 10 个关键知识点——从"为什么用切片"到"为什么切片不能比较",并对照仓库中的演示源码与练习,讲透二者的底层差异。读完后,你将能准确判断一个集合该用数组还是切片、能解释 nil 切片的行为边界,并理解动态长度在运行时(而非编译期)意味着什么。
一图看懂:数组与切片的本质区别
在逐题拆解之前,先建立一个总览框架。两者最核心的区别可以用一句话概括:数组的长度是类型的一部分,切片则不是。这一点决定了后续所有行为差异。
| 维度 | 数组(Array) | 切片(Slice) |
|---|---|---|
| 长度是否属于类型 | 是([5]int与[3]int是不同类型) | 否([]int与[]int永远是同一类型) |
| 长度变化时机 | 编译期固定,不可变 | 运行时可变(动态集合) |
| 零值 | 长度确定的"零值元素"集合 | nil |
| 可比较性 | 可以用==比较 | 只能与nil比较 |
| 是否可动态增删元素 | 不可 | 借助append动态增长 |
这一总览可以从仓库演示源码得到直接印证:16-slices/02-slices-vs-arrays/main.go 中同时打印了数组与切片的类型和零值行为:
var array [2]int fmt.Printf("array : %#v\n", array) // [2]int{0, 0}:零值元素 var slice []int fmt.Println("slice == nil?", slice == nil) // true:零值是 nil fmt.Println("len(slice) :", len(slice)) // 0:len 依然可用 fmt.Printf("array's type: %T\n", array) // [2]int fmt.Printf("slice's type: %T\n", slice) // []int第 1 题:为什么你要用切片而不是数组?
原题:Why you want to use a slice instead of an array? 正确答案:A slice's length is dynamic, so I can create dynamic collections(切片长度是动态的,可以创建动态集合)
数组的长度在声明时就已经确定并写进了类型里,例如[5]int永远只能容纳 5 个元素;而切片是对底层数组的一段"视图",长度可以在运行时通过append增长。这意味着:
- 编译期你不需要预先知道数据规模;
- 运行时可以按需扩容、收缩;
- 同一个切片变量可以被重新赋值成不同长度的切片。
仓库 16-slices/04-slices-vs-arrays-unique-nums/02-with-slices/main.go 给出了一个极具说服力的实战案例:生成若干互不重复的随机数。它从一个未初始化的var uniques []int(nil 切片)开始,循环中使用append动态累积元素,直到长度达到目标max:
var uniques []int loop: for len(uniques) < max { // len 用于 nil 切片同样有效 n := rand.Intn(max) + 1 for _, u := range uniques { if u == n { continue loop } } uniques = append(uniques, n) // 动态追加 } fmt.Println("length of uniques:", len(uniques)) sort.Ints(uniques) // 排序后打印uniques的最终长度在程序运行时才确定,这正是"动态集合"的含义。对照同目录下01-with-arrays的实现(需要found计数器配合手动赋值),可以直观感受切片的优势。
第 2 题:切片的长度属于编译期还是运行时?
原题:Where does the length of a slice belong to? 正确答案:Runtime(运行时);错误选项里的 "Compile-Time"(编译期)、"Walk-Time"、"Sleep-Time" 都是干扰项。
原文档给出了解析要点:"A slice's length is not a part of its type. So its length can change at runtime."(切片的长度不属于它的类型,所以它能在运行时改变。)
这句话正是切片与数组的分水岭。数组[5]int的长度 5 被编码在类型系统中,编译期就固定下来;而切片类型是[]int,不携带任何长度信息,任何[]int值(长度 0、5、1000)都满足该类型。因此:
- 函数签名里写
[]int就能接收任意长度的切片; - 赋值时可以把任意长度的切片赋给同一个变量;
- 长度的合法性在运行时由
len、append、索引边界等机制检查。
仓库 16-slices/01-slices-vs-arrays/main.go 用并排对比代码展示了这一点:注释中明确写着数组"its length is part of its type",切片"its length is not part of its type"。
第 3 题:函数调用中的类型精确匹配
原题:以下哪个函数调用是正确的?给定函数
func sort(nums []int) { ... }选项:
sort([...]int{3, 1, 6})—— 错误:不能用数组调用,它期望的是 int 切片;sort([]int32{3, 1, 6})—— 错误:不能用 int32 切片调用,它期望的是 int 切片;sort([]int{3, 1, 6})——正确
这道题揭示 Go 严格的静态类型特性:[]int、[3]int、[]int32是三种完全不同的类型,函数参数必须"精确匹配",Go 不会做隐式转换。
- 数组 ≠ 切片:
[...]int{3, 1, 6}是数组(类型[3]int),不能传给要求[]int的参数。若确实要用数组,需要显式切片:sort(nums[:])——这正是 16-slices/04-slices-vs-arrays-unique-nums/02-with-slices/main.go 中sort.Ints(nums[:])的用法,它对[5]int数组做了一次切片表达式得到[]int。 - 元素类型也须一致:
[]int32与[]int的元素类型不同,同样无法互传。即使int32和int底层都是整数,Go 也不允许隐式转换。
第 4 题:切片的零值是 nil
原题:
var tasks []string的零值是什么? 正确答案:nil(错误选项:0、1、unknown)
原文档解析:"This is a nil slice. Unlike an array, a slice's zero value is nil."(这是 nil 切片。与数组不同,切片的零值是 nil。)
一个只声明未初始化的切片变量,值为nil,表示"它还没有指向任何底层数组"。
对比记忆:
- 数组的零值是"长度确定、每个元素为零值"的集合,例如
[2]int的零值是[0 0]; - 切片的零值是
nil。
仓库 16-slices/exercises/01-declare-nil/solution/main.go 演示了各种元素类型的 nil 切片声明,并统一打印类型、长度与是否为 nil:
var ( names []string // The names of your friends distances []int // The distances data []byte // A data buffer ratios []float64 // Currency exchange ratios alives []bool // Up/Down status of web servers ) fmt.Printf("names : %T %d %t\n", names, len(names), names == nil)无论元素类型是 string、int、byte、float64 还是 bool,未初始化的切片一律是nil,len均为 0,与nil比较均为true。
第 5 题:nil 切片上使用 len 会怎样?
原题:
var tasks []string后执行fmt.Println(len(tasks))输出什么? 正确答案:0
原文档解析:"Yes, you can use the len function on a nil slice. It returns 0 because the slice doesn't contain any elements yet."(是的,你可以对 nil 切片使用 len 函数,它返回 0,因为切片还不包含任何元素。)
这是切片非常友好的特性:len(nil_slice)合法且返回 0。因此你总是可以放心地对一个可能为 nil 的切片调用len、range等操作,无需先做 nil 判断。这在 16-slices/02-slices-vs-arrays/main.go 中有直接演示(len(slice): 0),16-slices/04-slices-vs-arrays-unique-nums/02-with-slices/main.go 中for len(uniques) < max的循环条件也依赖这一点——初始的 nil 切片 len 为 0,循环得以启动。
第 6 题:nil 切片上做索引会怎样?
原题:
var tasks []string后执行fmt.Println(tasks[0])输出什么? 正确答案:It doesn't work.(程序会报错)(错误选项:0、1、nil)
原文档解析:"You can't get an element that does not exist. A nil slice does not contain any elements."(你不能取一个不存在的元素,nil 切片不包含任何元素。)
这是上一题的"硬币另一面":虽然len对 nil 切片宽容地返回 0,但索引越界会触发运行时 panic。tasks[0]试图访问不存在的第 1 个元素,Go 运行时立即报错:
panic: runtime error: index out of range [0] with length 0仓库 16-slices/01-slices-vs-arrays/main.go 中特意用注释保留了这两行"会崩"的代码,提醒学习者:
// won't work: the slice is nil. // fmt.Printf("nums[0]: %d\n", nums[0]) // fmt.Printf("nums[1]: %d\n", nums[1])实际开发中,安全读取切片的惯用写法是先判断len(slice) == 0或索引前检查i < len(slice)。
第 7 题:哪种写法才是正确的切片声明?
原题:Which declaration below is a correct slice declaration? 选项:
[...]int{}—— 这是数组(省略号让编译器按元素个数推断长度),不是切片;[2]string{"hello", "world"}—— 这是长度为 2 的数组;[]string{"hello", "world"}——正确:方括号内没有长度,就是切片字面量;string[2]{"hello", world"}—— 语法错误(类型后置、world"缺引号),且 Go 中不存在这种写法。
判定数组与切片的"一眼技巧"就是看方括号里有没有长度或...:
[...]T{}、[N]T{}→ 数组;[]T{}→ 切片。
仓库 16-slices/exercises/04-declare-arrays-as-slices/solution/main.go 用一组切片字面量展示了各元素类型的标准写法:
names := []string{"Einstein", "Tesla", "Shepard"} distances := []int{50, 40, 75, 30, 125} data := []byte{'H', 'E', 'L', 'L', 'O'} ratios := []float64{3.14145} alives := []bool{true, false, true, false} zero := []byte{}注意最后的zero := []byte{}:这是一个非 nil 的空切片(长度 0 但有底层数组),与var zero []byte的 nil 切片不同,但二者的len都是 0。这一细微差别会在后续关于切片内部的测验中进一步展开。
第 8 题:为什么切片不能用 == 比较?
原题:下面的代码为什么编译不过?
colors := []string{"red", "blue", "green"} tones := []string{"dark", "light"} if colors == tones { ... }正确答案:Slices cannot be compared(切片不能被比较)
原文档解析:"That's right! A slice value can only be compared to a nil value."(切片值只能与 nil 比较。)
这是切片与数组又一个重要区别:数组可以用==逐元素比较,切片不行(Go 1.21 之前唯一合法的是slice == nil这种与 nil 的比较;切片之间比较是非法的编译错误)。
根本原因与切片的内部结构有关:切片由"指针 + 长度 + 容量"三部分组成,它指向底层数组的一段,两个切片即使元素相同也可能指向不同底层数组,语义上难以定义"相等";此外==需要类型可哈希,而切片可变。因此 Go 直接禁止切片之间用==。
那如何判断两个切片内容是否相等?标准做法是手动逐元素比较,并先比较长度。仓库 16-slices/03-slices-vs-arrays-examples/main.go 演示了完整的"切片相等性判断"模式:
games := []string{"pokemon", "sims"} newGames := []string{"pacman", "doom", "pong"} newGames = games // 切片可以整体赋值,指向同一底层数组 games = nil // 显式置为 nil games = []string{} // 空切片字面量 var ok string for i, game := range games { if game != newGames[i] { // 逐元素比较 ok = "not " break } } if len(games) != len(newGames) { // 先比较长度 ok = "not " } fmt.Printf("games and newGames are %sequal\n", ok)同一文件中,数组books与newBooks则直接使用if books == newBooks,并在长度相同的前提下用books = newBooks整体赋值——这正是数组与切片比较行为的直观对照。
第 9 题:空切片字面量的长度是多少?
原题:
[]uint64{}的长度是多少? 正确答案:0(错误选项:64、1、Error)
原文档解析:"That's right. This is an empty slice, it doesn't contain any elements."(这是空切片,不包含任何元素。)
这道题的干扰项设计得很巧妙:uint64中的 64 是位数(8 字节),与元素个数无关;[]uint64{}花括号内没有任何元素,所以len返回 0。切片字面量的长度就是花括号中实际列出的元素个数,与元素类型完全无关。
第 10 题:切片字面量的长度如何计算?
原题:
[]string{"I'm", "going", "to", "stay", "\"here\""}的长度是多少? 正确答案:5
花括号里共有 5 个字符串元素:
"I'm"—— 单引号是普通字符,无需转义;"going""to""stay""\"here\""—— 字符串值为"here"(含转义的双引号),它仍然只是一个元素。
注意两点:
- 字符串内部的转义(
\")只影响字符串内容,不影响元素个数; - 长度计数的是顶层元素个数,字符串本身再长也只是一个元素。
所以len的结果是 5。这道题提醒初学者:计算切片长度时,把花括号里的元素逐一数清即可,不要被元素内容的复杂格式干扰。
从源码看切片背后的动态能力
看完 10 道题,再回到仓库源码,你会发现切片的所有"便利"都围绕一个主题:把长度从类型中解放出来,交给运行时管理。几个可以继续深入探索的证据链:
- 动态增长依赖
append:nil/空切片都没有现成元素可用,增长只能通过append完成,且必须接收其返回值(nums = append(nums, 4)),因为append可能返回新的底层数组。演示见 16-slices/05-append/1-theory/main.go,其中_ = append(nums, 4)与nums = append(nums, 4)的对比尤其值得注意——前者丢弃返回值,切片长度不会改变。 - 数组可以一键"切片化":需要把数组传给要求切片参数的函数时,用切片表达式
nums[:]即可(见 16-slices/04-slices-vs-arrays-unique-nums/02-with-slices/main.go 中sort.Ints(nums[:]))。 - 切片相等性需手动实现:因为切片不能直接比较,通用模式是"先比长度、再逐元素比",参考 16-slices/03-slices-vs-arrays-examples/main.go。
- 配套练习:仓库 16-slices/exercises 中的
01-declare-nil(声明各种 nil 切片)、02-empty(空切片 vs nil 切片)、04-declare-arrays-as-slices(数组字面量改写为切片字面量)等练习,都是对这 10 道题知识点的动手巩固。
此外,16-slices/README.md 提到该章节的部分演示代码(如 05-append 的1-theory)依赖github.com/inancgumus/prettyslice库来可视化切片内部结构,可通过go get -u github.com/inancgumus/prettyslice安装——它能把底层数组、长度、容量渲染成表格,是直观观察"长度不属于类型"这一特性的利器。更多测验主题(append、slicing、backing array、capacity 等)见 16-slices/questions/README.md。
小结
- 数组长度是类型的一部分(编译期固定),切片长度不属于类型(运行时可变)——这是全部差异的总根源;
- 未初始化的切片零值为
nil,len返回 0,但索引会 panic; - 切片只能与
nil比较,不能与另一个切片==比较;判断内容相等需"先比长度再逐元素比"; - 函数参数要求精确的类型匹配:
[]int与[3]int、[]int32互不兼容; - 日常开发中,动态集合优先选切片;需要固定大小、可整体比较的场合才使用数组。
把这 10 道题连同仓库源码和练习全部跑通,你就掌握了 Go 集合类型中最重要的一个判断:什么时候选切片、为什么,以及它的边界在哪里。
- 示例工程
- 教程
【免费下载链接】learngo
❤️ 1000+ Hand-Crafted Go Examples, Exercises, and Quizzes. 🚀 Learn Go by fixing 1000+ tiny programs.
相关推荐
LunaTV网盘搜索与ACG种子搜索:PanSou与Mikan Project双源系统终极指南
LunaTV网盘搜索与ACG种子搜索:PanSou与Mikan Project双源系统终极指南 欢迎来到LunaTV增强版的全功能影视聚合平台!🎬 在这篇完整
MoviePy初探:Python视频剪辑自动化终极指南,为什么它是程序员的剪辑神器
MoviePy初探:Python视频剪辑自动化终极指南,为什么它是程序员的剪辑神器 MoviePy 是一个用 Python 做视频剪辑的开源库,让视频剪辑自动化
示例工程教程BepInEx IL2CPP 启动失败排查:黑窗闪退到正常进游戏的 3 条最短路径
BepInEx IL2CPP 启动失败排查:黑窗闪退到正常进游戏的 3 条最短路径 游戏刚启动,控制台黑窗一闪而过,进程眨眼就没了;可你把 BepInEx 文件
游戏开发插件系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考