找回密码
 立即注册
首页 业界区 业界 Golang基础笔记三之数组和切片

Golang基础笔记三之数组和切片

慎气 2025-6-21 11:02:25
本文首发于公众号:Hunter后端
原文链接:Golang基础笔记三之数组和切片
这一篇笔记介绍 Golang 里的数组和切片,以下是本篇笔记目录:

  • 数组定义和初始化
  • 数组属性和相关操作
  • 切片的创建
  • 切片的长度和容量
  • 切片的扩容
  • 切片操作
1、数组定义与初始化

第一篇笔记的时候介绍过数组的定义与初始化,这里再介绍一下。
数组是具有固定长度的相同类型元素的序列。
这里有两个点需要注意,数组的长度是固定的,数组的元素类型是相同的,且在定义的时候就确定好的。
1. 一维数组

我们可以通过下面几种方式对数组进行定义和赋值:
  1.     var arr [3]int
  2.     arr[0] = 1
  3.     arr[1] = 2
  4.     arr[2] = 3
  5.     fmt.Println("arr: ", arr)
复制代码
也可以在定义的时候直接对其赋值:
  1.     var arr [3]int = [3]int{1, 2, 3}
  2.     fmt.Println("arr: ", arr)
复制代码
或者定义的时候不指定数量,自动获取:
  1.     var arr = [...]int{1, 2, 3}
  2.     fmt.Println("arr: ", arr)
复制代码
还可以在定义的时候,指定索引位置的值:
  1.     var arr = [...]string{0: "Peter", 3: "Tome", 1: "Hunter"}
  2.     fmt.Println("arr: ", arr)
复制代码
2. 多维数组

多维数组一般是二维数组用的较多,示例如下,表示一个两行三列的二维数组:
  1.     var s [2][3]int
  2.     for i := 0; i < 2; i++ {
  3.         for j := 0; j < 3; j++ {
  4.             s[i][j] = 0
  5.         }
  6.     }
  7.     fmt.Println(s)
  8.     // [[0 0 0] [0 0 0]]
复制代码
2、数组属性和相关操作

1. 获取数组长度和容量

获取数组长度和容量分别使用 len() 和 cap() 函数。
  1.     arr := [...]int{2, 3, 4}
  2.     fmt.Println("len: ", len(arr))
  3.     fmt.Println("cap: ", cap(arr))
复制代码
对于数组而言,数组的长度是固定的,所以其长度和容量都是一样的。
对于长度和容量的概念,我们在后面介绍切片的时候,再详细介绍。
2. 数组的复制

我们可以通过 copy() 函数将一个数组复制到另一个数组,其返回值是复制元素的个数:
  1.     arr := [3]int{2, 3, 4}
  2.     var arr2 [3]int
  3.     numCopied := copy(arr2[:], arr[:])
  4.     fmt.Printf("复制的元素个数:%d, arr2:%v\n", numCopied, arr2)
复制代码
3. 数组的排序

我们可以引入 sort 包,使用 sort.Ints() 函数对数组进行排序:
  1. import "sort"
  2. func main() {
  3.     arr := [3]int{5, 2, 4}
  4.     sort.Ints(arr[:])
  5.     fmt.Println(arr) // 2, 4, 5
  6. }
复制代码
3、切片的创建

切片是对数组的一个连续片段的引用,它本身不存储数据,而是指向底层数据。
切片由三部分组成:指针,长度,容量。
指针指向引用的数组的起始位置,长度则是切片中元素的数量,容量则是从切片起始位置到底层数据末尾的元素数量。
下面介绍切片创建的几种方式。
1. 引用数组创建切片

切片本身的定义就是对数组的引用,所以可以通过引用数组的方式来创建切片:
  1. var arr = [3]int{1, 2, 3}
  2. var slice = arr[1:]
  3. fmt.Println(slice) // [2 3]
复制代码
在这里,切片 slice 是从 arr 第二个元素开始引用,因此 slice 的内容是 [2, 3]。
注意,这里 slice 的切片是引用的 arr 数组,所以他们指向的是同一个内存空间,如果修改切片内的元素,会同步影响数组的元素,而修改数组的元素,也会影响切片内容:
  1.     var arr = [3]int{1, 2, 3}
  2.     var slice = arr[1:]
  3.     fmt.Printf("修改前: arr:%v, slice:%v\n", arr, slice)
  4.     // 修改前: arr:[1 2 3], slice:[2 3]
  5.     arr[1] = 7
  6.     fmt.Printf("修改 arr 后,arr:%v, slice:%v\n", arr, slice)
  7.     // 修改 arr 后,arr:[1 7 3], slice:[7 3]
  8.     slice[1] = 10
  9.     fmt.Printf("修改 slice 后,arr:%v, slice:%v\n", arr, slice)
  10.     // 修改 slice 后,arr:[1 7 10], slice:[7 10]
复制代码
2. 创建数组的方式创建切片

使用创建数组的方式不定义其长度,创建的就是一个切片:
  1. slice := []int{1, 2, 3}
复制代码
3. make 的方式创建切片

