📖 数组与切片¶
学习时间: 约 3-4 小时 | 难度: ⭐⭐ 初级 | 前置知识: Go基础语法、变量与数据类型
📚 章节概述¶
数组和切片是 Go 语言中最基础也最重要的数据结构。数组是定长的值类型,切片是对数组的动态抽象。理解二者的底层实现原理和使用差异,是写出高性能 Go 代码的关键。本章将从底层内存模型出发,深入讲解数组与切片的核心机制。
上图展示了切片的 ptr/len/cap 三元组与底层数组之间的对应关系,便于理解切片扩容与共享底层数组行为。
🎯 学习目标¶
- 掌握数组的定义、初始化和值类型特性
- 理解切片的底层三元组(指针、长度、容量)
- 精通 make、append、copy 的使用与性能特征
- 了解切片扩容策略与内存分配
- 掌握多维切片与常见陷阱
📌 概念:数组(Array)¶
1.1 数组的定义与初始化¶
数组在 Go 中是值类型,长度是类型的一部分。[3]int 和 [5]int 是不同的类型。
package main
import "fmt"
func main() {
// 方式1:声明后赋值
var arr1 [5]int
arr1[0] = 10
arr1[4] = 50
fmt.Println("arr1:", arr1) // [10 0 0 0 50]
// 方式2:短变量声明 + 字面量初始化
arr2 := [5]int{1, 2, 3, 4, 5}
fmt.Println("arr2:", arr2) // [1 2 3 4 5]
// 方式3:省略号自动推断长度
arr3 := [...]int{10, 20, 30}
fmt.Println("arr3:", arr3) // [10 20 30]
fmt.Println("arr3 长度:", len(arr3)) // 3
// 方式4:指定索引初始化
arr4 := [5]int{1: 100, 3: 300}
fmt.Println("arr4:", arr4) // [0 100 0 300 0]
}
1.2 数组是值类型¶
这是 Go 数组与其他语言最大的区别之一。赋值和传参会完整复制整个数组。
func modifyArray(arr [3]int) {
arr[0] = 999
fmt.Println("函数内:", arr) // [999 2 3]
}
func main() {
original := [3]int{1, 2, 3}
modifyArray(original)
fmt.Println("函数外:", original) // [1 2 3] — 未被修改
}
⚠️ 注意: 大数组作为参数传递时会有性能开销,建议使用切片或指针。
1.3 数组的遍历¶
arr := [5]int{10, 20, 30, 40, 50}
// 方式1:经典 for 循环
for i := 0; i < len(arr); i++ {
fmt.Printf("arr[%d] = %d\n", i, arr[i])
}
// 方式2:range 遍历(推荐)
for index, value := range arr {
fmt.Printf("索引: %d, 值: %d\n", index, value)
}
// 方式3:只要值,忽略索引
for _, value := range arr {
fmt.Println(value)
}
// 方式4:只要索引
for index := range arr {
fmt.Println(index)
}
1.4 多维数组¶
// 二维数组:3行4列
var matrix [3][4]int
matrix[0][0] = 1
matrix[2][3] = 12
// 字面量初始化
grid := [2][3]int{
{1, 2, 3},
{4, 5, 6},
}
// 遍历二维数组
for i, row := range grid {
for j, val := range row {
fmt.Printf("grid[%d][%d] = %d ", i, j, val)
}
fmt.Println()
}
1.5 数组的比较¶
Go 中同类型数组可以直接用 == 和 != 比较:
a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
c := [3]int{1, 2, 4}
fmt.Println(a == b) // true
fmt.Println(a == c) // false
// 注意: [3]int 和 [4]int 不能比较,类型不同
📌 概念:切片(Slice)¶
2.1 切片的底层结构¶
切片在运行时由三个字段组成(reflect.SliceHeader):
package main
import "fmt"
func main() {
// 从数组创建切片
arr := [5]int{10, 20, 30, 40, 50}
slice := arr[1:4] // 包含索引1,2,3
fmt.Println("切片:", slice) // [20 30 40]
fmt.Println("长度:", len(slice)) // 3
fmt.Println("容量:", cap(slice)) // 4(从索引1到数组末尾)
}
2.2 切片的创建方式¶
// 方式1:字面量
s1 := []int{1, 2, 3, 4, 5}
// 方式2:make 创建
s2 := make([]int, 5) // 长度5,容量5,零值填充
s3 := make([]int, 3, 10) // 长度3,容量10
// 方式3:从数组切割
arr := [5]int{10, 20, 30, 40, 50}
s4 := arr[1:3] // [20, 30]
s5 := arr[:3] // [10, 20, 30]
s6 := arr[2:] // [30, 40, 50]
s7 := arr[:] // [10, 20, 30, 40, 50]
// 方式4:从切片再切片
s8 := s1[1:3] // [2, 3]
// 方式5:nil 切片 vs 空切片
var s9 []int // nil 切片,len=0, cap=0
s10 := []int{} // 空切片,len=0, cap=0
s11 := make([]int, 0) // 空切片,len=0, cap=0
fmt.Println(s9 == nil) // true
fmt.Println(s10 == nil) // false
2.3 append 操作详解¶
func main() {
s := []int{1, 2, 3}
fmt.Printf("追加前: len=%d, cap=%d, %v\n", len(s), cap(s), s)
// 追加单个元素
s = append(s, 4) // append 向切片追加元素,返回新切片
fmt.Printf("追加1个: len=%d, cap=%d, %v\n", len(s), cap(s), s)
// 追加多个元素
s = append(s, 5, 6, 7)
fmt.Printf("追加3个: len=%d, cap=%d, %v\n", len(s), cap(s), s)
// 追加另一个切片(展开操作符)
extra := []int{8, 9, 10}
s = append(s, extra...)
fmt.Printf("追加切片: len=%d, cap=%d, %v\n", len(s), cap(s), s)
}
2.4 切片扩容策略¶
Go 1.18+ 的扩容策略: - 当前容量 < 256 时,新容量 = 旧容量 × 2 - 当前容量 ≥ 256 时,新容量 = 旧容量 + (旧容量 + 3×256) / 4
func main() {
var s []int
for i := 0; i < 20; i++ {
s = append(s, i)
fmt.Printf("len=%2d, cap=%2d, ptr=%p\n", len(s), cap(s), s)
}
}
// 输出示意:可以观察到容量变化 1->2->4->8->16->...
// 注意每次扩容时 ptr 会变化(底层数组被重新分配)
2.5 copy 函数¶
// copy 返回实际复制的元素数量,等于 min(len(dst), len(src))
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src)
fmt.Println(n, dst) // 3 [1 2 3]
// 完整复制
fullCopy := make([]int, len(src))
copy(fullCopy, src)
fmt.Println(fullCopy) // [1 2 3 4 5]
// 切片之间可以有重叠
s := []int{1, 2, 3, 4, 5}
copy(s[1:], s[2:]) // 向前移动
fmt.Println(s) // [1 3 4 5 5]
2.6 切片的删除操作¶
Go 没有内置的 delete 操作,需手动实现:
// 删除索引 i 处的元素(保持顺序)
func removeOrdered(s []int, i int) []int {
return append(s[:i], s[i+1:]...)
}
// 删除索引 i 处的元素(不保持顺序,更高效)
func removeUnordered(s []int, i int) []int {
s[i] = s[len(s)-1]
return s[:len(s)-1]
}
func main() {
s := []int{10, 20, 30, 40, 50}
s = removeOrdered(s, 2)
fmt.Println(s) // [10 20 40 50]
s2 := []int{10, 20, 30, 40, 50}
s2 = removeUnordered(s2, 2)
fmt.Println(s2) // [10 20 50 40]
}
2.7 切片作为函数参数¶
切片是引用类型,传参不会复制底层数组,但 append 可能导致底层数组更换:
func modifySlice(s []int) {
s[0] = 999 // 修改会影响外部
}
func appendSlice(s []int) []int {
s = append(s, 100) // 可能不影响外部(底层数组可能被替换)
return s
}
func main() {
s := []int{1, 2, 3}
modifySlice(s)
fmt.Println(s) // [999 2 3]
s2 := []int{1, 2, 3}
_ = appendSlice(s2)
fmt.Println(s2) // [1 2 3] — append 可能未影响原切片
}
📌 概念:切片的常见陷阱¶
3.1 切片共享底层数组¶
func main() {
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[1:3] // [2, 3]
s2 := arr[2:5] // [3, 4, 5]
s1[1] = 999 // 修改 s1[1] 就是修改 arr[2]
fmt.Println(arr) // [1 2 999 4 5]
fmt.Println(s2) // [999 4 5] — s2 也被影响了!
}
3.2 用三索引切片限制容量¶
// 用 s[low:high:max] 限制切片容量,防止 append 覆盖原数组
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:3:3] // len=2, cap=2(而不是4)
fmt.Println(len(s), cap(s)) // 2 2
s = append(s, 100) // 容量不足,会分配新数组,不影响 arr
fmt.Println(arr) // [1 2 3 4 5] — 未被修改
3.3 内存泄漏:大切片的小引用¶
// 不好:data 的底层大数组无法被 GC 回收
func getHeader(data []byte) []byte {
return data[:10]
}
// 好:复制需要的部分,释放大数组的引用
func getHeaderSafe(data []byte) []byte {
header := make([]byte, 10)
copy(header, data[:10])
return header
}
💻 代码示例:实战应用¶
示例1:实现一个简单的栈¶
type IntStack struct {
data []int
}
func NewIntStack() *IntStack {
return &IntStack{data: make([]int, 0, 16)}
}
func (s *IntStack) Push(v int) {
s.data = append(s.data, v)
}
func (s *IntStack) Pop() (int, bool) {
if len(s.data) == 0 {
return 0, false
}
n := len(s.data) - 1
val := s.data[n]
s.data = s.data[:n]
return val, true
}
func (s *IntStack) Len() int {
return len(s.data)
}
func main() {
stack := NewIntStack()
stack.Push(10)
stack.Push(20)
stack.Push(30)
for stack.Len() > 0 {
val, _ := stack.Pop()
fmt.Println(val) // 30, 20, 10
}
}
示例2:切片去重¶
func unique(s []int) []int {
seen := make(map[int]bool)
result := make([]int, 0, len(s))
for _, v := range s {
if !seen[v] {
seen[v] = true
result = append(result, v)
}
}
return result
}
func main() {
nums := []int{3, 1, 4, 1, 5, 9, 2, 6, 5, 3}
fmt.Println(unique(nums)) // [3 1 4 5 9 2 6]
}
示例3:矩阵转置¶
func transpose(matrix [][]int) [][]int {
if len(matrix) == 0 {
return nil
}
rows, cols := len(matrix), len(matrix[0])
result := make([][]int, cols)
for i := range result {
result[i] = make([]int, rows)
}
for i := 0; i < rows; i++ {
for j := 0; j < cols; j++ {
result[j][i] = matrix[i][j]
}
}
return result
}
func main() {
m := [][]int{
{1, 2, 3},
{4, 5, 6},
}
t := transpose(m)
for _, row := range t {
fmt.Println(row)
}
// [1 4]
// [2 5]
// [3 6]
}
示例4:滑动窗口最大值¶
func maxSlidingWindow(nums []int, k int) []int {
if len(nums) == 0 || k == 0 {
return nil
}
result := make([]int, 0, len(nums)-k+1)
deque := make([]int, 0) // 存储索引
for i := 0; i < len(nums); i++ {
// 移除超出窗口的元素
for len(deque) > 0 && deque[0] < i-k+1 {
deque = deque[1:]
}
// 保持递减队列
for len(deque) > 0 && nums[deque[len(deque)-1]] < nums[i] {
deque = deque[:len(deque)-1]
}
deque = append(deque, i)
if i >= k-1 {
result = append(result, nums[deque[0]])
}
}
return result
}
✅ 最佳实践¶
1. 优先使用切片而非数组¶
2. 预分配容量避免频繁扩容¶
// ✅ 已知大小时预分配
result := make([]string, 0, len(input))
for _, item := range input {
result = append(result, transform(item))
}
// ❌ 零容量逐步追加
var result []string
for _, item := range input {
result = append(result, transform(item))
}
3. 使用 copy 而非 append 进行安全复制¶
// ✅ 安全复制,与原切片完全独立
clone := make([]int, len(original))
copy(clone, original)
// ⚠️ 这种方式也行,但语义不如 copy 直观
clone := append([]int(nil), original...)
4. 注意切片作为 map 值时不能取地址¶
// ❌ 不能直接修改 map 中切片的元素
m := map[string][]int{"a": {1, 2, 3}}
// m["a"][0] = 10 // 这是允许的,因为切片本身是引用
// 但要追加需要重新赋值
m["a"] = append(m["a"], 4)
5. 函数返回切片时注意文档说明¶
// ✅ 明确说明返回的切片是否可以安全修改
// SortedCopy 返回 s 的有序副本,调用方可以安全修改返回值
func SortedCopy(s []int) []int {
result := make([]int, len(s))
copy(result, s)
sort.Ints(result)
return result
}
🎯 面试题¶
Q1: 数组和切片的区别是什么?¶
A: 核心区别: 1. 长度:数组长度固定且是类型的一部分([3]int ≠ [5]int),切片长度动态可变 2. 值类型 vs 引用类型:数组赋值和传参是完整复制,切片传参只复制三元组(指针、长度、容量) 3. 比较:数组可用 == 比较,切片不能(只能与 nil 比较) 4. 底层:切片是对数组的引用抽象,包含指向底层数组的指针
Q2: 切片的 append 操作在什么时候会分配新的底层数组?¶
A: 当 append 后的元素总数超过当前容量(cap)时,Go 会分配一个更大的底层数组,并将已有元素复制过去。扩容策略(Go 1.18+):容量 < 256 时翻倍,≥ 256 时按 oldcap + (oldcap + 3*256) / 4 增长。扩容后,新切片和旧切片不再共享底层数组。
Q3: 以下代码输出什么?为什么?¶
s := make([]int, 0, 5)
s = append(s, 1, 2, 3)
a := append(s, 4)
b := append(s, 5)
fmt.Println(a, b)
A: 输出 [1 2 3 5] [1 2 3 5]。因为 s 的 cap=5,s 长度为3,a 和 b 的 append 都在同一底层数组的第4个位置写入。b 的写入覆盖了 a 的写入。这是切片共享底层数组的经典陷阱。
Q4: 如何安全地从切片中删除元素?¶
A: 两种方式: 1. 保持顺序:s = append(s[:i], s[i+1:]...) — O(n) 需要移动元素 2. 不保持顺序:s[i] = s[len(s)-1]; s = s[:len(s)-1] — O(1) 将末尾元素移到删除位置
Q5: nil 切片和空切片有什么区别?¶
A: var s []int 是 nil 切片(底层指针为 nil),s := []int{} 是空切片(底层指针非 nil 但长度为0)。它们的 len 和 cap 都是0,可以安全地用 append、range 等操作。区别在于 JSON 序列化时,nil 切片序列化为 null,空切片序列化为 []。在大多数场景下二者行为一致。
Q6: 以下代码有什么问题?如何修复?¶
A: 返回的切片与 data 共享底层数组。如果 data 是一个很大的切片(如读取整个文件),返回的小切片会阻止整个大数组被 GC 回收,导致内存泄漏。修复方式:用 copy 创建独立副本:
func getFirstN(data []byte, n int) []byte {
result := make([]byte, n)
copy(result, data[:n])
return result
}
📋 学习检查清单¶
- 能区分数组和切片的类型和行为差异
- 理解切片的底层三元组结构(指针、长度、容量)
- 掌握 make、append、copy 的使用及返回值
- 理解切片扩容策略对性能的影响
- 能识别切片共享底层数组的陷阱
- 会使用三索引切片
s[low:high:max]限制容量 - 了解 nil 切片与空切片的区别(含 JSON 序列化场景)
- 能实现常见的切片操作(去重、删除、栈、滑动窗口)