在 Go 语言中,切片(Slice) 是数组的「动态视图」,也是开发中最常用的复合数据类型。它解决了数组「长度固定」的痛点,支持动态扩容,且本质是对底层数组的引用,兼顾灵活性和性能。
一、切片的核心本质
切片不是独立的存储结构,而是一个包含 3 个字段的引用类型结构体:
type slice struct {ptr *[]T // 指向底层数组的指针len int // 切片当前可用元素的长度(可访问的索引范围:0 ~ len-1)cap int // 切片的容量(底层数组从指针位置开始的总长度,cap >= len)
}
简单理解:切片 = 「数组指针」+「当前长度」+「最大容量」。
二、切片的声明与初始化
1. 基本声明(未初始化,值为 nil)
// 格式:var 切片名 []元素类型
var s1 []int // nil 切片,len=0,cap=0
var s2 []string // nil 切片,len=0,cap=0
fmt.Println(s1 == nil) // true
2. 常用初始化方式
方式1:从数组/已有切片截取(最核心)
格式:切片 = 原数组/切片[起始索引:结束索引](左闭右开,包含起始,不包含结束)。
// 1. 从数组截取
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[1:4] // 截取索引1~3,结果 [2,3,4],len=3,cap=4(底层数组从索引1到末尾共4个元素)
s2 := arr[:3] // 省略起始索引,默认从0开始,结果 [1,2,3],len=3,cap=5
s3 := arr[2:] // 省略结束索引,默认到数组末尾,结果 [3,4,5],len=3,cap=3
s4 := arr[:] // 截取全部,结果 [1,2,3,4,5],len=5,cap=5// 2. 从切片截取
s5 := s1[1:2] // 从s1截取索引1~1,结果 [3],len=1,cap=3(继承s1的cap)
方式2:字面量初始化(直接创建)
// 格式:切片名 := []元素类型{元素1, 元素2, ...}
s1 := []int{1, 2, 3} // len=3,cap=3(底层自动创建长度为3的数组)
s2 := []string{"a", "b"} // len=2,cap=2
方式3:make 函数初始化(指定长度/容量,推荐)
适用于提前知道切片大致大小,避免频繁扩容,格式:make([]T, len, cap)(cap 可选,默认等于 len)。
// 格式1:只指定长度(cap = len)
s1 := make([]int, 3) // len=3,cap=3,元素默认值0 → [0,0,0]// 格式2:指定长度和容量(cap >= len)
s2 := make([]int, 3, 5) // len=3,cap=5,前3个元素为0,后2个为底层数组预留空间
三、切片的核心操作
1. 访问/修改元素
与数组一致,通过索引访问,修改切片元素会同步修改底层数组(因为是引用):
package main
import "fmt"func main() {arr := [5]int{1,2,3,4,5}s := arr[1:4] // [2,3,4]// 访问元素fmt.Println(s[0]) // 2// 修改切片元素 → 底层数组也会变s[1] = 300fmt.Println(s) // [2,300,4]fmt.Println(arr) // [1,2,300,4,5](原数组被修改)
}
2. 遍历切片
与数组完全一致,推荐用 for-range:
s := []string{"Go", "Python", "Java"}// 方式1:下标遍历
for i := 0; i < len(s); i++ {fmt.Println(i, s[i])
}// 方式2:for-range(推荐)
for i, v := range s {fmt.Printf("索引%d:%s\n", i, v)
}// 忽略索引
for _, v := range s {fmt.Println(v)
}
3. 动态扩容(append 函数)
append 是切片动态扩容的核心函数,格式:新切片 = append(原切片, 元素1, 元素2, ...)。
- 若
len < cap:直接在底层数组空闲位置添加元素,len+1,cap 不变; - 若
len == cap:自动扩容(通常扩容为原 cap 的 2 倍,小切片可能扩更多),创建新底层数组,拷贝原元素,再添加新元素。
s := make([]int, 3, 5) // len=3, cap=5, [0,0,0]
s = append(s, 1) // len=4, cap=5, [0,0,0,1]
s = append(s, 2, 3) // len=6, cap=10(扩容), [0,0,0,1,2,3]// 切片拼接(第二个切片后加 ...)
s1 := []int{1,2}
s2 := []int{3,4}
s3 := append(s1, s2...) // [1,2,3,4]
⚠️ 注意:append 可能返回新切片(扩容时),因此必须接收返回值,否则原切片可能失效。
4. 切片的拷贝(copy 函数)
copy 用于将一个切片的元素拷贝到另一个切片,格式:拷贝的元素数 = copy(目标切片, 源切片)。
- 拷贝数量取两个切片长度的较小值;
- 拷贝是值拷贝,修改目标切片不会影响源切片。
s1 := []int{1,2,3,4}
s2 := make([]int, 2) // [0,0]n := copy(s2, s1) // 拷贝2个元素,s2 = [1,2],n=2
fmt.Println(s2, n)// 完整拷贝(目标切片长度 >= 源切片)
s3 := make([]int, len(s1))
copy(s3, s1) // s3 = [1,2,3,4]
5. 切片作为函数参数
切片是引用类型,传参时仅拷贝「切片头」(指针、len、cap,共 24 字节),函数内修改切片元素会影响原切片:
func modifySlice(s []int) {s[0] = 100 // 修改底层数组元素s = append(s, 4) // 若扩容,s指向新数组,原切片不受影响
}func main() {s := []int{1,2,3}modifySlice(s)fmt.Println(s) // [100,2,3](元素被修改,但append的4未体现,因为扩容后函数内s是新切片)
}
四、切片的常见坑(新手必避)
-
nil 切片 vs 空切片
- nil 切片:
var s []int→ ptr=nil,len=0,cap=0; - 空切片:
s := []int{}或s := make([]int, 0)→ ptr≠nil(指向空数组),len=0,cap=0; - 二者均可正常使用
append/len/cap,判断切片是否为空应使用len(s) == 0,而非s == nil。
- nil 切片:
-
切片共享底层数组
多个切片截取同一数组时,修改其中一个切片的元素会影响其他切片:arr := [5]int{1,2,3,4,5} s1 := arr[1:3] // [2,3] s2 := arr[2:4] // [3,4] s1[1] = 300 // s1=[2,300], s2=[300,4], arr=[1,2,300,4,5] -
扩容后切片解耦
切片扩容后会指向新数组,原切片与新切片不再共享底层数组:s1 := []int{1,2} // len=2, cap=2 s2 := s1 s1 = append(s1, 3) // 扩容,s1指向新数组 [1,2,3] s2[0] = 100 // s2仍指向原数组 [100,2] fmt.Println(s1) // [1,2,3] fmt.Println(s2) // [100,2]
五、切片 vs 数组(核心对比)
| 特性 | 数组(Array) | 切片(Slice) |
|---|---|---|
| 类型标识 | [N]T(长度是类型的一部分) |
[]T(无长度) |
| 长度 | 固定 | 动态(len 可通过 append 改变) |
| 容量 | 等于长度 | 独立于长度(cap >= len) |
| 底层 | 直接存储元素 | 引用底层数组 |
| 传参 | 值拷贝(拷贝整个数组) | 引用传递(拷贝切片头,开销小) |
| 初始化 | 可直接声明 | 需字面量/make/截取 |
总结
- Go 切片是引用类型,核心结构为「指针+长度+容量」,底层依赖数组,支持动态扩容;
- 切片的核心操作:
append(扩容)、copy(拷贝)、截取([start:end]),append必须接收返回值; - 切片传参是引用传递(修改元素影响原切片),但扩容会生成新切片,需注意共享底层数组的副作用;
- 判断切片是否为空用
len(s) == 0,而非s == nil,实际开发中切片几乎完全替代数组。