使用 make 的方式创建切片,可以指定切片的长度和容量,其格式如下:
  1. var 切片名 []type = make([]type, len, [cap])
复制代码
make 函数接受三个参数,第一个就是切片类型,第二个是切片长度,第三个是切片容量,其中切片容量是可选参数,如不填写则默认等于切片长度。
以下是一个创建切片的示例:
  1.     slice := make([]int, 3)
  2.     fmt.Printf("slice length:%d, cap:%d\n", len(slice), cap(slice)) // 3 3
复制代码
4、切片的长度和容量

切片的长度和容量分别使用 len() 和 cap() 函数来获取。
长度的概念很好理解,就是切片的元素个数就是它的长度。
而对于容量,可以理解是这个切片预留的总长度,而如果切片是从数组中引用而来,其定义是 从切片的第一个元素到引用数组的最后一个元素的长度就是切片的容量。
对于下面这个 arr,其长度是 5,两个切片分别从第三个和第五个元素开始引用:
  1.     arr := [5]int{1,2,3,4,5}
  2.     slice1 := arr[2:4]
  3.     slice2 := arr[4:]
复制代码
对于 slice1, 它的长度就是 2,因为它引用的元素个数是两个
slice2 的长度是 1,它是从第五个元素开始引用,直到数组结尾。
但是两个切片的容量因为其开始引用的下标的不同而不一致,原数组总长度为 5
slice1 是从下标为 2 开始引用,所以它的容量是 5-2=3
slice2 是从下标为 4 开始引用,它的容量是 5-4=1
  1.     fmt.Printf("slice1 length:%d, cap:%d\n", len(slice1), cap(slice1))
  2.     // slice1 length:2, cap:3
  3.     fmt.Printf("slice2 length:%d, cap:%d\n", len(slice2), cap(slice2))
  4.     // slice2 length:1, cap:1
复制代码
5、切片的扩容

当我们向一个切片添加元素,且其长度超出了定义的容量大小,这个就涉及到切片扩容的概念。
首先,我们可以创建一个切片,然后查看其长度和容量:
  1.     slice := make([]int, 2, 2)
  2.     slice[0] = 1
  3.     slice[1] = 2
  4.     fmt.Printf("slice length:%d, cap:%d, %p\n", len(slice), cap(slice), slice)
  5.     // slice length:2, cap:2, addr:0xc0000120c0
复制代码
注意,在上面创建 slice 的时候,它的长度和容量如果是一样的话,可以默认不填写 cap 参数,这里为了表示清楚,所以显式指定其长度和容量。
当我们向 slice 再添加一个元素,就已经超出了其容量大小了,因此切片会自动进行扩容,其容量会变为原来的两倍,接着可以看到切片地址已经发生了变化:
  1.     slice = append(slice, 3)
  2.     fmt.Printf("slice length:%d, cap:%d, addr:%p\n", len(slice), cap(slice), slice)
  3.     // slice length:3, cap:4, addr:0xc000020160
复制代码
而如果再往其中添加两个元素,其容量又会扩大,变为原来的两倍,变成 8:
  1.     slice = append(slice, 4)
  2.     slice = append(slice, 5)
  3.     fmt.Printf("slice length:%d, cap:%d, addr:%p\n", len(slice), cap(slice), slice)
  4.     // slice length:5, cap:8, addr:0xc00001c180
复制代码
切片自动扩容规律

关于 Golang 里切片的自动扩容规律,之前搜索到这样一个扩容规律:

  • 如果新元素追加后所需要的容量超过原容量的两倍,新容量会直接设为所需的容量
  • 当原切片长度小于 1024 时,新容量会是原来容量的两倍
  • 当原切片的大于等于 1024 时,新容量会是原来容量 1.25 倍
但是这个规律并不完全准确,下面是基于 go1.22.6 版本做的相应的测试:
  1.     slice := make([]int, 2)
  2.     for _ = range 1028 {
  3.         slice = append(slice, 1)
  4.         fmt.Printf("slice length:%d, cap:%d, addr:%p\n", len(slice), cap(slice), &slice)
  5.     }
复制代码
对于输出的结果,后面的 cap 的变化趋势是 32, 64, 128, 256, 512, 848, 1280。
可以看到,在容量为 512 之前,确实是遵循两倍扩容的规律,但是 512 之后的扩容规律则不再是两倍,而且 1024 之后的扩容也不是 1.25 倍。
因此,去查询相关资料和源代码,发现切片自动扩容的计算分为两个阶段,第一个阶段是扩容容量计算阶段,第二个阶段是内存对齐阶段。
1) 扩容容量计算

第一阶段进行扩容容量计算的源代码如下:
[code]func nextslicecap(newLen, oldCap int) int {    newcap := oldCap    doublecap := newcap + newcap    if newLen > doublecap {        return newLen    }    const threshold = 256    if oldCap < threshold {        return doublecap    }    for {        newcap += (newcap + 3*threshold) >> 2        if uint(newcap) >= uint(newLen) {            break        }    }    if newcap
您需要登录后才可以回帖 登录 | 立即注册