Golang-slice相关问题

slice的底层实现

  1. 底层数组(Underlying Array):切片引用一个底层数组。这个数组包含实际的元素数据。切片的长度(len)表示切片包含的元素数量,容量(cap)表示底层数数组中的可用元素数量。

  2. 切片结构(Slice Header):切片本身是一个轻量级的数据结构,通常由Go运行时(runtime)管理。切片结构包含了以下信息:

  • 指向底层数数组的指针
  • 切片的长度(len
  • 切片的容量(cap

这个切片结构允许切片引用底层数数组中的一部分数据。切片的容量决定了切片能够增长的最大长度,但不能超过底层数数组的容量。

  1. 动态扩容:当切片的长度超过其容量时,Go运行时会分配一个新的底层数组,并将原始数据复制到新的数组中。新的切片引用新的底层数数组,原始底层数数组可能会被回收。

  2. 引用语义:切片是引用类型,多个切片可以引用同一个底层数数组,这使得数据的共享成为可能。修改一个切片中的数据会影响到共享同一底层数数组的其他切片。

数组和slice的区别

  1. 长度固定 vs. 长度可变:
  • 数组:数组的长度是固定的,在声明时需要指定长度,且不能更改。一旦创建,数组的大小保持不变。
  • 切片:切片的长度可以动态改变。它是对数组的一个引用,可以根据需要动态增加或缩小长度。
  1. 内存分配:
  • 数组:数组的内存是静态分配的,分配时就会占用固定大小的内存空间。
  • 切片:切片是动态分配的,它引用底层数组的一部分,根据需要分配内存。这意味着切片可以更加灵活地管理内存,但也可能导致较多的内存分配和垃圾回收操作。
  1. 值语义 vs. 引用语义:
  • 数组:数组是值类型,当你将一个数组赋值给另一个数组时,会进行数据拷贝,创建一个独立的副本。
  • 切片:切片是引用类型,它是对底层数组的引用。当你将一个切片赋值给另一个切片时,它们共享同一个底层数组,而不进行数据拷贝。
  1. 长度和容量:
  • 数组:数组的长度是固定的,没有容量的概念。
  • 切片:切片有长度(len)和容量(cap)两个属性。长度表示切片当前包含的元素数量,容量表示底层数组的大小,即切片可以增长的最大长度。
  1. 用法:
  • 数组:通常用于固定大小的数据集,如矩阵、固定大小的缓冲区等。
  • 切片:用于动态管理数据集,支持添加、删除、截取等操作。
1// 声明一个数组,长度为3 
2var arr [3]int 
3// 创建一个切片,长度为0,容量为3 
4slice := make([]int, 0, 3)

切片动态扩容

以go 1.18+来说,

原来的slice 容量oldcap小于256的时候,新 slice 的容量newcap是oldcap 的2倍;

当oldcap容量大于等于 256 的时候,newcap会有个计算公式,threshold = 256:

newcap += (newcap + 3*threshold) / 4

再对 newcap 作了一个内存对齐,这个和内存分配策略相关。进行内存对齐之后,新 slice 的容量是要 大于等于 按照前半部分生成的newcap。

扩容前后的slice是否相同?

情况一:原数组还有容量可以扩容(实际容量没有填充完),这种情况下,扩容以后的数组还是指向原来的数组, 对一个切片的操作可能影响多个指针指向相同地址的slice。

情况二:原来数组的容量已经达到了最大值,再想扩容,go默认会先开一片内存区域,把原来的值拷贝过来,然后再执行append()操作。这种情况丝毫不影响原数组。

1slice1 := []int{1, 2, 3} // 初始时,slice1 和底层数数组关联 
2slice2 := append(slice1, 4) // 扩容,slice2 引用新的底层数数组,而不是 slice1 的底层数数组

在上述例子中,slice1slice2在扩容后分别引用了不同的底层数数组,它们不会共享底层数数组,因此对slice2的修改不会影响slice1

nil切片和空切片指向的地址一样吗?

  1. nil 切片:表示一个未分配内存的切片,它的指针部分为nil,即没有指向任何有效的内存。当你声明一个切片但没有分配内存或将一个切片显式设置为nil时,将是一个nil 切片
1var a []int
2data1 := (*reflect.SliceHeader)(unsafe.Pointer(&a)).Data // 0
  1. 空切片:空切片是一个切片,但长度和容量都为0,指向已分配内存的空间,它的指针部分不为空。它实际上是指向一个有效的内存地址,但由于其长度和容量为0,不能用于存储任何数据
1b := make([]int, 0)
2c := make([]int, 0)
3data2 := (*reflect.SliceHeader)(unsafe.Pointer(&b)).Data // 824634859200
4data3 := (*reflect.SliceHeader)(unsafe.Pointer(&c)).Data // 824634859200

nil切片引用数组指针地址为0(无指向任何实际地址; 空切片的引用数组指针地址是有的,且固定为一个值。

json库对nil slice和空slice的处理是一致的吗?

  • nil切片表示一个未初始化的切片,它没有底层数数组。JSON库通常会将其编码为JSON中的null,表示不存在或未初始化。
  • 空切片表示一个已初始化但没有元素的切片,它有一个底层数数组,但长度为0。JSON库通常会将其编码为JSON中的[],表示一个空数组。

因此,对于JSON库来说,nil切片和空切片是不同的状态,它们被编码为不同的JSON值。在解码JSON时,JSON库通常会根据这些不同的编码值还原为nil切片和空切片。这种行为有助于维护Go程序中的数据一致性。

拷贝大切片一定比小切片代价大吗?

在Go中,切片是由一个SliceHeader结构来描述的,它包含了切片的地址、长度和容量信息,而实际的数据存储在底层的数组中。拷贝切片时,只是复制了SliceHeader结构本身,而不会复制底层数组的数据。

因此,切片的大小(元素数量)对拷贝操作的代价没有直接影响,拷贝操作的代价通常较小,与切片的大小无关。这种设计使得切片在Go中可以高效地进行操作,因为数据复制仅涉及SliceHeader结构的复制,而不是实际的数据。

是小柒鸭
是小柒鸭
佛系·猫奴·程序媛

在无聊的时间里就从事学习。 —— 亚伯拉罕·林肯