文章目录
- Go语言切片:动态灵活的数据序列
-
- 一、引言
- 二、切片是什么
- 三、切片的结构
- 四、切片的创建
-
- [(一)、创建切片的标准方式:make 函数](#(一)、创建切片的标准方式:make 函数)
-
- [1. make 函数完整语法](#1. make 函数完整语法)
- [2、make 创建切片的 3个核心注意点(必看)](#2、make 创建切片的 3个核心注意点(必看))
-
- [(1). 容量不能小于长度](#(1). 容量不能小于长度)
- [(2). make 创建的切片会**默认初始化**](#(2). make 创建的切片会默认初始化)
- [(3). 预留容量,避免频繁扩容(性能关键)](#(3). 预留容量,避免频繁扩容(性能关键))
- [3. 2种常用创建示例](#3. 2种常用创建示例)
- (二)直接声明切片字面量
-
- [1. 基础示例](#1. 基础示例)
- [2. 多角度举例](#2. 多角度举例)
-
- 示例1:字符串类型切片
- 示例2:布尔类型切片
- 示例3:空元素切片(长度为0)
- [示例4:只声明不初始化(nil 切片)](#示例4:只声明不初始化(nil 切片))
- [(三)字面量声明 vs make 函数创建(核心对比)](#(三)字面量声明 vs make 函数创建(核心对比))
-
- [1. 切片字面量 `[]int{10,20,30}`](#1. 切片字面量
[]int{10,20,30}) - [2. make 函数 `make([]int, 3, 5)`](#2. make 函数
make([]int, 3, 5))
- [1. 切片字面量 `[]int{10,20,30}`](#1. 切片字面量
- [五、空切片(nil 切片)](#五、空切片(nil 切片))
-
- [(一)定义与核心特性](#(一)定义与核心特性)
- [(二)、nil 切片 VS make([]int, 0) 深度对比](#(二)、nil 切片 VS make([]int, 0) 深度对比)
- [(三)、make 创建切片的 4 种方式对比(零基础必看)](#(三)、make 创建切片的 4 种方式对比(零基础必看))
-
- [① var s []int → nil 切片(空切片)](#① var s []int → nil 切片(空切片))
- [② s1 := make([]int, 0) → 零长度切片](#② s1 := make([]int, 0) → 零长度切片)
- [③ s2 := make([]int, 10) → 长度=容量](#③ s2 := make([]int, 10) → 长度=容量)
- [④ s3 := make([]int, 0, 10) → 预分配容量](#④ s3 := make([]int, 0, 10) → 预分配容量)
- [🔥 完整代码示例(4 种切片一次性演示)](#🔥 完整代码示例(4 种切片一次性演示))
- [✅ 运行结果(真实输出)](#✅ 运行结果(真实输出))
- [🔍 结果极简说明](#🔍 结果极简说明)
- 六、切片的操作
- 七、切片与数组的关系
- 八、切片的注意事项
- 九、总结
- [技术标签:#Go语言 #切片 #数据结构 #编程入门 #动态数组 #nil切片](#Go语言 #切片 #数据结构 #编程入门 #动态数组 #nil切片)
Go语言切片:动态灵活的数据序列
一、引言
大家好!在上一篇博客中,我们深入了解了 Go 语言数组,知晓它如同固定格子数量的书架,具有长度固定等特性。今天,我们将走进与数组紧密相关的切片的世界。切片就像是一个可以根据需求灵活调整大小的书架,为我们在编程中处理数据带来了极大的便利。在深入了解切片的各种操作之前,先让我们认识一下切片的内部结构,这将有助于我们更好地理解切片的行为和特性。
二、切片是什么
切片是 Go 语言中灵活且动态的数据序列,它基于数组实现,但克服了数组长度固定、无法扩展的缺点。
你可以把切片理解成:
底层数组的一个"动态窗口"------通过这个窗口,你可以观察、修改数组的一部分或全部数据,并且窗口大小可以随时变化。
切片自身不真正存储数据,数据都存放在底层数组中,切片只是对数组的引用 。
在 Go 运行时中,切片本质是一个固定的结构体,包含3个核心字段:
go
// 切片的真实底层结构(runtime 内部定义)
type slice struct {
array unsafe.Pointer // 1. 指针:指向底层数组的起始元素
len int // 2. 长度:当前可访问的元素个数
cap int // 3. 容量:底层数组从指针开始的最大可用长度
}
三、切片的结构
切片在 Go 运行时内部,是一个固定的结构体,包含三个关键字段:
- 指向底层数组的指针:记录切片从数组的哪个位置开始
- 长度(len) :切片当前实际拥有的元素个数
- 容量(cap):从指针位置到数组末尾,一共还能存放多少元素(不扩容前提下)
切片结构可视化
+---------------------------+
| 切片结构体 Slice |
+---------------------------+
| 指针 *int → 指向数组[0] |
| 长度 len = 3 |
| 容量 cap = 5 |
+---------------------------+
│
▼
+-------------------------------+
| 底层数组 [5]int |
+-------------------------------+
| 0 | 1 | 2 | 3 | 4 |
+-------------------------------+
└─────────┘ └──────────┘
len=3(可用) 剩余空间
简单总结:
- len:你现在能用多少
- cap:你最多还能用多少(不扩容)
四、切片的创建
(一)、创建切片的标准方式:make 函数
开发中,make 是创建切片最推荐、最常用的方式,它会自动分配底层数组。
1. make 函数完整语法
// 语法
make([]T, len, cap)
// 参数说明
// T:切片存储的元素类型(int/string/struct 等)
// len:切片长度(必填)
// cap:切片容量(可选,省略则默认等于长度)
回到开头的图解:对应完整代码
我们把结构图变成可运行的 Go 代码,验证结构一致性:
go
package main
import "fmt"
func main() {
// 1. 底层数组:5个元素 [0,1,2,3,4]
arr := [5]int{0, 1, 2, 3, 4}
// 2. 创建切片:指向数组首元素,len=3,cap=5
s := arr[:3]
// 3. 输出验证
fmt.Println("切片值:", s)
fmt.Println("长度 len:", len(s))
fmt.Println("容量 cap:", cap(s))
}
运行结果
切片值: [0 1 2]
长度 len: 3
容量 cap: 5
和我们的结构图完全一致!
2、make 创建切片的 3个核心注意点(必看)
(1). 容量不能小于长度
这是最常见的错误,编译直接报错:
go
// 错误:cap 不能 < len
s := make([]int, 5, 3)
规则 :容量 ≥ 长度 永远成立。
(2). make 创建的切片会默认初始化
make 会分配内存并将元素清零(int=0,string="",bool=false),不会出现野值。
(3). 预留容量,避免频繁扩容(性能关键)
如果预先知道数据量,指定容量 > 长度,可以避免切片自动扩容:
go
// 推荐:提前预留足够容量,性能更高
userList := make([]User, 0, 100)
3. 2种常用创建示例
go
package main
import "fmt"
func main() {
// 1. 只传长度:容量 = 长度
s1 := make([]int, 3)
fmt.Println("s1:", s1, "len:", len(s1), "cap:", cap(s1))
// 2. 传长度+容量:预留空间,提升性能
s2 := make([]int, 3, 5)
fmt.Println("s2:", s2, "len:", len(s2), "cap:", cap(s2))
}
运行结果
s1: [0 0 0] len: 3 cap: 3
s2: [0 0 0] len: 3 cap: 5
我给你大幅扩充、丰富、专业、零基础友好 地重写这一段,多角度举例 + 和 make 清晰对比,直接替换你原文即可,博客瞬间更饱满、更专业!
(二)直接声明切片字面量
除了使用 make 函数,我们还可以直接声明并初始化切片字面量 ,这种方式和声明数组有些相似,但不需要指定长度,Go 会自动推断为切片。
它的特点:
- 写法直观、简洁
- 自动分配底层数组
- 自动设置
len = cap = 元素个数 - 适合已知具体元素的场景
1. 基础示例
go
package main
import "fmt"
func main() {
// 直接声明并初始化切片
s := []int{10, 20, 30}
fmt.Println("切片:", s)
fmt.Println("长度len:", len(s))
fmt.Println("容量cap:", cap(s))
}
运行结果:
切片: [10 20 30]
长度len: 3
容量cap: 3
2. 多角度举例
示例1:字符串类型切片
s := []string{"Go", "Java", "Python"}
示例2:布尔类型切片
s := []bool{true, false, true}
示例3:空元素切片(长度为0)
s := []int{}
⚠️ 注意:这是零长度切片,不是 nil 切片。
示例4:只声明不初始化(nil 切片)
go
var s []int
(三)字面量声明 vs make 函数创建(核心对比)
这是初学者必须掌握的知识点,两种方式适用场景完全不同:
1. 切片字面量 []int{10,20,30}
- 适合:已经知道具体元素值
- 自动设置:
len = cap = 元素数量 - 无需手动管理长度和容量
- 代码更短、更直观
2. make 函数 make([]int, 3, 5)
- 适合:暂时不知道元素值,但需要预先分配空间
- 可手动指定
len和cap - 元素自动初始化为零值(0、false、"")
- 适合后续大量
append操作,性能更高
| 创建方式 | 适用场景 | len/cap | 元素初始值 |
|---|---|---|---|
[]int{10,20,30} |
已知具体元素 | len=cap=3 | 手动赋值 |
make([]int, 3) |
预分配长度,后续填充 | len=cap=3 | 自动零值 |
make([]int, 0, 10) |
预分配容量,后续append | len=0, cap=10 | 无元素 |
总结
- 切片字面量:简单直观,适合已知元素的场景;
- make:灵活可控,适合预先分配内存、追求性能的场景;
- 两者最终都会创建切片并分配底层数组,只是使用姿势不同。
五、空切片(nil 切片)
(一)定义与核心特性
Go 官方定义:未初始化的切片变量就是空切片,也叫 nil 切片。
声明方式:
go
var nilSlice []int
为什么它叫空切片?
核心原因:切片内部的指针字段是空指针(nil),没有指向任何底层数组内存地址。
空切片三大特征:
- 内部指针:为空 nil,不指向任何底层数组
- 长度
len = 0,容量cap = 0 - 没有分配任何底层数组内存,只是一个切片结构体零值
代码示例 + 打印地址
go
package main
import (
"fmt"
"unsafe"
)
func main() {
var nilSlice []int
// 打印切片信息、是否为nil、底层数组地址
fmt.Printf("空切片(nil切片):%v\n", nilSlice)
fmt.Printf("len=%d cap=%d\n", len(nilSlice), cap(nilSlice))
fmt.Printf("是否为nil:%t\n", nilSlice == nil)
// 强制打印底层数组指针地址
fmt.Printf("底层数组指针地址:%#x\n", *(*uintptr)(unsafe.Pointer(&nilSlice)))
}
运行结果:
空切片(nil切片):[]
len=0 cap=0
是否为nil:true
底层数组指针地址:0x0
解读 :地址为 0x0 代表指针是空地址,没有绑定任何底层数组,这就是空切片名字的由来。
空切片内存结构示意图
+----------------------+
| 空切片 结构体 |
+----------------------+
| 指针:nil(地址0x0) |
| len:0 |
| cap:0 |
+----------------------+
无
无底层数组
(二)、nil 切片 VS make([]int, 0) 深度对比
很多初学者迷惑:
两个打印出来都是 [],len 和 cap 也都是 0,到底有什么区别?
1. 核心本质区别
var s []int:nil 空切片
指针 = nil,地址为 0,无底层数组s1 := make([]int, 0):非空零长度切片
指针 有真实内存地址 ,已分配底层数组,只是没有存放元素
2. 完整对比代码(打印地址+nil判断)
go
package main
import (
"fmt"
"unsafe"
)
func main() {
// 1. nil 空切片
var nilSlice []int
fmt.Println("========== nil 空切片 ==========")
fmt.Printf("值:%v\n", nilSlice)
fmt.Printf("len=%d cap=%d\n", len(nilSlice), cap(nilSlice))
fmt.Printf("是否nil:%t\n", nilSlice == nil)
fmt.Printf("底层数组指针地址:%#x\n\n", *(*uintptr)(unsafe.Pointer(&nilSlice)))
// 2. make([]int, 0) 创建的切片
emptySlice := make([]int, 0)
fmt.Println("====== make([]int, 0) 切片 ======")
fmt.Printf("值:%v\n", emptySlice)
fmt.Printf("len=%d cap=%d\n", len(emptySlice), cap(emptySlice))
fmt.Printf("是否nil:%t\n", emptySlice == nil)
fmt.Printf("底层数组指针地址:%#x\n", *(*uintptr)(unsafe.Pointer(&emptySlice)))
}
运行结果示例
========== nil 空切片 ==========
值:[]
len=0 cap=0
是否nil:true
底层数组指针地址:0x0
====== make([]int, 0) 切片 ======
值:[]
len=0 cap=0
是否nil:false
底层数组指针地址:0x1400001a080
结果通俗解读
- nil 空切片:指针地址是
0x0,空指针,没分配数组; - make([]int,0):指针是正常内存地址,已经分配了底层数组,只是长度为0、没放元素;
- 只有
var s []int是真正 nil,make 创建的切片永远不等于 nil。
内存结构对比图
【nil 空切片】
+----------------------+
| 指针:nil(0x0) |
| len:0 |
| cap:0 |
+----------------------+
无底层数组
【make([]int, 0) 切片】
+----------------------+
| 指针:0x1400001a080 |
| len:0 |
| cap:0 |
+----------------------+
│
▼
底层空数组内存
(已分配,只是无元素)
小结
- 空切片叫空切片,根本原因是内部指针为 nil 空地址;
var s []int是 nil 空切片,无底层数组;make([]int,0)不是 nil,有底层数组,只是长度为0;- 两者肉眼打印一样,底层内存结构完全不同,可通过指针地址精准区分。
(三)、make 创建切片的 4 种方式对比(零基础必看)
为了让你彻底理解 len 和 cap,我们一次性对比 4 种最常见的切片创建方式,帮你一次性理清所有疑惑。
① var s []int → nil 切片(空切片)
- 指针:nil
- len:0
- cap:0
- 无底层数组
② s1 := make([]int, 0) → 零长度切片
- 指针:有真实地址
- len:0
- cap:0
- 已分配空数组
③ s2 := make([]int, 10) → 长度=容量
- len:10
- cap:10
- 元素自动初始化为 0
- 适合:已知元素数量
④ s3 := make([]int, 0, 10) → 预分配容量
- len:0
- cap:10
- 已分配数组,预留空间
- 性能最优,推荐使用
超级总结表(一眼看懂区别)
切片写法 | 指针 | len | cap | 底层数组
------------------------|---------|-----|-----|----------------
var s []int | nil | 0 | 0 | 无
s1 := make([]int, 0) | 有地址 | 0 | 0 | 已分配空数组
s2 := make([]int, 10) | 有地址 | 10 | 10 | 已分配长度10数组
s3 := make([]int, 0, 10)| 有地址 | 0 | 10 | 已分配容量10数组
🔥 完整代码示例(4 种切片一次性演示)
go
package main
import (
"fmt"
"unsafe"
)
func main() {
fmt.Println("========================")
fmt.Println("① var s []int → nil切片")
var s []int
fmt.Printf("切片: %v\n", s)
fmt.Printf("len: %d, cap: %d\n", len(s), cap(s))
fmt.Printf("是否为nil: %t\n", s == nil)
fmt.Printf("底层指针地址: %#x\n\n", *(*uintptr)(unsafe.Pointer(&s)))
fmt.Println("========================")
fmt.Println("② s1 := make([]int, 0) → 零长度切片")
s1 := make([]int, 0)
fmt.Printf("切片: %v\n", s1)
fmt.Printf("len: %d, cap: %d\n", len(s1), cap(s1))
fmt.Printf("是否为nil: %t\n", s1 == nil)
fmt.Printf("底层指针地址: %#x\n\n", *(*uintptr)(unsafe.Pointer(&s1)))
fmt.Println("========================")
fmt.Println("③ s2 := make([]int, 10) → 长度=容量")
s2 := make([]int, 10)
fmt.Printf("切片: %v\n", s2)
fmt.Printf("len: %d, cap: %d\n", len(s2), cap(s2))
fmt.Printf("是否为nil: %t\n", s2 == nil)
fmt.Printf("底层指针地址: %#x\n\n", *(*uintptr)(unsafe.Pointer(&s2)))
fmt.Println("========================")
fmt.Println("④ s3 := make([]int, 0, 10) → 预分配容量")
s3 := make([]int, 0, 10)
fmt.Printf("切片: %v\n", s3)
fmt.Printf("len: %d, cap: %d\n", len(s3), cap(s3))
fmt.Printf("是否为nil: %t\n", s3 == nil)
fmt.Printf("底层指针地址: %#x\n", *(*uintptr)(unsafe.Pointer(&s3)))
}
✅ 运行结果(真实输出)
========================
① var s []int → nil切片
切片: []
len: 0, cap: 0
是否为nil: true
底层指针地址: 0x0
========================
② s1 := make([]int, 0) → 零长度切片
切片: []
len: 0, cap: 0
是否为nil: false
底层指针地址: 0x1400001a080
========================
③ s2 := make([]int, 10) → 长度=容量
切片: [0 0 0 0 0 0 0 0 0 0]
len: 10, cap: 10
是否为nil: false
底层指针地址: 0x140000a2000
========================
④ s3 := make([]int, 0, 10) → 预分配容量
切片: []
len: 0, cap: 10
是否为nil: false
底层指针地址: 0x140000a2050
🔍 结果极简说明
- nil 切片地址是 0x0,真正的空,无底层数组;
- make([]int, 0) 有地址、有数组,只是长度为 0;
- make([]int, 10) 直接创建 10 个元素,自动赋 0;
- make([]int, 0, 10) 预分配空间,append 不扩容,性能最高。
六、切片的操作
(一)访问元素
切片元素的访问方式与数组相似,借助索引获取或修改元素值。索引从 0 起始,最大索引为切片长度减 1。例如:
go
package main
import "fmt"
func main() {
slice := []int{10, 20, 30}
// 获取第一个元素
firstElement := slice[0]
fmt.Println("第一个元素:", firstElement)
// 修改第二个元素
slice[1] = 25
fmt.Println("修改后的切片:", slice)
}
运行结果说明:程序先定义切片 slice,接着获取其首个元素并打印,随后修改第二个元素的值并打印修改后的切片。程序输出为:
第一个元素: 10
修改后的切片: [10 25 30]
(二)遍历切片
与数组相同,我们可利用 for 循环遍历切片,常见方式有普通 for 循环与 for - range 循环。
- 普通
for循环遍历:
go
package main
import "fmt"
func main() {
slice := []int{10, 20, 30}
for i := 0; i < len(slice); i++ {
fmt.Printf("索引 %d 处的元素: %d\n", i, slice[i])
}
}
运行结果说明:通过普通 for 循环遍历切片 slice,逐个输出各元素的索引与值。程序输出为:
索引 0 处的元素: 10
索引 1 处的元素: 20
索引 2 处的元素: 30
for - range循环遍历:
go
package main
import "fmt"
func main() {
slice := []int{10, 20, 30}
for index, value := range slice {
fmt.Printf("索引 %d 对应的值: %d\n", index, value)
}
}
运行结果说明:运用 for - range 循环遍历切片 slice,同时获取并打印每个元素的索引与值。程序输出与普通 for 循环遍历类似:
索引 0 对应的值: 10
索引 1 对应的值: 20
索引 2 对应的值: 30
(三)追加元素
切片的显著优势之一是能够动态追加元素,我们借助 append 函数实现此操作。例如:
go
package main
import "fmt"
func main() {
slice := []int{10, 20}
// 追加一个元素
newSlice := append(slice, 30)
fmt.Println("追加元素后的切片:", newSlice)
// 追加多个元素
newSlice = append(newSlice, 40, 50)
fmt.Println("再次追加元素后的切片:", newSlice)
}
运行结果说明:程序先定义切片 slice,接着使用 append 函数追加一个元素,然后再次追加多个元素,并分别打印每次追加后的切片。程序输出为:
追加元素后的切片: [10 20 30]
再次追加元素后的切片: [10 20 30 40 50]
这里追加元素有很多注意事项,其原理我们将在下一章详细介绍。
七、切片与数组的关系
切片构建于数组之上,是对数组的封装,为我们提供了更为灵活的操作方式。切片自身并不存储数据,而是依托底层数组存储,切片仅是对数组部分内容的引用。
数组与切片的区别主要体现在以下方面:
- 长度可变:数组长度一经确定便不可更改;切片长度则可按需动态增减。
- 内存分配:数组在声明时即确定所需内存空间;切片的内存分配更为灵活,随元素增加自动扩容。
- 赋值和传递:数组属于值类型,赋值与函数传参时会复制整个数组;切片是引用类型,赋值和传参时传递的是切片结构体(含指针、长度和容量),而非底层数组的副本,效率更高。
八、切片的注意事项
(一)切片越界
与数组相同,切片访问也严禁越界。切片的有效索引范围是从 0 到切片长度减 1。若访问超出此范围,程序运行时将引发 panic。例如:
go
package main
import "fmt"
func main() {
slice := []int{10, 20, 30}
// 以下代码会导致越界错误
// outOfRange := slice[3]
// 若取消注释,运行时会报错:panic: runtime error: index out of range [3] with length 3
}
运行结果说明:若取消注释试图访问越界索引 3,程序将引发运行时错误,提示索引超出范围。当前代码注释掉了越界访问部分,程序正常运行无输出。
(二)切片的内存管理
尽管切片的自动扩容机制带来诸多便利,但内存管理仍需留意。频繁的扩容操作可能引发性能问题,因为每次扩容都涉及内存重新分配与数据复制。实际应用中,若能预先估算切片所需大致容量,创建切片时指定合适容量,可减少不必要的扩容。
九、总结
通过本文,我们全方位了解了Go语言中的切片,涵盖切片的结构、空切片特性,以及 make 函数创建切片的不同形式。切片作为基于数组的动态数据结构,具备灵活的长度调整、高效的内存管理及便捷的操作方式。我们学习了切片的创建、结构、操作方法,以及它与数组的关系和使用注意事项。
下一篇
下一篇,我们将深入探讨Go语言切片的扩容机制与策略、追加元素的注意事项,进一步丰富对切片的认知与运用。
关注我,点赞👍、收藏⭐本篇内容,我们一同在后续博客中持续探索Go语言切片的更多奥秘。