原文:Go语言进阶:数组与切片

Array (数组)

数组 Array 是一片连续的内存区域,存储相同类型的元素,元素的个数固定。在Go语言中,数组Array不能进行扩容、在复制和传递时为值复制。且数组的长度决定了其容量。数组的长度可以通过内置函数len()来获取。

数组Array声明

Go语言中,数组声明主要有三种方式(其他方式一般为以下三种方式的变种)

1
2
3
var arr1 [5]int // 默认方式,定义一个长度为5的int类型数组,元素自动初始化为0
var arr2 = [5]int{1, 2, 3, 4, 5} // 声明并初始化一个长度为5的int类型数组
arr3 := [...]int{1, 2, 3, 4, 5} // 使用...让Go自动计算数组长度

数组Array的优缺点分析

优点

  1. 类型安全:数组中的所有元素都是同一类型,这有助于确保类型的一致性和安全性。
  2. 内存连续:数组在内存中占用连续的空间,这使得访问数组元素非常高效。
  3. 性能可预测:由于数组的大小是固定的,因此其性能表现是可预测的,没有切片可能带来的扩容开销。

缺点

  1. 固定长度:数组的长度在创建后不能改变,这限制了其灵活性。
  2. 使用不便:在实际编程中,经常需要动态调整集合的大小,而数组无法满足这一需求,因此切片通常更受欢迎。
  3. 传递开销:当数组作为参数传递给函数时,如果数组很大,将发生值的完整复制,可能导致不必要的性能开销。虽然可以通过指针传递数组来避免这个问题,但这增加了代码的复杂性。

Slice(切片)

Slice本身并不存储数据,而是对底层数组的引用,并包含指向数组起始元素的指针、切片长度以及切片的容量等信息。

由于Slice的这种特性,它可以在不改变底层数组的情况下进行动态地增长和缩小,使得在处理可变大小的集合时更加高效和灵活。

Slice(切片)声明与初始化

下面是slice的常见声明方式

1
2
3
4
slice1 := []int{1, 2, 3} // 声明并初始化Slice
var slice2 []int         // 声明Slice但不分配空间
slice3 := make([]int, 3) // 使用make函数声明Slice
slice4 := make([]int, 3, 5) // 使用make函数声明Slice, 长度为3, 容量为5

Slice(切片)扩容的实现原理

切片使用append函数添加元素,但不是使用了append函数就需要进行扩容,如下代码向长度为3,容量为4的切片a中添加元素后不需要扩容。

1
slice := make([]int, 3, 4) // 使用make函数声明Slice, 长度为3, 容量为4

当你向切片中添加元素,而切片的容量不足以容纳更多元素时,Go 会创建一个新的、容量更大的底层数组,并将原有元素复制到新数组中,这个过程即为扩容。

切片扩容的实现原理涉及以下几个步骤:

  1. 计算新容量:当你向切片追加元素时,如果容量不足,Go 会根据当前容量计算一个新的容量。新容量的计算方式通常是将当前容量翻倍,但这不是绝对的。如果当前容量小于1024个元素,那么通常会翻倍;如果大于1024个元素,增长因子会逐渐减小,增长到原来的1.25倍左右。
  2. 分配新数组:根据计算出的新容量,Go 会分配一个新的底层数组。
  3. 复制元素:将原切片中的元素复制到新的底层数组中。
  4. 更新切片数组指针:切片在Go中是由一个结构体表示的,包含指向底层数组的指针array、切片的长度len和容量cap。在扩容后,需要更新这个结构体的信息,指向新的底层数组,并更新长度和容量的值。

Slice(切片)的优缺点

优点

  1. 动态大小:与数组不同,切片的长度是动态的,可以根据需要增长或缩小。这使得切片非常灵活,适用于不确定大小的数据集合。
  2. 内存效率:切片背后是数组,它们可以共享同一个底层数组,这意味着在多个切片之间传递数据时,可以避免数据的复制,提高内存使用效率。
  3. 便捷操作:Go语言为切片提供了许多内置函数,如append、copy等,使得对切片的操作非常方便。
  4. 引用类型:切片是引用类型,这意味着当你将切片传递给函数或从函数返回切片时,传递的是引用而不是整个数据的副本。
  5. 内置函数支持:Go语言的内置函数可以直接作用于切片,例如len可以获取切片的长度,cap可以获取切片的容量。

缺点

  1. 非线程安全:切片不是线程安全的,如果在多个goroutine中同时操作同一个切片(特别是进行写操作),可能会导致竞态条件。需要外部同步机制来保证并发安全。
  2. 内存泄漏风险:由于切片是对底层数组的引用,如果切片的某个元素指向了一个大的内存块,即使只有一个小的切片在使用它,整个内存块也不会被垃圾回收,可能导致内存泄漏。
  3. 性能开销:切片的动态扩容可能会导致性能开销,因为每次扩容都需要分配新的数组并复制数据。如果不合理地使用切片,可能会导致频繁的内存分配和复制。
  4. 容量管理:虽然切片的动态扩容提供了便利,但是对于性能敏感的应用,开发者需要仔细管理切片的容量,以避免不必要的扩容操作。
  5. 隐式行为:切片的一些行为可能不是很直观,比如切片的扩容规则、切片之间共享底层数组的行为等,这可能会导致一些难以发现的bug。