go slice切片的详细知识(包含底层扩容)——2

目录

例子

例3:使用append逐个添加元素和一次性添加多个元素的区别

例4:order[low:high:max]

例5:当容量大于1024的时候,每次扩容真的是1.25倍吗?


本文是对上一篇文章的补充:

go slice切片的详细知识(包含底层扩容)-CSDN博客


例子

例3:使用append逐个添加元素和一次性添加多个元素的区别

一次性添加多个元素:slice = append(slice, 1, 2, 3)

  • 性能:可能更高效,因为只进行了一次内存分配和复制操作。
  • 代码简洁:代码更简洁明了。

逐个添加元素:slice = append(slice, 1)、slice = append(slice, 2)、slice = append(slice, 3)

  • 性能:可能较低,因为每次添加元素时,切片可能需要多次进行内存分配和复制操作(取决于底层数组的容量)。
  • 内存分配:可能会多次触发内存分配操作。
  • 代码冗长:代码较为冗长,不如一次性添加多个元素的方式简洁。
Go 复制代码
// 示例 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 可能会导致多次内存分配,而一次性添加多个元素则可能只需要进行一次内存分配和复制。

例4:order[low:high:max]

切片在被截取时的另一个特点是,被截取后的数组仍然指向原始切片的底层数据。

如:bar 执行了 append 函数之后,最终也修改了 foo 的最后一个元素,这是一个在实践中非常常见的陷阱。

Go 复制代码
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]

Go 复制代码
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]:

  • low 是 1,表示新切片从 foo 的索引 1 开始(包含这个元素)。
  • high 是 4,表示新切片到 foo 的索引 4 结束(不包含这个元素)。
  • max 是 4,表示新切片的容量是从 low 开始到 max 结束的长度。

练习:

Go 复制代码
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
Go 复制代码
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
Go 复制代码
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]

例5:当容量大于1024的时候,每次扩容真的是1.25倍吗?

1024 直接扩容到 1536,不是1.25倍,而是1.5倍,这是为什么?(对于容量大于等于 1024 的切片,在扩容时 Go 并不是简单地按照1.25倍扩容,而是使用了一种更复杂的策略。)

Go 复制代码
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 函数:

Go 复制代码
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 语言中切片扩容的策略为:

  • 如果新申请容量(cap)大于旧容量(old.cap)的两倍,则最终容量(newcap)是新申请的容量(cap);
  • 如果旧切片的长度小于 1024,则最终容量是旧容量的 2 倍,即"newcap=doublecap";
  • 如果旧切片的长度大于或等于 1024,则最终容量从旧容量开始循环增加原来的 1/4,直到最终容量大于或等于新申请的容量为止;
  • 如果最终容量计算值溢出,即超过了 int 的最大范围,则最终容量就是新申请容量。
相关推荐
hkNaruto15 小时前
【P2P】【Go】采用go语言实现udp hole punching 打洞 传输速度测试 ping测试
golang·udp·p2p
入 梦皆星河15 小时前
go中常用的处理json的库
golang
海绵波波10717 小时前
Gin-vue-admin(2):项目初始化
vue.js·golang·gin
每天写点bug18 小时前
【go每日一题】:并发任务调度器
开发语言·后端·golang
一个不秃头的 程序员18 小时前
代码加入SFTP Go ---(小白篇5)
开发语言·后端·golang
基哥的奋斗历程18 小时前
初识Go语言
开发语言·后端·golang
ZVAyIVqt0UFji1 天前
go-zero负载均衡实现原理
运维·开发语言·后端·golang·负载均衡
唐墨1231 天前
golang自定义MarshalJSON、UnmarshalJSON 原理和技巧
开发语言·后端·golang
老大白菜1 天前
FastAPI vs Go 性能对比分析
开发语言·golang·fastapi
千年死缓1 天前
golang结构体转map
开发语言·后端·golang