Go 语言完全指南 / 08 - 数组与切片:底层原理、扩容机制、copy、append
08 - 数组与切片
8.1 数组(Array)
数组是固定长度、相同类型元素的序列。
package main
import "fmt"
func main() {
// 声明并初始化
var a [5]int // 零值初始化: [0 0 0 0 0]
b := [5]int{1, 2, 3, 4, 5} // 字面量初始化
c := [...]int{10, 20, 30} // 编译器自动计算长度
d := [5]int{0: 10, 4: 50} // 指定索引初始化
fmt.Println(a, b, c, d)
// [0 0 0 0 0] [1 2 3 4 5] [10 20 30] [10 0 0 0 50]
// 数组操作
fmt.Println("长度:", len(b)) // 5
fmt.Println("元素:", b[0], b[4]) // 1, 50
// 遍历
for i, v := range b {
fmt.Printf("[%d]=%d ", i, v)
}
fmt.Println()
// 修改元素
b[0] = 100
fmt.Println(b) // [100 2 3 4 5]
}
数组是值类型
func main() {
a := [3]int{1, 2, 3}
b := a // 复制整个数组
b[0] = 100
fmt.Println(a) // [1 2 3](不受影响)
fmt.Println(b) // [100 2 3]
// 数组比较
c := [3]int{1, 2, 3}
fmt.Println(a == c) // true
}
二维数组
func main() {
// 3x3 矩阵
var matrix [3][3]int
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
matrix[i][j] = i*3 + j + 1
}
}
fmt.Println(matrix)
// [[1 2 3] [4 5 6] [7 8 9]]
}
8.2 切片(Slice)
切片是对底层数组的一个动态视图,包含三个要素:指针、长度、容量。
// 切片的内部结构
type slice struct {
array unsafe.Pointer // 指向底层数组
len int // 长度
cap int // 容量
}
func main() {
// 创建切片的多种方式
s1 := []int{1, 2, 3, 4, 5} // 字面量
s2 := make([]int, 5) // 长度 5,容量 5
s3 := make([]int, 3, 10) // 长度 3,容量 10
s4 := []int{} // 空切片(非 nil)
fmt.Println(s1, s2, s3, s4)
fmt.Printf("s3: len=%d, cap=%d\n", len(s3), cap(s3)) // len=3, cap=10
}
切片 vs 数组
| 特性 | 数组 | 切片 |
|---|---|---|
| 长度 | 固定 | 动态 |
| 类型 | 值类型 | 引用类型 |
| 比较 | 可用 == | 只能与 nil 比较 |
| 声明 | [5]int | []int |
| 传参 | 复制整个数组 | 传递引用(共享底层数组) |
8.3 切片操作
切片表达式
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// 基本切片 [low:high]
fmt.Println(s[2:5]) // [2 3 4]
fmt.Println(s[:5]) // [0 1 2 3 4]
fmt.Println(s[5:]) // [5 6 7 8 9]
fmt.Println(s[:]) // [0 1 2 3 4 5 6 7 8 9]
// 三索引切片 [low:high:max],控制容量
s2 := s[2:5:7]
fmt.Printf("s2: %v, len=%d, cap=%d\n", s2, len(s2), cap(s2))
// s2: [2 3 4], len=3, cap=5
// ⚠️ 注意:切片不创建新底层数组
s3 := s[0:3]
s3[0] = 999
fmt.Println(s) // [999 1 2 3 4 5 6 7 8 9](原数组也被修改)
}
append 函数
func main() {
// 基本 append
s := []int{1, 2, 3}
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, 10}
s = append(s, s2...) // 展开切片
fmt.Println(s) // [1 2 3 4 5 6 7 8 9 10]
// append 可能触发扩容(创建新底层数组)
s3 := make([]int, 3, 4)
fmt.Printf("s3: len=%d, cap=%d, ptr=%p\n", len(s3), cap(s3), s3)
s3 = append(s3, 1)
fmt.Printf("s3: len=%d, cap=%d, ptr=%p\n", len(s3), cap(s3), s3)
// 容量足够,不扩容
s3 = append(s3, 2)
fmt.Printf("s3: len=%d, cap=%d, ptr=%p\n", len(s3), cap(s3), s3)
// 容量不足,扩容,底层数组地址变化
}
copy 函数
func main() {
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
// 复制(取较小长度)
n := copy(dst, src)
fmt.Printf("复制了 %d 个元素: %v\n", n, dst) // [1 2 3]
// 复制到指定位置
dst2 := make([]int, 5)
copy(dst2[2:], src[:3])
fmt.Println(dst2) // [0 0 1 2 3]
// 同一切片内复制(可以重叠)
s := []int{1, 2, 3, 4, 5}
copy(s[1:], s[:3])
fmt.Println(s) // [1 1 2 3 5]
// 字符串转 []byte
str := "Hello, 世界"
bytes := []byte(str)
fmt.Println(bytes)
// 创建切片的独立副本
original := []int{1, 2, 3}
cloned := make([]int, len(original))
copy(cloned, original)
cloned[0] = 999
fmt.Println("original:", original) // [1 2 3]
fmt.Println("cloned:", cloned) // [999 2 3]
}
8.4 切片扩容机制
// Go 1.21+ 的扩容策略:
// 1. 如果新容量 > 2倍旧容量,直接使用新容量
// 2. 如果旧容量 < 256,新容量 = 2倍旧容量
// 3. 如果旧容量 >= 256,新容量 = 旧容量 * 1.25 + 192(近似)
func main() {
s := make([]int, 0)
prevCap := cap(s)
for i := 0; i < 1000; i++ {
s = append(s, i)
if cap(s) != prevCap {
fmt.Printf("len=%4d, cap=%4d, growth=%.2f\n",
len(s), cap(s), float64(cap(s))/float64(prevCap))
prevCap = cap(s)
}
}
}
⚠️ 注意:如果能预估切片大小,使用 make([]T, 0, estimatedSize) 预分配,减少扩容次数。
陷阱:切片共享底层数组
func main() {
// 陷阱:子切片 append 修改了原数组
original := []int{1, 2, 3, 4, 5}
sub := original[1:3] // [2 3], len=2, cap=4
sub = append(sub, 999) // 写入 original[3] 的位置!
fmt.Println(original) // [1 2 3 999 5]
fmt.Println(sub) // [2 3 999]
// 解决方案:使用三索引限制容量
original2 := []int{1, 2, 3, 4, 5}
sub2 := original2[1:3:3] // len=2, cap=2
sub2 = append(sub2, 999) // 触发扩容,不影响原数组
fmt.Println(original2) // [1 2 3 4 5]
fmt.Println(sub2) // [2 3 999]
// 解决方案:创建独立副本
original3 := []int{1, 2, 3, 4, 5}
sub3 := make([]int, 2)
copy(sub3, original3[1:3])
sub3 = append(sub3, 999)
fmt.Println(original3) // [1 2 3 4 5]
}
8.5 切片删除操作
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// 删除索引 i 处的元素(保持顺序)
i := 3
s = append(s[:i], s[i+1:]...)
fmt.Println(s) // [0 1 2 4 5 6 7 8 9]
// 删除索引 i 处的元素(不保持顺序,更快)
s2 := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
i = 3
s2[i] = s2[len(s2)-1]
s2 = s2[:len(s2)-1]
fmt.Println(s2) // [0 1 2 9 4 5 6 7 8]
// 删除子切片 [i:j]
s3 := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
i, j := 3, 6
s3 = append(s3[:i], s3[j:]...)
fmt.Println(s3) // [0 1 2 6 7 8 9]
// 清空切片(保留容量)
s4 := []int{1, 2, 3, 4, 5}
s4 = s4[:0]
fmt.Println(s4, len(s4), cap(s4)) // [] 0 5
// 释放内存(GC 可以回收底层数组)
s5 := make([]int, 1000)
s5 = nil
// 或者 s5 = make([]int, 0)
}
8.6 切片插入操作
func main() {
// 在索引 i 处插入元素
s := []int{1, 2, 3, 7, 8}
i := 3
val := 99
// 方法一:使用 append
s = append(s[:i], append([]int{val}, s[i:]...)...)
fmt.Println(s) // [1 2 3 99 7 8]
// 方法二:更高效(减少内存分配)
s2 := []int{1, 2, 3, 7, 8}
s2 = append(s2, 0) // 扩展一个位置
copy(s2[i+1:], s2[i:]) // 后移元素
s2[i] = val // 插入值
fmt.Println(s2) // [1 2 3 99 7 8]
// 插入多个元素
s3 := []int{1, 2, 7, 8}
insert := []int{3, 4, 5, 6}
s3 = append(s3[:2], append(insert, s3[2:]...)...)
fmt.Println(s3) // [1 2 3 4 5 6 7 8]
}
8.7 性能优化
import "testing"
// ❌ 差:多次 append,多次扩容
func bad() []int {
var s []int
for i := 0; i < 10000; i++ {
s = append(s, i)
}
return s
}
// ✅ 好:预分配容量
func good() []int {
s := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
s = append(s, i)
}
return s
}
// ❌ 差:不必要的 slice 创建
func filterBad(s []int) []int {
var result []int
for _, v := range s {
if v > 0 {
result = append(result, v)
}
}
return result
}
// ✅ 好:原地过滤
func filterGood(s []int) []int {
n := 0
for _, v := range s {
if v > 0 {
s[n] = v
n++
}
}
return s[:n]
}
8.8 综合示例
package main
import (
"fmt"
"sort"
)
func main() {
// 去重
nums := []int{1, 3, 2, 3, 1, 4, 2, 5}
seen := make(map[int]bool)
unique := nums[:0]
for _, v := range nums {
if !seen[v] {
seen[v] = true
unique = append(unique, v)
}
}
fmt.Println("去重:", unique) // [1 3 2 4 5]
// 合并两个有序切片
a := []int{1, 3, 5, 7}
b := []int{2, 4, 6, 8}
merged := make([]int, 0, len(a)+len(b))
i, j := 0, 0
for i < len(a) && j < len(b) {
if a[i] <= b[j] {
merged = append(merged, a[i])
i++
} else {
merged = append(merged, b[j])
j++
}
}
merged = append(merged, a[i:]...)
merged = append(merged, b[j:]...)
fmt.Println("合并:", merged)
// 分块
chunk := func(s []int, size int) [][]int {
var chunks [][]int
for size < len(s) {
s, chunks = s[size:], append(chunks, s[:size:size])
}
return append(chunks, s)
}
fmt.Println("分块:", chunk([]int{1, 2, 3, 4, 5, 6, 7, 8, 9}, 3))
// [[1 2 3] [4 5 6] [7 8 9]]
// 洗牌(Fisher-Yates)
s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
sort.Slice(s, func(i, j int) bool {
return false // 使用真正的随机数代替
})
}
🏢 业务场景
- 批量数据处理:切片是存储数据集合的基础
- 缓冲区:预分配切片做 I/O 缓冲区
- 滑动窗口:切片表达式实现固定大小窗口
- 栈/队列:
append和s[:len(s)-1]模拟栈 - 分页:切片表达式实现数据分页