一架梯子,一头程序猿,仰望星空!
Golang程序设计教程(2024版) > 内容正文

学习 Golang 数组和切片


Go 语言中的数组

1.1 数组的定义及声明

数组是一个固定大小的元素序列,这些元素具有相同的类型。在Go语言中,数组的长度被视为数组类型的一部分。这意味着具有不同长度的数组被看作不同类型。

声明一个数组的基本语法如下:

var arr [n]T

其中,var 是声明变量的关键字,arr 是数组名,n 表示数组的长度,T 表示数组中元素的类型。

例如,要声明一个包含5个整数的数组:

var myArray [5]int

在这个例子中,myArray 是一个可以包含5个int类型整数的数组。

1.2 数组的初始化和使用

初始化数组可以在声明时直接进行,也可以使用索引进行赋值。数组初始化有多种方法:

直接初始化

var myArray = [5]int{10, 20, 30, 40, 50}

还可以允许编译器根据初始化值的个数自行推断数组的长度:

var myArray = [...]int{10, 20, 30, 40, 50}

这里的 ... 表示数组的长度由编译器计算得出。

通过索引初始化

var myArray [5]int
myArray[0] = 10
myArray[1] = 20
// 其余的元素为0,这是因为int的零值是0

数组的使用也非常简单,可以通过下标访问数组中的元素:

fmt.Println(myArray[2]) // 访问第三个元素

1.3 数组的遍历

遍历数组常用的有两种方法:使用传统的for循环和使用range

使用for循环遍历

for i := 0; i < len(myArray); i++ {
    fmt.Println(myArray[i])
}

使用range遍历

for index, value := range myArray {
    fmt.Printf("Index: %d, Value: %d\n", index, value)
}

使用range的好处是它会返回两个值:当前位置的索引和该位置的值。

1.4 数组的特性和局限性

Go语言中的数组是值类型,这意味着当数组作为参数传递给函数时,实际上会传递数组的副本。因此,如果需要在函数内对原数组进行修改,通常会使用切片或数组的指针。

2 Go 语言中的切片

2.1 切片的概念

在Go语言中,切片(Slice)是对数组的抽象。Go 数组的大小不可变,这一属性在某些情况下限制了数组的使用。Go 切片则设计得更为灵活,它提供了一种方便、灵活且功能强大的接口来序列化数据结构。切片本身没有数据,它只是对底层数组的引用。它们的动态特性主要表现在以下几点:

  • 动态大小:不像数组,切片的长度是动态的,它可以根据需要自动增长或缩短。
  • 灵活性:可以通过内置的append函数方便地添加元素到切片中。
  • 引用类型:切片通过引用来访问底层数组中的元素,不会创建数据的副本。

2.2 切片的声明与初始化

声明切片的语法与声明数组相似,但是在声明时不需要指定元素的数量。例如,声明一个整型切片的方式如下:

var slice []int

可以使用切片字面量来初始化一个切片:

slice := []int{1, 2, 3}

上述slice变量将初始化为包含三个整数的切片。

使用make函数也能初始化切片,其中可以指定切片的长度和容量:

slice := make([]int, 5)  // 创建一个长度和容量都是5的整型切片

如果需要更大的容量,可以将容量作为第三个参数传递给make函数:

slice := make([]int, 5, 10)  // 创建一个长度为5,容量为10的整型切片

2.3 切片与数组的关系

切片可以通过指定数组的一段来创建,形成对该段的引用。例如,给定以下数组:

array := [5]int{10, 20, 30, 40, 50}

我们可以通过如下方式创建一个切片:

slice := array[1:4]

这个切片slice将引用数组array中从索引1到索引3(包含索引1,不包含索引4)的元素。

请注意,切片实际上没有复制数组的值,它只是指向原始数组的某个连续片段。因此,对切片进行的修改也会影响底层数组,反之亦然。这种引用关系是理解和使用切片时至关重要的知识点。

2.4 切片的基本操作

2.4.1 索引

切片通过索引来访问其元素,与数组类似,索引从 0 开始。例如:

slice := []int{10, 20, 30, 40}
// 获取第一个和第三个元素
fmt.Println(slice[0], slice[2])

2.4.2 长度和容量

切片有两个属性:长度(len)和容量(cap)。长度是切片中元素的数量,容量是从切片的第一个元素开始,到其底层数组元素末尾的个数。

slice := []int{10, 20, 30, 40}
// 打印切片的长度和容量
fmt.Println(len(slice), cap(slice))

2.4.3 追加元素

使用 append 函数可以向切片追加元素。当切片的容量不足以放下新增的元素时,append 函数会自动扩展切片的容量。

slice := []int{10, 20, 30}
// 追加单个元素
slice = append(slice, 40)
// 追加多个元素
slice = append(slice, 50, 60)
fmt.Println(slice)

请注意,在使用 append 追加元素时,返回的可能是一个新的切片。如果底层数组容量不足,append 操作将会导致切片指向一个新的更大的数组。

2.5 切片的扩展与复制

