目录
例3:使用append逐个添加元素和一次性添加多个元素的区别
例5:当容量大于1024的时候,每次扩容真的是1.25倍吗?
本文是对上一篇文章的补充:
go slice切片的详细知识(包含底层扩容)-CSDN博客
一次性添加多个元素:slice = append(slice, 1, 2, 3)
逐个添加元素:slice = append(slice, 1)、slice = append(slice, 2)、slice = append(slice, 3)
- // 示例 1: 一次性添加多个元素
- var s1 []int
- s1 = append(s1, 1, 2, 3)
- fmt.Printf("%p %v %d %d\n", s1, s1, len(s1), cap(s1)) // 0xc000020090 [1 2 3] 3 3
-
- var slice1 []int = []int{1, 2}
- fmt.Printf("%p %v %d %d\n", slice1, slice1, len(slice1), cap(slice1)) // 0xc000128010 [1 2] 2 2
- slice1 = append(slice1, 3, 4, 5)
- fmt.Printf("%p %v %d %d\n", slice1, slice1, len(slice1), cap(slice1)) // 0xc000120060 [1 2 3 4 5] 5 6
-
- // 示例 2: 逐个添加元素
- var slice2 []int = []int{1, 2}
- fmt.Printf("%p %v %d %d\n", slice2, slice2, len(slice2), cap(slice2)) // 0xc000128060 [1 2] 2 2
- slice2 = append(slice2, 3)
- fmt.Printf("%p %v %d %d\n", slice2, slice2, len(slice2), cap(slice2)) // 0xc00012c020 [1 2 3] 3 4
- slice2 = append(slice2, 4)
- fmt.Printf("%p %v %d %d\n", slice2, slice2, len(slice2), cap(slice2)) // 0xc00012c020 [1 2 3 4] 4 4
- slice2 = append(slice2, 5)
- fmt.Printf("%p %v %d %d\n", slice2, slice2, len(slice2), cap(slice2)) // 0xc00012e000 [1 2 3 4 5] 5 8
内容最终相同,但它们的内存分配情况可能不同。多次调用 append 可能会导致多次内存分配,而一次性添加多个元素则可能只需要进行一次内存分配和复制。
切片在被截取时的另一个特点是,被截取后的数组仍然指向原始切片的底层数据。
如:bar 执行了 append 函数之后,最终也修改了 foo 的最后一个元素,这是一个在实践中非常常见的陷阱。
- foo := []int{0, 0, 0, 42, 100}
- bar := foo[1:4]
-
- fmt.Println(len(foo), cap(foo), foo) // 5 5 [0 0 0 42 100]
- fmt.Println(len(bar), cap(bar), bar) // 3 4 [0 0 42]
-
- fmt.Printf("%p %p\n", foo, bar) // 0xc00001c1b0 0xc00001c1b8 虽然地址不同(切片结构体还有长度和容量属性,所以切片结构体地址不同),但是指向的底层数组是同一个,因为没有扩容
-
- bar = append(bar, 99)
- fmt.Println(len(foo), cap(foo), foo) // 5 5 [0 0 0 42 99]
- fmt.Println(len(bar), cap(bar), bar) // 4 4 [0 0 42 99]
如果要解决这样的问题,其实可以在截取时指定容量:order[low:high:max]
- foo := []int{0, 0, 0, 42, 100}
- bar := foo[1:4:4]
- // bar := foo[1:4:3] // 报错:Invalid index values, must be low <= high <= max
-
- fmt.Println(len(foo), cap(foo), foo) // 5 5 [0 0 0 42 100]
- fmt.Println(len(bar), cap(bar), bar) // 3 3 [0 0 42]
-
- fmt.Printf("%p %p\n", foo, bar) // 0xc00001c1b0 0xc00001c1b8
-
- bar = append(bar, 99)
- fmt.Println(len(foo), cap(foo), foo) // 5 5 [0 0 0 42 100]
- fmt.Println(len(bar), cap(bar), bar) // 4 6 [0 0 42 99]
解释foo[1:4:4]:
练习:
- sliceA := make([]int, 5, 10)
- sliceB := sliceA[0:5]
- sliceC := sliceA[0:5:5]
- fmt.Println(len(sliceA), cap(sliceA)) // 5 10
- fmt.Println(len(sliceB), cap(sliceB)) // 5 10
- fmt.Println(len(sliceC), cap(sliceC)) // 5 5
- orderLen := 5
- order := make([]uint16, 2*orderLen)
-
- pollorder := order[:orderLen:orderLen]
- lockorder := order[orderLen:][:orderLen:orderLen]
- // pollorder切片指的是order的前半部分切片,lockorder指的是order的后半部分切片,即原order分成了两段。所以,pollorder和lockerorder的长度和容量都是5。
- fmt.Println(len(pollorder), cap(pollorder)) // 5 5
- fmt.Println(len(lockorder), cap(lockorder)) // 5 5
- sli := make([]int, 0)
- sli = append(sli, []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}...)
- s := sli[5:][:5] // sli 和 s 共享同一个底层数组
- // sli[5:]:[6, 7, 8, 9, 10]
- // [:5]:从上述新切片中再取前5个元素,结果是[6, 7, 8, 9, 10]
-
- fmt.Println(s) // [6 7 8 9 10]
-
- s[0] = 111
- fmt.Println(s, sli) // [111 7 8 9 10] [1 2 3 4 5 111 7 8 9 10]
1024 直接扩容到 1536,不是1.25倍,而是1.5倍,这是为什么?(对于容量大于等于 1024 的切片,在扩容时 Go 并不是简单地按照1.25倍扩容,而是使用了一种更复杂的策略。)
- s2 := make([]int, 1024)
- fmt.Printf("s2: len: %d, cap: %d\n", len(s2), cap(s2)) // s2: len: 1024, cap: 1024
- s2 = append(s2, 1)
- fmt.Printf("s2: len: %d, cap: %d\n", len(s2), cap(s2)) // s2: len: 1025, cap: 1536
向 slice 追加元素的时候,若容量不够,会调用 growslice 函数:
- func growslice(et *_type, old slice, cap int) slice {
- // ……
-
- for 0 < newcap && newcap < cap {
- // Transition from growing 2x for small slices
- // to growing 1.25x for large slices. This formula
- // gives a smooth-ish transition between the two.
- newcap += (newcap + 3*threshold) / 4
- }
- // ……
- capmem = roundupsize(uintptr(newcap) * ptrSize)
- newcap = int(capmem / ptrSize)
- }
for循环:会不断循环直到 newcap 超过所需的容量。最终的结果可能会比严格的1.25倍略大一些,具体取决于当前的容量和内存分配的优化策略。
最后两行代码:对 newcap 作了一个内存对齐,这个和内存分配策略相关,所以最终结果不一定是 1.25的整数倍(有时候扩容和元素类型的字节数有关系)。
Go 语言的切片扩容机制是相当复杂的,它考虑了多种因素来确定新的容量,以便在性能和内存使用之间找到平衡。Go 语言的 runtime 库在执行切片扩容时,有时会为了减少频繁的内存分配而使用稍大的倍数。这种优化主要是为了减少内存分配次数,提高性能。
Go 语言中切片扩容的策略为: