Go语言数组与切片

在Go语言中,数组和切片是两种重要的线性数据结构。数组长度固定,值类型;切片则是动态数组,引用类型,是实际开发中最常用的序列类型。理解二者之间的关系和底层实现,是高效使用Go语言的关键。

一句话总结:数组是“值类型”且长度不可变;切片是对数组某个连续段的“引用”,可动态扩容,几乎可以替代数组。

1. 数组(Array)

数组是具有固定长度、存储相同类型元素的序列。长度是数组类型的一部分,例如 [3]int 和 [4]int 是不同的类型。

1.1 声明与初始化

多种数组初始化方式
package main

import "fmt"

func main() {
    // 方式1:声明后赋值(零值数组)
    var arr1 [3]int               // [0,0,0]
    arr1[0] = 10

    // 方式2:字面量初始化
    arr2 := [3]int{1, 2, 3}

    // 方式3:自动推断长度(...)
    arr3 := [...]int{4, 5, 6, 7}   // 长度由初始化元素个数决定

    // 方式4:指定索引初始化
    arr4 := [5]int{0: 10, 3: 40}   // [10,0,0,40,0]

    fmt.Println(arr1, arr2, arr3, arr4)
}

1.2 遍历数组

// 传统for
for i := 0; i < len(arr2); i++ {
    fmt.Println(arr2[i])
}

// range遍历
for index, value := range arr3 {
    fmt.Printf("索引%d: 值%d\n", index, value)
}

1.3 数组是值类型(重要)

数组在赋值或传参时会发生完整拷贝,对新数组的修改不影响原数组。

original := [3]int{1, 2, 3}
copyArr := original
copyArr[0] = 100
fmt.Println(original)  // [1 2 3],原数组不变
fmt.Println(copyArr)   // [100 2 3]

// 函数传参也是拷贝
func modify(arr [3]int) {
    arr[0] = 99
}
// 调用后 original 仍为 [1,2,3]
因为值拷贝,大数组作为函数参数会比较低效。通常使用切片代替。

1.4 多维数组

var matrix [2][3]int = [2][3]int{
    {1, 2, 3},
    {4, 5, 6},
}
fmt.Println(matrix[1][2]) // 6

2. 切片(Slice)

切片是对底层数组一个连续片段的引用,长度可变,是Go中最常用的序列类型。切片本身不存储数据,操作切片可能会影响底层数组。

2.1 创建切片

// 从数组切割
arr := [5]int{1,2,3,4,5}
s1 := arr[1:4]      // [2,3,4],包含索引1~3

// 字面量创建
s2 := []int{10,20,30}   // 长度=容量=3

// 使用 make 创建(指定长度和容量)
s3 := make([]int, 5, 10) // 长度5,容量10,元素零值
s4 := make([]int, 3)      // 长度=容量=3

2.2 切片的内部结构

切片包含三个字段:指向底层数组的指针、长度(len)和容量(cap)。长度是切片中元素个数,容量是从切片起始位置到底层数组末尾的元素个数。

numbers := []int{1,2,3,4,5,6}
s := numbers[1:4]       // 元素: [2,3,4]
fmt.Println(len(s))     // 3
fmt.Println(cap(s))     // 5 (从索引1到末尾共5个元素)

// 重新切片可以扩展切片(不超过容量)
s = s[:cap(s)]          // 扩展到长度为5: [2,3,4,5,6]

2.3 切片是引用类型

多个切片可以共享同一个底层数组,修改会互相影响。

a := []int{1,2,3}
b := a       // b 和 a 指向同一底层数组
b[0] = 99
fmt.Println(a)          // [99,2,3]

2.4 append 追加与扩容

使用 append 向切片添加元素,如果容量不足,会自动扩容(通常以2倍容量增长)。

var s []int               // nil 切片
s = append(s, 1)          // 追加1个元素
s = append(s, 2, 3, 4)    // 追加多个
s = append(s, []int{5,6}...) // 解包追加

fmt.Println(s) // [1 2 3 4 5 6]

// 查看扩容过程
var slice []int
for i := 0; i < 10; i++ {
    slice = append(slice, i)
    fmt.Printf("len=%d cap=%d\n", len(slice), cap(slice))
}
扩容策略:当原容量小于1024时,新容量成为原容量的2倍;大于等于1024时,增长因子约为1.25倍(可能因内存对齐微调)。

2.5 切片复制:copy

copy(dst, src) 将源切片元素复制到目标切片,返回复制的元素个数。

src := []int{1,2,3}
dst := make([]int, 2)
n := copy(dst, src)   // dst=[1,2], n=2

// 复制全部元素
dst2 := make([]int, len(src))
copy(dst2, src)

2.6 删除切片中的元素

利用append和切片操作实现删除。

// 删除索引 i 的元素
a := []int{0,1,2,3,4,5}
i := 2
a = append(a[:i], a[i+1:]...)   // 结果: [0,1,3,4,5]

// 删除首部元素
a = a[1:]

// 删除尾部元素
a = a[:len(a)-1]

2.7 nil 切片与空切片

nil 切片未分配底层数组,空切片已分配但长度为0。二者在大多数情况下(如range、len、append)表现一致。

var s1 []int          // nil 切片
s2 := []int{}         // 空切片(非nil)
fmt.Println(s1 == nil) // true
fmt.Println(s2 == nil) // false
// 但两者都可直接append

3. 数组 vs 切片 对比表

特性数组切片
长度固定,编译时确定动态,运行时可变
类型定义[n]T[]T
值/引用值类型(赋值拷贝)引用类型(共享底层)
内存分配栈上或静态区底层数组在堆上
作为参数整个数组拷贝(大数组低效)只拷贝切片头(指针/len/cap)高效
零值各个元素零值nil(未make时)

综合示例:切片操作实战

数组转切片、动态过滤、去重
package main

import "fmt"

func main() {
    // 数组转切片
    arr := [5]int{1,2,3,4,5}
    slice := arr[:]   // 得到包含所有元素的切片

    // 过滤函数:返回新切片
    filter := func(s []int, fn func(int) bool) []int {
        result := make([]int, 0, len(s))
        for _, v := range s {
            if fn(v) {
                result = append(result, v)
            }
        }
        return result
    }
    even := filter(slice, func(v int) bool { return v%2 == 0 })
    fmt.Println("偶数:", even)   // [2,4]

    // 去重示例
    nums := []int{1,2,2,3,4,4,5}
    unique := []int{}
    seen := make(map[int]bool)
    for _, v := range nums {
        if !seen[v] {
            seen[v] = true
            unique = append(unique, v)
        }
    }
    fmt.Println("去重后:", unique) // [1,2,3,4,5]
}
偶数: [2 4]
去重后: [1 2 3 4 5]
小结:在实际开发中,99%的场景使用切片而非数组。切片提供了动态容量、灵活操作,且通过append很容易实现增长。理解切片底层原理(长度、容量、扩容)对编写高性能Go程序至关重要。下一章我们将学习map(哈希表)的用法。