Go 语言中的 slice 类型

1. 数组和 slice

1.1. 数组

数组是具有固定长度且拥有零个或者多个相同数据类型元素的序列。由于数组的长度固定,所以在 Go 中很少直接使用。

go 复制代码
var arr1 [3]int              // 定义一个长度为 3,元素类型为 int 的数组,元素的值为其类型的零值
arr2 := [3]int{1, 2, 3}      // 定义一个长度为 3,元素类型为 int 的数组,并用给定的值初始化每个元素
arr3 := [...]int{4, 5, 6, 7} // 如果使用 ... 代替数组的长度,Go 会根据初始化时数组元素的数量确定数据的长度
arr4 := [5]int{1: 10, 3: 30} // 支持索引为 1、3 的元素的值,其他索引的值为其类型的零值

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

输出:

css 复制代码
3 [0 0 0]
3 [1 2 3]
4 [4 5 6 7]
5 [0 10 0 30 0]

1.2. slice

在 Go 中,slice 表示一个拥有相同类型元素的可变长度的序列,写作 []T,其中元素的类型都是 T

slice 的定义位于 src/runtime/slice.go

go 复制代码
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

其中:

  • array:指向 slice 底层数组第一个元素的指针;
  • len:slice 中当前存储的元素个数,可以用内置函数 len() 获取;
  • cap:slice 底层数组的长度,可以用内置函数 cap() 获取。

1.3. 数组和 slice 的区别

  1. 数组长度是固定的,使用前必须确定长度。slice 长度可变。
  2. 数组是基于连续的内存空间存储数据的,作为函数参数传递时传递的是数组的拷贝。slice 是一个包含指向底层数组的指针、长度和容量的结构体。
  3. 当两个数组长度相同,且数组元素类型可比较时,可以使用 ==!= 来比较这两个数组;slice 只能和 nil 比较。

2. 创建 slice

go 复制代码
s1 := make([]int, 5)     // 创建长度为 5,容量为 5(默认容量等于长度)的 slice
s2 := make([]int, 3, 10) // 创建长度为 3,显式指定容量为 10 的 slice
s3 := []int{1, 2, 3}     // 创建长度为 3,容量为 3 的 slice
s4 := []int{99: 1}       // 创建长度为 100,容量为 100 的 slice。索引为 99 的元素显式初始化为 1,其他元素为类型的零值(0)

fmt.Println(len(s1), cap(s1))
fmt.Println(len(s2), cap(s2))
fmt.Println(len(s3), cap(s3))
fmt.Println(len(s4), cap(s4))

3. 扩容 slice

3.1. append 函数

内置函数 append 用于将元素追加到 slice 的后面,返回追加后的 slice。

go 复制代码
s = append(s, elem)

3.2. slice 的扩容策略

Slice 的扩容策略位于 src/runtime/slice.go

go 复制代码
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
    ...
}

func nextslicecap(newLen, oldCap int) int {
    ...
}

在 Go 1.18 及之后:

  1. 如果 slice 扩容后的元素个数大于两倍扩容前的容量,新容量等于扩容后的元素个数;
  2. 如果扩容前的容量小于 256,新容量等于两倍扩容前的容量;
  3. 如果扩容前的容量大于等于 256,新容量初始值等于扩容前的容量,每次增加 25% + 192,直到值大于新长度。

在 Go 1.18 之前

  1. 如果 slice 扩容后元素个数大于两倍扩容前的容量,新容量等于扩容后元素个数;
  2. 如果扩容前的容量小于 1024,新容量等于两倍扩容前的容量;
  3. 如果扩容前的容量大于等于 1024,新容量初始值等于扩容前的容量,每次增加 25%,直到值大于新长度。

如何理解这次变动呢?比较下两个方式的扩容因子:

起始容量 Go 1.18 之前扩容因子 Go 1.18 及之后扩容因子
< 256 2.0 2.0
256 2.0 2.0
512 2.0 1.625
1024 1.25 1.4375
2048 1.25 1.34375
4096 1.25 1.296875
超大容量 1.25 无限趋近 1.25

可以看到,旧方式中,旧容量从 1024 开始扩容因子突降至 1.25;新方式中,旧容量从 256 开始,扩容因子平滑过渡到 1.25。

3.3. 当 slice 作为函数参数

当 slice 作为函数参数传递时,一般选择直接传递 slice 的值 ,而不会传递 slice 的指针

当我们将 slice 作为函数参数传递时,实质上传递的是一个指向底层数组的指针、元素的个数(len)和底层数组的长度(cap)。函数内部对 lencap 的修改在函数外部不会生效,而函数内部对 slice 元素的修改则需要分情况讨论。

如果在函数内部只修改了 slice 元素的值,在函数外部这些修改也会生效;如果在函数内部对 slice 进行了扩容(修改了 len),则分为以下两种情况:

  1. 扩容后 slice 的 cap 没有改变。此时函数外部 slice 和函数内部 slice 指向的底层数组一致,所以函数内部的修改在函数外部也是生效的。但是函数外部 slice 的 len 没有改变,因此看不到使用 append 函数扩容的元素内容。但函数内部对扩容前长度范围内元素的修改会影响到函数外。
  2. 扩容后 slice 的 cap 发生了改变。此时 append 操作会重新创建一个新的底层数组,这种情况下函数内外的 slice 指向的底层数组不一样,对函数内 slice 的修改完全不影响函数外的 slice。

如果函数内部需要改变原 slice 的长度或容量,并需要在函数外部生效,可以选择返回一个新的 slice 让函数外部接收。

相关推荐
追随者永远是胜利者12 小时前
(LeetCode-Hot100)253. 会议室 II
java·算法·leetcode·go
追随者永远是胜利者12 小时前
(LeetCode-Hot100)207. 课程表
java·算法·leetcode·go
追随者永远是胜利者17 小时前
(LeetCode-Hot100)169. 多数元素
java·算法·leetcode·go
追随者永远是胜利者1 天前
(LeetCode-Hot100)226. 翻转二叉树
java·算法·leetcode·职场和发展·go
追随者永远是胜利者1 天前
(LeetCode-Hot100)200. 岛屿数量
java·算法·leetcode·职场和发展·go
追随者永远是胜利者1 天前
(LeetCode-Hot100)301. 删除无效的括号
java·算法·leetcode·职场和发展·go
追随者永远是胜利者1 天前
(LeetCode-Hot100)239. 滑动窗口最大值
java·算法·leetcode·职场和发展·go
追随者永远是胜利者1 天前
(LeetCode-Hot100)215. 数组中的第K个最大元素
java·算法·leetcode·职场和发展·go
golang学习记1 天前
Go 1.26 新特性速览:更安全、更快、更聪明的 Go
后端·go