在Go语言中,切片(Slice)是最常用的数据结构之一,它作为动态数组的实现,解决了数组长度固定的局限性,同时保留了数组的高效性。
在学习切片之前,我们首先要理解数组(Array) 的局限性——Go语言中的数组是长度固定的连续内存空间,一旦声明,长度便无法修改。例如:
// 数组a的长度为5,无法动态添加或删除元素
var a [5]int = [5]int{1,2,3,4,5}当业务场景需要动态调整数据长度时,数组就显得力不从心。而切片(Slice) 作为数组的"视图",本质是一个包含三个字段的结构体:
len() 函数获取)cap() 函数获取)这种结构既保留了数组的内存连续性(高效访问),又支持动态长度调整(动态扩容),成为Go语言中处理序列数据的首选。
切片的声明和初始化是使用切片的基础,不同场景下需要选择合适的方式。以下结合代码示例详细讲解。
通过 var 关键字声明切片但不初始化时,切片会处于nil状态(指针为nil,长度和容量均为0)。
package main
import "fmt"
func main() {
// 1. 仅声明,未初始化:nil切片
var a []int
// 2. 声明并初始化空切片(非nil)
var b []int = []int{}
// 3. make创建空切片(非nil)
c := make([]int, 0)
// 判断是否为nil
if a == nil {
fmt.Println("a==nil") // 输出:a==nil
}
fmt.Println("a:", a, "len(a):", len(a), "cap(a):", cap(a)) // 输出:a: [] len(a): 0 cap(a): 0
if b == nil {
fmt.Println("b==nil") // 无输出(b非nil)
}
fmt.Println("b:", b, "len(b):", len(b), "cap(b):", cap(b)) // 输出:b: [] len(b): 0 cap(b): 0
if c == nil {
fmt.Println("c==nil ") // 无输出(c非nil)
}
fmt.Println("c:", c, "len(c):", len(c), "cap(c):", cap(c)) // 输出:c: [] len(c): 0 cap(c): 0
}var s []T 声明,未初始化(如示例中的 a),指针为nil,常用于表示"未赋值的空状态"(如函数返回错误时的空结果)。[]T{} 或 make([]T, 0) 创建(如示例中的 b 和 c),指针非nil,仅长度为0,常用于表示"明确的空集合"(如查询结果为空但无错误)。len(s) == 0,而非 s == nil!因为空切片的 len 也是0,但 s != nil,若用 s == nil 判断会误判。切片可以通过数组切片表达式(array[low:high])从数组中截取一部分,形成新的切片。切片的底层数组就是原数组,因此修改切片会影响原数组。
package main
import "fmt"
func main() {
// 1. 定义一个长度为5的数组
a := [5]int{55, 56, 57, 58, 59}
// 2. 基于数组创建切片:左闭右开区间 [low, high)
b := a[1:4] // 截取数组索引1、2、3的元素(56,57,58)
fmt.Println("切片b:", b) // 输出:切片b: [56 57 58]
fmt.Println("b的长度len(b):", len(b)) // 输出:b的长度len(b): 3(元素个数)
fmt.Println("b的容量cap(b):", cap(b)) // 输出:b的容量cap(b): 4(从索引1到数组末尾共4个元素:56,57,58,59)
fmt.Printf("b的类型: %T\n", b) // 输出:b的类型: []int(切片类型)
// 3. 切片再次切片(基于切片b创建新切片c)
c := b[:len(b)] // 截取b的所有元素(等同于b[0:3])
fmt.Println("切片c:", c) // 输出:切片c: [56 57 58]
fmt.Printf("c的类型: %T\n", c) // 输出:c的类型: []int
}array[low:high:max](可选max参数,限制切片的最大容量为 max - low)low 省略时默认0(如 a[:4] 等同于 a[0:4])high 省略时默认数组长度(如 a[1:] 等同于 a[1:5])low 和 high 都省略时表示整个数组(如 a[:] 等同于 a[0:5])cap(切片) = len(数组) - lowcap(切片) = max - low(避免切片越界访问原数组后续元素)切片是数组的视图,修改切片元素会同步修改原数组。例如:
b[0] = 100 // 修改切片b的第一个元素
fmt.Println(a) // 输出:[55 100 57 58 59](原数组a的索引1元素被修改)make 函数是Go语言用于创建引用类型(切片、映射、通道)的内置函数,创建切片时可以直接指定长度(len) 和容量(cap),底层会自动分配一个匿名数组。
package main
import "fmt"
func main() {
// make(类型, 长度, 容量):容量可选,默认等于长度
d := make([]int, 5, 10) // 类型int,长度5(初始5个0),容量10
fmt.Println("切片d:", d) // 输出:切片d: [0 0 0 0 0](长度5,元素默认初始化)
fmt.Println("d的长度len(d):", len(d)) // 输出:d的长度len(d): 5
fmt.Println("d的容量cap(d):", cap(d)) // 输出:d的容量cap(d): 10
fmt.Printf("d的类型: %T\n", d) // 输出:d的类型: []int
}// 扩展示例:make创建切片后追加元素
func main6() {
// 创建长度5、容量10的字符串切片(初始5个空字符串)
var a = make([]string, 5, 10)
// 向切片追加10个元素(从索引5开始填充)
for i := 0; i < 10; i++ {
a = append(a, fmt.Sprintf("%v", i))
}
fmt.Println("最终切片a:", a)
// 输出:最终切片a: [ 0 1 2 3 4 5 6 7 8 9](前5个为空字符串,后10个为数字)
}make([]T, len):容量默认等于长度(如 make([]int, 3) 等同于 make([]int, 3, 3))。掌握切片的核心操作是实际开发的基础,以下结合代码示例逐一讲解。
切片的赋值是引用传递,即新切片与原切片指向同一个底层数组,修改其中一个会影响另一个。
package main
import "fmt"
func main() {
// 1. 创建切片a(长度3,容量3,底层数组[0,0,0])
a := make([]int, 3) // [0 0 0]
// 2. 切片赋值:b与a共享底层数组
b := a
// 3. 修改b的元素
b[0] = 100
// 4. 打印a和b:两者都被修改
fmt.Println("切片a:", a) // 输出:切片a: [100 0 0]
fmt.Println("切片b:", b) // 输出:切片b: [100 0 0]
}a 和 b 的指针、长度、容量完全相同,指向同一个底层数组。b[0] = 100),底层数组的对应位置会被修改,因此所有引用该数组的切片都会看到变化。copy 函数(见3.3节)或重新创建切片(如 b := append([]int{}, a...))。切片的遍历与数组类似,支持索引遍历和for range遍历,后者更简洁,是Go语言的推荐写法。
package main
import "fmt"
func main() {
// 定义一个切片c
c := []int{1, 2, 3, 4, 5}
// 方式1:基于索引遍历(适合需要索引和元素的场景)
fmt.Println("=== 索引遍历 ===")
for i := 0; i < len(c); i++ {
fmt.Printf("索引%d: %d\n", i, c[i])
}
// 输出:
// 索引0: 1
// 索引1: 2
// 索引2: 3
// 索引3: 4
// 索引4: 5
// 方式2:for range遍历(适合仅需元素或同时需要索引的场景)
fmt.Println("\n=== for range遍历 ===")
for index, value := range c {
fmt.Printf("索引%d: %d\n", index, value)
}
// 输出与索引遍历一致
}_ 忽略(如 for _, v := range c { ... })。for _, v := range c[1:3] { ... } 遍历索引1和2的元素)。由于切片赋值是引用传递,若需完全独立的切片(不共享底层数组),需使用内置的 copy 函数实现深拷贝。copy 函数的签名为:
func copy(dst, src []T) int // 返回值为实际拷贝的元素个数package main
import "fmt"
func main5() {
// 1. 原切片a(底层数组[1,2,3,4,5])
a := []int{1, 2, 3, 4, 5}
// 2. 创建目标切片b(长度5,容量5,底层数组[0,0,0,0,0])
b := make([]int, 5, 5)
// 3. 切片赋值:c与b共享底层数组(用于验证拷贝效果)
var c []int = b
// 4. 拷贝src(a)到dst(b):拷贝5个元素
copy(b, a)
// 5. 修改b的元素(验证是否影响a和c)
b[0] = 100
// 6. 打印结果:a未变,b和c被修改(b和c共享底层数组)
fmt.Println("原切片a:", a) // 输出:原切片a: [1 2 3 4 5](未受影响)
fmt.Println("目标切片b:", b) // 输出:目标切片b: [100 2 3 4 5](被修改)
fmt.Println("切片c:", c) // 输出:切片c: [100 2 3 4 5](与b共享底层数组)
}len(dst) 和 len(src) 中的较小值(如 dst 长度3,src 长度5,仅拷贝3个元素)。dst 和 src 指向不同的底层数组,修改其中一个不会影响另一个。copy 是"元素级拷贝",若切片元素是引用类型(如切片、映射),则仅拷贝指针(仍共享底层数据),需注意嵌套场景的深拷贝问题。dst 的长度等于 src(如 dst := make([]T, len(src)),再 copy(dst, src))。copy(dst, src[1:3]) 拷贝 src 的索引1和2的元素)。Go语言没有专门的 delete 函数用于切片,而是通过切片表达式+append函数实现元素删除,核心思路是:将删除位置前后的元素拼接成新切片。
package main
import "fmt"
func main() {
// 1. 定义一个字符串切片(包含4个元素)
e := []string{"北京", "上海", "广州", "深圳"}
fmt.Println("删除前:", e) // 输出:删除前: [北京 上海 广州 深圳]
// 2. 删除索引为2的元素("广州")
// 原理:将e[:2](["北京","上海"])和e[3:](["深圳"])拼接
e = append(e[:2], e[3:]...)
fmt.Println("删除后:", e) // 输出:删除后: [北京 上海 深圳]
}删除切片 s 中索引为 index 的元素:
s = append(s[:index], s[index+1:]...)s[:index]:切片 s 中索引 0 到 index-1 的元素(左半部分)。s[index+1:]...:切片 s 中索引 index+1 到末尾的元素,... 表示将切片展开为可变参数。append 函数:将左半部分和右半部分拼接,返回新的切片(可能指向新的底层数组)。index 在 [0, len(s)-1] 范围内,否则会导致切片越界(运行时panic)。s[index] = nil 或 s[index] = 0)。s = s[:index] 删除索引 index 及之后的所有元素)。append 函数是切片动态扩容的核心,当切片的长度(len)等于容量(cap)时,继续追加元素会触发扩容(分配新的底层数组,拷贝原数据,更新切片的指针、长度和容量)。理解扩容规则对优化切片性能至关重要。
append 函数的签名为:
func append(s []T, vs ...T) []T // 返回新的切片s:原切片。vs...:可变参数,可传入多个元素或另一个切片(需用 ... 展开)。package main
import "fmt"
func main() {
var a []int // nil切片
// 1. 追加单个元素
a = append(a, 10)
fmt.Println("追加单个元素后:", a) // 输出:追加单个元素后: [10]
// 2. 追加多个元素
a = append(a, 11, 12, 13)
fmt.Println("追加多个元素后:", a) // 输出:追加多个元素后: [10 11 12 13]
// 3. 追加另一个切片(需用...展开)
b := []int{14, 15}
a = append(a, b...)
fmt.Println("追加切片后:", a) // 输出:追加切片后: [10 11 12 13 14 15]
}append 函数不修改原切片,而是返回新的切片(原切片的指针、长度、容量可能不变)。len(s) < cap(s)),append 会直接在底层数组的剩余空间添加元素,新切片与原切片共享底层数组。len(s) == cap(s)),append 会触发扩容,新切片指向新的底层数组,与原切片完全独立。切片的扩容规则由Go语言 runtime 源码(src/runtime/slice.go)定义,不同版本有细微差异,以下分别讲解。
package main
import "fmt"
func main() {
var a []int // nil切片(len=0, cap=0)
// 循环追加10个元素,观察容量变化
for i := 0; i < 10; i++ {
a = append(a, i)
fmt.Printf("a=%v, len=%d, cap=%d, ptr=%p\n", a, len(a), cap(a), a)
}
}输出结果:
a=[0], len=1, cap=1, ptr=0x140000a6008
a=[0 1], len=2, cap=2, ptr=0x140000a6010
a=[0 1 2], len=3, cap=4, ptr=0x140000a8020 // len=3 <1024,cap翻倍(2→4)
a=[0 1 2 3], len=4, cap=4, ptr=0x140000a8020
a=[0 1 2 3 4], len=5, cap=8, ptr=0x140000aa040 // len=5 <1024,cap翻倍(4→8)
a=[0 1 2 3 4 5], len=6, cap=8, ptr=0x140000aa040
a=[0 1 2 3 4 5 6], len=7, cap=8, ptr=0x140000aa040
a=[0 1 2 3 4 5 6 7], len=8, cap=8, ptr=0x140000aa040
a=[0 1 2 3 4 5 6 7 8], len=9, cap=16, ptr=0x140000ac080 // len=9 <1024,cap翻倍(8→16)
a=[0 1 2 3 4 5 6 7 8 9], len=10, cap=16, ptr=0x140000ac080Go 1.18 优化了大容量切片的扩容效率,调整后的规则:
规则解读:
make 函数指定足够的容量(如 make([]int, 0, 1000)),避免频繁扩容。 s := make([]int, 0, 1000),后续append无需扩容,性能提升显著。s = s[:0] 清空切片,复用底层数组)。copy 函数创建小切片(如 small := make([]T, len(large[:10])),copy(small, large[:10])),释放原底层数组的内存。除了基础操作,切片在实战中还有一些高频用法,以下结合代码示例讲解。
Go语言的 sort 包提供了对切片的排序功能,支持int、string、float64等基本类型,核心函数包括 sort.Ints()、sort.Strings()、sort.Float64s()。
package main
import (
"fmt"
"sort"
)
func main() {
// 1. 定义一个int数组(需转换为切片后排序)
var a = [...]int{3, 7, 8, 9, 1}
fmt.Println("排序前数组a:", a) // 输出:排序前数组a: [3 7 8 9 1]
// 2. 将数组转换为切片(a[:]),调用sort.Ints排序
sort.Ints(a[:])
fmt.Println("排序后数组a:", a) // 输出:排序后数组a: [1 3 7 8 9]
}nil切片虽然长度和容量为0,但可以直接用于 append 函数(无需初始化),这是Go语言的设计特性,需合理利用。
package main
import "fmt"
func main() {
var a []int // nil切片(未初始化)
// 直接append,无需初始化(安全)
a = append(a, 1, 2, 3)
fmt.Println(a) // 输出:[1 2 3]
// 错误用法:直接对nil切片的索引赋值(会触发panic)
// a[0] = 100 // runtime error: index out of range [0] with length 0
}append,但不能直接通过索引赋值(需先初始化或append元素后再赋值)。[]T{} 或 make([]T, 0));若结果为空且有错误,建议返回nil切片(明确表示"未赋值"状态)。Go语言支持嵌套切片(切片的元素类型是另一个切片),常用于表示二维数据(如矩阵、表格)。
package main
import "fmt"
func main() {
// 1. 定义一个嵌套切片(二维int切片)
var matrix [][]int
// 2. 向嵌套切片中添加行(每行是一个切片)
matrix = append(matrix, []int{1, 2, 3})
matrix = append(matrix, []int{4, 5, 6})
matrix = append(matrix, []int{7, 8, 9})
fmt.Println("二维切片matrix:", matrix) // 输出:二维切片matrix: [[1 2 3] [4 5 6] [7 8 9]]
// 3. 遍历嵌套切片
fmt.Println("\n遍历二维切片:")
for i, row := range matrix {
for j, val := range row {
fmt.Printf("matrix[%d][%d] = %d ", i, j, val)
}
fmt.Println()
}
}matrix = append(matrix, []int{10}),形成不规则二维数据)。matrix[0] = append(matrix[0], 10) 为第一行追加元素)。在使用切片的过程中,容易因对底层原理理解不深而出现问题,以下总结常见坑点及解决方案。
问题示例:
func main() {
s1 := []int{1, 2, 3}
s2 := s1 // s2与s1共享底层数组
s2[0] = 100
fmt.Println(s1) // 输出:[100 2 3](意外修改)
}解决方案:
copy 函数创建独立切片:s2 := make([]int, len(s1)); copy(s2, s1)。append 函数创建新切片:s2 := append([]int{}, s1...)。问题示例:
func main() {
s1 := make([]int, 2, 2) // len=2, cap=2
s2 := s1
s1 = append(s1, 3) // s1扩容(cap变为4),指向新数组
s1[0] = 100
fmt.Println(s2) // 输出:[0 0](s2仍指向原数组,未被修改)
}解决方案:
make([]int, 2, 3))。[]*int),确保修改的是同一元素。for range 遍历切片时修改元素无效问题示例:
func main() {
s := []int{1, 2, 3}
// 错误:range遍历的是元素的副本,修改副本不影响原切片
for _, v := range s {
v *= 2
}
fmt.Println(s) // 输出:[1 2 3](未修改)
}解决方案:
使用索引遍历,直接修改原切片元素:
for i := range s {
s[i] *= 2
}
fmt.Println(s) // 输出:[2 4 6](修改成功)切片作为Go语言的核心数据结构,它的设计兼顾了效率和灵活性。掌握切片的关键在于理解其底层数组+指针+长度+容量的结构,以及引用传递、扩容等特性。 最后如果哪些地方的不足,欢迎大家在评论区中指正!