Go 语言切片
目录
切片(slice)是 Go 语言中使用最频繁的复合类型,它是对数组的动态封装,提供了动态长度、灵活的截取和高效的追加能力。日常开发中,几乎所有集合操作都通过切片完成,可以说切片是 Go 编程的核心数据结构。
切片的定义与创建
使用字面量创建
package main
import "fmt"
func main() {
// 与数组类似,但不指定长度
s := []int{1, 2, 3, 4, 5}
fmt.Println(s) // [1 2 3 4 5]
fmt.Printf("类型: %T\n", s) // []int
}使用 make 创建
make([]T, length, capacity) 用于创建指定长度和容量的切片。
package main
import "fmt"
func main() {
// 创建长度为 5,容量为 5 的切片,元素为零值
s1 := make([]int, 5)
fmt.Println(s1) // [0 0 0 0 0]
fmt.Printf("len=%d, cap=%d\n", len(s1), cap(s1)) // len=5, cap=5
// 创建长度为 3,容量为 10 的切片
s2 := make([]int, 3, 10)
fmt.Printf("len=%d, cap=%d\n", len(s2), cap(s2)) // len=3, cap=10
}从数组派生
package main
import "fmt"
func main() {
arr := [6]int{10, 20, 30, 40, 50, 60}
// 从数组索引 1 到 4(不含 4)创建切片
s := arr[1:4]
fmt.Println(s) // [20 30 40]
}切片 vs 数组的区别
| 特性 | 数组 | 切片 |
|---|---|---|
| 长度 | 固定,是类型的一部分 | 动态,可变 |
| 类型 | [N]T |
[]T |
| 赋值 | 值拷贝 | 引用底层数组 |
| 比较 | 可比较(元素可比较时) | 不可用 == 比较 |
| 函数传参 | 拷贝整个数组 | 只拷贝切片头 |
package main
import "fmt"
func main() {
// 数组:值类型
a := [3]int{1, 2, 3}
b := a
b[0] = 100
fmt.Println(a) // [1 2 3] —— 不受影响
// 切片:引用底层数组
s1 := []int{1, 2, 3}
s2 := s1
s2[0] = 100
fmt.Println(s1) // [100 2 3] —— 共享底层数组
}len() 和 cap() 函数
len(s)返回切片的长度(当前元素个数)cap(s)返回切片的容量(底层数组从切片起始位置到末尾的元素个数)
package main
import "fmt"
func main() {
s := make([]int, 3, 5)
fmt.Printf("len=%d, cap=%d\n", len(s), cap(s)) // len=3, cap=5
s = append(s, 1, 2)
fmt.Printf("len=%d, cap=%d\n", len(s), cap(s)) // len=5, cap=5
// 超出容量会触发扩容
s = append(s, 3)
fmt.Printf("len=%d, cap=%d\n", len(s), cap(s)) // len=6, cap=10
}append() 函数
append() 用于向切片末尾追加元素,返回新的切片。
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
fmt.Printf("原始: len=%d, cap=%d\n", len(s), cap(s))
// 追加单个元素
s = append(s, 4)
fmt.Println(s) // [1 2 3 4]
// 追加多个元素
s = append(s, 5, 6, 7)
fmt.Println(s) // [1 2 3 4 5 6 7]
// 追加另一个切片(使用 ... 展开)
s2 := []int{8, 9}
s = append(s, s2...)
fmt.Println(s) // [1 2 3 4 5 6 7 8 9]
}扩容机制
当切片容量不足时,append() 会自动扩容。Go 的扩容策略:
- 容量 < 256 时:新容量 = 旧容量的 2 倍
- 容量 >= 256 时:新容量 = 旧容量 × 1.25 + 192(渐进式增长)
package main
import "fmt"
func main() {
s := make([]int, 0)
for i := 0; i < 10; i++ {
s = append(s, i)
fmt.Printf("追加 %d 后: len=%d, cap=%d\n", i, len(s), cap(s))
}
}扩容时会分配新的底层数组,并将旧数据拷贝过去。因此,扩容前后的切片指向不同的底层数组。
切片截取
切片截取语法为 s[low:high],含义如下:
low:起始索引(包含)high:结束索引(不包含)- 结果长度 =
high - low - 结果容量 =
cap(s) - low
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Printf("原始: len=%d, cap=%d\n", len(s), cap(s))
// 基本截取
sub := s[2:5]
fmt.Println(sub) // [2 3 4]
fmt.Printf("sub: len=%d, cap=%d\n", len(sub), cap(sub)) // len=3, cap=8
// 省略 low,从开头截取
fmt.Println(s[:3]) // [0 1 2]
// 省略 high,截取到末尾
fmt.Println(s[7:]) // [7 8 9]
// 全部省略,复制切片描述符(仍共享底层数组)
fmt.Println(s[:]) // [0 1 2 3 4 5 6 7 8 9]
}截取后的切片与原切片共享底层数组,修改其中一个会影响另一个。
copy() 函数
copy(dst, src) 将 src 中的元素复制到 dst,返回实际复制的元素个数。
package main
import "fmt"
func main() {
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 5)
n := copy(dst, src)
fmt.Println(n) // 5
fmt.Println(dst) // [1 2 3 4 5]
// 修改 dst 不影响 src
dst[0] = 100
fmt.Println(src) // [1 2 3 4 5] —— 不受影响
}copy() 常用于创建独立的切片副本,避免共享底层数组带来的副作用:
package main
import "fmt"
func main() {
s1 := []int{1, 2, 3}
// 错误方式:共享底层数组
s2 := s1
s2[0] = 100
fmt.Println(s1) // [100 2 3]
// 正确方式:使用 copy 创建独立副本
s3 := make([]int, len(s1))
copy(s3, s1)
s3[0] = 200
fmt.Println(s1) // [100 2 3] —— 不受影响
fmt.Println(s3) // [200 2 3]
}切片的底层结构
切片的底层是一个结构体,包含三个字段:
// runtime 中的定义(简化版)
type slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 切片长度
cap int // 切片容量
}- array:指向底层数组的指针
- len:当前切片中元素的个数
- cap:从切片起始位置到底层数组末尾的容量
package main
import (
"fmt"
"unsafe"
)
func main() {
s := []int{10, 20, 30}
// 通过 unsafe 查看切片内部结构
ptr := *(*[3]uintptr)(unsafe.Pointer(&s))
fmt.Printf("array 指针: %x\n", ptr[0])
fmt.Printf("len: %d\n", ptr[1])
fmt.Printf("cap: %d\n", ptr[2])
}切片共享底层数组的陷阱
由于切片共享底层数组,不当操作可能导致难以排查的 bug。
package main
import "fmt"
func main() {
s1 := []int{1, 2, 3, 4, 5}
s2 := s1[1:3] // [2, 3],与 s1 共享底层数组
fmt.Printf("s1: len=%d, cap=%d\n", len(s1), cap(s1))
fmt.Printf("s2: len=%d, cap=%d\n", len(s2), cap(s2))
// 修改 s2 的元素会影响 s1
s2[0] = 200
fmt.Println(s1) // [1 200 3 4 5]
// 对 s2 追加,如果未超出容量,也会覆盖 s1 的数据
s2 = append(s2, 99)
fmt.Println(s1) // [1 200 3 99 5] —— s1[3] 被覆盖!
}避免陷阱的原则:
- 截取切片后,谨慎对子切片使用
append() - 需要独立数据时,使用
copy()创建副本 - 使用
append()时始终接收返回值
性能建议:预分配容量
如果预先知道切片需要的容量,使用 make() 预分配可以避免多次扩容带来的内存分配和数据拷贝。
package main
import "fmt"
func main() {
// 不推荐:动态扩容,可能触发多次内存分配
var s1 []int
for i := 0; i < 1000; i++ {
s1 = append(s1, i)
}
// 推荐:预分配容量,减少扩容次数
s2 := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
s2 = append(s2, i)
}
fmt.Printf("s1: len=%d, cap=%d\n", len(s1), cap(s1))
fmt.Printf("s2: len=%d, cap=%d\n", len(s2), cap(s2))
}在性能敏感的场景下,预分配容量可以显著减少内存分配次数,提升程序性能。
总结
本篇详细讲解了 Go 语言切片的核心知识:切片的创建方式(字面量、make、从数组派生)、切片与数组的区别、len() 和 cap() 的含义、append() 的追加与扩容机制、切片截取语法、copy() 函数、底层结构(pointer、len、cap)以及共享底层数组的陷阱。切片是 Go 中最灵活、最常用的数据结构,掌握其底层原理和最佳实践对编写高效、安全的 Go 程序至关重要。