切片可以使用 copy 函数来复制其元素到另一个切片中,目标切片必须已经分配了空间足以容纳复制的元素,并且不会改变目标切片的容量。

2.5.1 使用 copy 函数

如下代码展示了如何使用 copy

src := []int{1, 2, 3}
dst := make([]int, 3)
// 复制元素到目标切片
copied := copy(dst, src)
fmt.Println(dst, copied)

copy 函数返回复制的元素数量,且不会超过目标切片的长度和源切片的长度中较小的一个。

2.5.2 注意事项

在使用 copy 函数时,如果加入了新的元素复制,但目标切片没有足够的空间,那么只会复制到目标切片能够容纳的元素。

2.6 多维切片

多维切片是包含多个切片的切片。它类似于多维数组,但是由于切片的长度是可变的,多维切片更加灵活。

2.6.1 创建多维切片

创建一个二维的切片(切片的切片):

twoD := make([][]int, 3)
for i := 0; i < 3; i++ {
    twoD[i] = make([]int, 3)
    for j := 0; j < 3; j++ {
        twoD[i][j] = i + j
    }
}
fmt.Println("二维切片: ", twoD)

2.6.2 使用多维切片

多维切片使用和一维切片一样,通过索引来访问:

// 访问二维切片的元素
val := twoD[1][2]
fmt.Println(val)

3 数组和切片的应用比较

3.1 使用场景对比

数组和切片在Go语言中都用于存储同类型数据的集合,但它们在使用场景上存在明显的差异。

数组

  • 数组的长度在声明时就已经固定,因此适用于存储已知固定数量的元素。
  • 当需要一个总量不变的容器时,比如表示固定大小的矩阵,数组是最好的选择。
  • 数组可以在栈上分配空间,因此当数组大小不大时,可以获得更高的性能。

切片

  • 切片是动态数组的抽象,其长度是可变的,适合于存储数量未知或需要动态变化的元素集合。
  • 当你需要一个可以随时增长或缩小的动态数组时,比如保存不确定的用户输入,切片是更合适的选择。
  • 切片在内存中的布局可以使它很方便地实现对数组部分或全部的引用,常用于处理子字符串、切分文件内容等场景。

总结来说,数组适用于固定大小的需求场景,体现了Go语言对内存的静态管理特性;而切片更加灵活,是对数组一个抽象的扩展,便于处理动态集合。

3.2 性能考量

当我们需要选择是使用数组还是切片时,性能是一个重要的考量因素。

数组

  • 访问速度快,因为它是连续内存和固定索引。
  • 在栈上分配内存(如果数组大小已知且不是很大),不涉及额外的堆内存开销。
  • 没有额外的内存来存储长度和容量,这可能对内存敏感的程序是个好处。

切片

  • 动态增长或缩减会导致性能开销:增长可能导致分配新的内存并复制旧元素,缩减可能需要调整指针。
  • 切片操作本身很快,但如果经常增减元素,可能导致内存碎片化。
  • 虽然切片访问具有小额的间接开销,但通常不会对性能有太大影响,除非是在极其性能敏感的代码中。

因此,如果性能是关键考量,并且能够事先知道数据的大小,那么使用数组更合适。而如果需要灵活性和便利性则推荐使用切片,特别是对于大数据集的处理。

4 常见问题及解决策略

在Go语言的数组和切片使用过程中,开发者可能会遇到以下一些常见问题。

问题1:数组越界

  • 数组越界是指访问数组时使用的索引超出了其长度范围。这将导致程序运行时错误。
  • 解决方法:在访问数组元素之前,总是检查索引值是否在数组的有效范围内。这可以通过比较索引和数组的长度来完成。
var arr [5]int
index := 10 // 假设有一个超出范围的索引
if index < len(arr) {
    fmt.Println(arr[index])
} else {
    fmt.Println("索引超出了数组的范围。")
}

问题2:切片的内存泄漏

  • 切片可能会在不知不觉中持有原始数组的部分或全部的引用,即使是只需要其中很小一部分。如果原始数组很大,这将导致内存泄漏。
  • 解决方法:如果需要临时切片,可以考虑使用拷贝的方式创建一个新的切片。
original := make([]int, 1000000)
smallSlice := make([]int, 10)
copy(smallSlice, original[:10]) // 只复制所需的部分
// 这样,smallSlice没有引用original的其它部分,有助于GC回收不必要的内存

问题3:切片重用导致的数据错误

  • 由于切片的底层引用同一个数组,有可能在不同的切片中看到数据修改的影响,这可能导致不可预料的错误。
  • 解决方法:要避免这种情况,最好是创建一个新的切片副本。
sliceA := []int{1, 2, 3, 4, 5}
sliceB := make([]int, len(sliceA))
copy(sliceB, sliceA)
sliceB[0] = 100
fmt.Println(sliceA[0]) // 输出: 1
fmt.Println(sliceB[0]) // 输出: 100

以上只是Go语言在使用数组和切片时可能遇到的常见问题和解决方法,实际开发中可能会有更多细节需要注意,但遵循这些基本原则,可以避免很多常见错误。