0
点赞
收藏
分享

微信扫一扫

golang slice详解

young_d807 2022-11-14 阅读 107

简介

​slice​​ 翻译成中文就是切片,它和数组(array)很类似,可以用下标的方式进行访问, 如果越界,就会产生 panic, 但是它比数组更灵活,可以自动地进行扩容。

slice与array区别

array 类型

array是固定长度的数组,使用前必须确定数组长度

​golang array​​ 特点:

  • golang中的数组是值类型,也就是说,如果你将一个数组赋值给另外一个数组,那么,实际上就是整个数组拷贝了一份
  • 如果golang中的数组作为函数的参数,那么实际传递的参数是一份数组的拷贝,而不是数组的指针
  • array的长度也是Type的一部分,这样就说明[10]int和[20]int是不一样的。

slice类型

  • slice是一个引用类型,是一个动态的指向数组切片的指针。
  • slice是一个不定长的,总是指向底层的数组array的数据结构。
  1. 创建slice

//动态数组创建,类似创建数组,但是没有指定固定长度
var al []int //创建slice
sl := make([]int,10) //创建有10个元素的slice
sl:=[]int{1,2,3} //创建有初始化元素的slice

2.先创建数组,在数组的基础上建立切片slice

var arr =[10]{1,2,3,4,5。6}  
sl := arr[2:5] //创建有3个元素的slice

3.slice有一些简便的操作

  • ​slice​​​的默认开始位置是0,​​ar[:n]​​等价于​​ar[0:n]​
  • ​slice​​​的第二个序列默认是数组的长度,​​ar[n:]​​等价于​​ar[n:len(ar)]​

array和slice的区别

声明数组时,方括号内写明了数组的长度或者...,声明slice时候,方括号内为空 作为函数参数时,数组传递的是数组的副本,而slice传递的是指针

slice数据结构

// runtime/slice.go
type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
}

  • 指针,指向底层数组;
  • 长度,表示切片可用元素的个数,也就是说使用下标对 slice 的元素进行访问时,下标不能超过 slice 的长度;
  • 容量,底层数组的元素个数,容量 >= 长度。在底层数组不进行扩容的情况下,容量也是 slice 可以扩张的最大限度。

追加扩容

向切片中追加元素应该是最常见的切片操作,在 Go 语言中我们会使用 append 关键字向切片追加元素

代码栗子

slice1 := make([]int,1,)
fmt.Println("cap of slice1",cap(slice1))
slice1 = append(slice1,1)
fmt.Println("cap of slice1",cap(slice1))
slice1 = append(slice1,2)
fmt.Println("cap of slice1",cap(slice1))

fmt.Println()

slice1024 := make([]int,1024)
fmt.Println("cap of slice1024",cap(slice1024))
slice1024 = append(slice1024,1)
fmt.Println("cap of slice1024",cap(slice1024))
slice1024 = append(slice1024,2)
fmt.Println("cap of slice1024",cap(slice1024))

output

cap of slice1 1
cap of slice1 2
cap of slice1 4

cap of slice1024 1024
cap of slice1024 1280
cap of slice1024 1280

原理

append 中间代码生成阶段的 cmd/compile/internal/gc.state.append 方法会拆分 append 关键字,该方法 追加元素会根据返回值是否会覆盖原变量,分别进入两种流程,如果 append 返回的『新切片』不需要赋值回原有的变量,就会进入如下的处理流程:

// append(slice, 1, 2, 3)
ptr, len, cap := slice
newlen := len + 3
if newlen > cap {
ptr, len, cap = growslice(slice, newlen)
newlen = len + 3
}
*(ptr+len) = 1
*(ptr+len+1) = 2
*(ptr+len+2) = 3
return makeslice(ptr, newlen, cap)

我们会先对切片结构体进行解构获取它的数组指针、大小和容量,如果在追加元素后切片的大小大于容量, 那么就会调用 runtime.growslice 对切片进行扩容并将新的元素依次加入切片;如果 append 后的切片会覆盖 原切片,即 slice = append(slice, 1, 2, 3),cmd/compile/internal/gc.state.append 就会使用另一种方式改写关键字:

// slice = append(slice, 1, 2, 3)
a := &slice
ptr, len, cap := slice
newlen := len + 3
if uint(newlen) > uint(cap) {
newptr, len, newcap = growslice(slice, newlen)
vardef(a)
*a.cap = newcap
*a.ptr = newptr
}
newlen = len + 3
*a.len = newlen
*(ptr+len) = 1
*(ptr+len+1) = 2
*(ptr+len+2) = 3

是否覆盖原变量的逻辑其实差不多,最大的区别在于最后的结果是不是赋值会原有的变量, 如果我们选择覆盖原有的变量,也不需要担心切片的拷贝,因为 Go 语言的编译器已经对这种情况作了优化。

到这里我们已经通过 append 关键字被转换的控制流了解了在切片容量足够时如何向切片中追加元素, 但是当切片的容量不足时就会调用 runtime.growslice 函数为切片扩容, 扩容就是为切片分配一块新的内存空间并将原切片的元素全部拷贝过去,我们分几部分分析该方法:

func growslice(et *_type, old slice, cap int) slice {
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
if newcap <= 0 {
newcap = cap
}
}
}

在分配内存空间之前需要先确定新的切片容量,Go 语言根据切片的当前容量选择不同的策略进行扩容:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于 1024 就会将容量翻倍;
  3. 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

确定了切片的容量之后,就可以计算切片中新数组占用的内存了,计算的方法就是将目标容量和元素大小相乘, 计算新容量时可能会发生溢出或者请求的内存超过上限,在这时就会直接 panic,不过相关的代码在这里就被省略了:

var overflow bool
var newlenmem, capmem uintptr
switch {
...
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem, _ = math.MulUintptr(et.size, uintptr(newcap))
capmem = roundupsize(capmem)
newcap = int(capmem / et.size)
}
...
var p unsafe.Pointer
if et.kind&kindNoPointers != 0 {
p = mallocgc(capmem, nil, false)
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
p = mallocgc(capmem, et, true)
if writeBarrier.enabled {
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem)
}
}
memmove(p, old.array, lenmem)
return slice{p, old.len, newcap}
}

小结

append 的时候发生扩容的动作

append 单个元素,或者 append 少量的多个元素,这里的少量指 double 之后的容量能容纳,这样就会走以下扩容流程,不足 1024,双倍扩容,超过 1024 的,1.25 倍扩容。

若是 append 多个元素,且 double 后的容量不能容纳,直接使用预估的容量。

此外,以上两个分支得到新容量后,均需要根据 slice 的类型 size,算出新的容量所需的内存情况 capmem,然后再进行 capmem 向上取整,得到新的所需内存,除上类型 size,得到真正的最终容量,作为新的 slice 的容量。


举报

相关推荐

0 条评论