Contents

Go语言数组与切片详解

在 Go 语言中,数组(Array)和切片(Slice)是最常用的顺序容器。它们看似相似,却有本质上的区别:数组是值类型、长度固定;切片是引用类型、长度可变。本文将从定义、内存模型、函数传参/返回、使用场景等方面进行详细总结。


一、数组(Array)

1. 定义与声明

数组是 定长的元素集合,长度是类型的一部分:

var arr1 [3]int               // 默认初始化为 [0,0,0]
arr2 := [3]int{1, 2, 3}       // 显式初始化
arr3 := [...]int{1, 2, 3, 4}  // 由编译器推导长度

2. 特点

  • 长度固定,一旦声明,长度不能改变。
  • 值类型,赋值和传参时会复制整个数组。
  • [3]int[4]int 是完全不同的类型。

3. 使用示例

func modifyArray(a [3]int) {
    a[0] = 100 // 修改的是副本
}

func main() {
    arr := [3]int{1, 2, 3}
    modifyArray(arr)
    fmt.Println(arr) // [1 2 3] 未改变
}

4. 内存模型

  • 数组的所有元素连续存储在栈或堆上。
  • 传参/赋值时,会整体拷贝。

二、切片(Slice)

1. 定义与声明

切片是对数组的 动态视图,本质上是一个 slice header,包含三个字段:

  • ptr:指向底层数组的指针
  • len:当前切片的长度
  • cap:切片的容量(从 ptr 到底层数组末尾的元素个数)
var s1 []int                 // nil 切片,len=0, cap=0
s2 := []int{1, 2, 3}         // 字面量声明
s3 := make([]int, 5, 10)     // len=5, cap=10
s4 := s2[1:3]                // 基于数组/切片的切片

2. 特点

  • 引用类型,传参和赋值只会拷贝 slice header,不会拷贝底层数组。
  • 长度和容量可变,可以 append
  • 多个切片可能共享同一底层数组。

3. 使用示例

func modifySlice(s []int) {
    s[0] = 100 // 修改底层数组,外部可见
}

func main() {
    s := []int{1, 2, 3}
    modifySlice(s)
    fmt.Println(s) // [100 2 3] 已改变
}

4. 内存模型

slice header {
    ptr -> 底层数组
    len
    cap
}

多个切片可能共享同一个底层数组。


三、数组与切片在函数传参/返回的区别

1. 数组

  • 值类型,传参和返回时会完整拷贝。
  • 适合小规模、固定大小的数据。
func returnArray() [3]int {
    return [3]int{1, 2, 3} // 拷贝整个数组返回
}

2. 切片

  • 引用类型,传参和返回只会拷贝 header。
  • 修改切片元素会影响外部。
  • append 时可能触发 扩容,新分配底层数组,导致与原切片分离。
func returnSlice() []int {
    return []int{1, 2, 3} // 返回引用,轻量
}

四、数组 vs 切片对比表

特性数组 [N]T切片 []T
类型值类型引用类型(header 包含 ptr,len,cap)
长度固定,编译期确定可变,运行期调整
内存开销存储整个数组仅存储 header(24 字节,64 位机上)
传参/返回拷贝整个数组拷贝 header,轻量
修改是否影响外部是(共享底层数组)
常用场景固定大小、矩阵、缓存块动态序列、切片操作、集合处理

五、使用场景与最佳实践

  • 数组

    • 适合存放固定大小的数据,如 [16]byte、矩阵、环形缓冲区。
    • 避免频繁拷贝大数组,可以用指针 *[N]T
  • 切片

    • 适合动态集合和大部分日常开发场景。
    • 避免切片越界,注意扩容时可能导致与原切片“断开”。
    • 推荐通过 make 提前设置合理的容量,减少扩容开销。

六、总结

  • 数组:定长、值类型、拷贝成本高,更像 C 语言的数组。
  • 切片:变长、引用类型、使用灵活,是 Go 中实际开发的首选。
  • 牢记一句话:在 Go 中,数组偏底层,切片才是主角。