35

快速理解Go数组和切片的内部实现原理

 5 years ago
source link: https://studygolang.com/articles/14147?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

很多人对Go语言的 arrayslice 傻傻分不清楚,今天我们就从底层出发,来聊聊它俩到底有什么区别。

数组

几乎所有计算机语言,数组的实现都是相似的:一段连续的内存,Go语言也一样,Go语言的数组底层实现就是一段连续的内存空间。每个元素有唯一一个索引(或者叫 下标 )来访问。如下图所示,下图是 [5]int{1:10, 2:20} 数组的内部实现逻辑图:

b6bEfiq.png!web

由于内存连续,CPU很容易计算索引(即数组的 下标 ),可以快速迭代数组里的所有元素。 Go语言的数组不同于C语言或者其他语言的数组,C语言的数组变量是指向数组第一个元素的指针;而Go语言的数组是一个值,Go语言中的数组是值类型,一个数组变量就表示着整个数组,意味着Go语言的数组在传递的时候,传递的是原数组的拷贝。你可以理解为Go语言的数组是一种有序的 struct

slice

切片是一个很小的对象,是对数组进行了抽象,并提供相关的操作方法。切片有三个属性字段:长度、容量和指向数组的指针。

bYZVr2i.png!web

上图中, ptr 指的是指向array的pointer, len 是指切片的长度, cap 指的是切片的容量。现在,我想你对数组和切片有了一个本质的认识。

切片有多种声明方式,每种初始化方式对应的逻辑图是怎样的呢?

对于 s := make([]byte, 5)s := []byte{...} 的方式

NFvYJ3z.png!web

对于 s = s[2:4] 的方式

AV3Urer.png!web

对于 nil 的切片即 var s []byte 对应的逻辑图是

2eyaEba.png!web

在此有一个说明: nil 切片和 切片是不太一样的,空切片即 s := make([]byte, 0) 或者 s := []byte{} 出来的切片 空切片的逻辑图为:

RVjaYfz.png!web

空切片指针不为nil,而nil切片指针为nil。但是,不管是空切片还是nil切片,对其调用内置函数 append()lencap 的效果都是一样的,感受不到任何区别。

扩容

slice这种数据结构便于使用和管理数据集合,可以理解为是一种“动态数组”, slice 也是围绕动态数组的概念来构建的。既然是动态数组,那么slice是如何扩容的呢?

请记住以下两条规则:

  • 如果切片的容量小于1024个元素,那么扩容的时候slice的cap就翻番,乘以2;一旦元素个数超过1024个元素,增长因子就变成1.25,即每次增加原来容量的四分之一。
  • 如果扩容之后,还没有触及原数组的容量,那么,切片中的指针指向的位置,就还是原数组,如果扩容之后,超过了原数组的容量,那么,Go就会开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组。

知道了一下规则,请看下面程序,试问输出结果:

import (
    "fmt"
)
func main(){
    array := [4]int{10, 20, 30, 40}
    slice := array[0:2]
    newSlice := append(append(append(slice, 50), 100), 150)
    newSlice[1] += 1
    fmt.Println(slice)
}

输出:

[10 20]

答对了吗?

参考文献: 《Go in action》 https://blog.golang.org/go-slices-usage-and-internals


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK