Cwww3's Blog

Record what you think

0%

slice

Slice

1
2
3
4
5
type SliceHeader struct {
Data uintptr
Len int
Cap int
}

初始化

1
2
3
arr[0:3] or slice[0:3]
slice := []int{1, 2, 3}
slice := make([]int, 10)
  • Empty
1
2
3
// 底层数据长度为0
nums := make([]int,0)
nums := []int{}
  • Nil
1
2
// 未分配底层数组
var nums []int
  • empty和nil都可以使用append,底层会申请内存进行扩容
  • 字面量创建
1
2
3
4
5
6
7
var vstat [3]int // 1.根据切片中的元素数量对底层数组的大小进行推断并创建一个数组;
vstat[0] = 1 // 2.将这些字面量元素存储到初始化的数组中;
vstat[1] = 2
vstat[2] = 3
var vauto *[3]int = new([3]int) // 3.创建一个同样指向 [3]int 类型的数组指针;
*vauto = vstat // 4.将静态存储区的数组 vstat 赋值给 vauto 指针所在的地址;
slice := vauto[:] //5.通过 [:] 操作获取一个底层使用 vauto 的切片;

[:] 就是使用下标创建切片的方法,从这一点也能看出 [:] 操作是创建切片最底层的一种方法。

  • 关键字创建

如果使用字面量的方式创建切片,大部分的工作都会在编译期间完成。但是当我们使用 make 关键字创建切片时,很多工作都需要运行时的参与;调用方必须向 make 函数传入切片的大小以及可选的容量。

1
2
3
1. 检查len是否传入,检查len是否小于cap
2. 当切片发生逃逸或者非常大时,运行时需要 runtime.makeslice 在堆上初始化切片
3. 当前的切片不会发生逃逸并且切片非常小时,会使用与字面量相同的方式创建

使用 makeslice 创建切片

1
2
3
4
5
6
7
8
9
10
11
12
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}

return mallocgc(mem, et, true)
}

计算切片所需空间并在堆上申请一段连续的内存 (内存空间=切片中元素大小×切片容量)

如果发生一下情况,会崩溃

  • 内存空间的大小发生了溢出;
  • 申请的内存大于最大可分配的内存;
  • 传入的长度小于 0 或者长度大于容量;

mallocgc 是用于申请内存的函数,遇到了比较小的对象会直接初始化在 Go 语言调度器里面的 P 结构中,而大于 32KB 的对象会在堆上初始化。

追加和扩容

1
2
3
4
5
a := make([]int,0,1)
// 当容量足够时,直接添加
a = append(a,1)
// 当容量不足时,先扩容(拷贝原先的数据),再添加 这是底层数组已经改变
a = append(a,2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
}
...
}
}
...
}

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

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

上述代码片段仅会确定切片的大致容量,下面还需要根据切片中的元素大小对齐内存,当数组中元素所占的字节大小为 1、8 或者 2 的倍数时,运行时会使用如下所示的代码对齐内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var overflow bool
var lenmem, newlenmem, capmem uintptr
switch {
case et.size == 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.size == sys.PtrSize:
lenmem = uintptr(old.len) * sys.PtrSize
newlenmem = uintptr(cap) * sys.PtrSize
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
newcap = int(capmem / sys.PtrSize)
case isPowerOfTwo(et.size):
...
default:
...
}

// roundupsize函数会将待申请的内存向上取整 取整时会使用 runtime.class_to_size 数组
var class_to_size = [_NumSizeClasses]uint16{0,8,16,32,48,64,80,...}

例子

1
2
3
4
5
6
7
8
9
a = []int{}
a = a.append(1,2,3,4,5) // len=5 cap=6
// 期望容量(5)大于原容量的两倍(2*0) 得到新容量(5) 占用内存(5*8byte)
// 进行内存对齐得到(48)最终容量(48/8==6)

a = []int{1,2,3,4}
a = a.append(5) // len=5 cap=8
// 期望容量(5)小于1024 (2*0) 新容量为原容量的两倍(8) 占用内存(8*8byte)
// 进行内存对齐得到(64)最终容量(64/8==8)

拷贝切片

1
2
// 将src的内容拷贝到dst中  拷贝的大小取两者中的较小值
copy(dst, src)

传值和传指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

func myAppend(s []int) []int {
// 这里 s 虽然改变了,但并不会影响外层函数的 s
s = append(s, 100)
return s
}

func myAppendPtr(s *[]int) {
// 会改变外层 s 本身
*s = append(*s, 100)
return
}

func main() {
s := []int{1, 1, 1}
newS := myAppend(s)

fmt.Println(s) // 1,1,1
fmt.Println(newS) // 1,1,1,100

s = newS

myAppendPtr(&s)
fmt.Println(s) // 1,1,1,100,1000
}
Donate comment here.