• GO语言-切片底层探索(下)


    目录

    切片的底层数据结构

    扩容机制

    总结:

    练习验证代码


    这是切片的底层探索下篇,上篇地址请见:GO语言-切片底层探索(上)

    在上篇我们讲解了切片的两个重要实现或者说是两个特征

    1. 切片是引用类型,会进行引用传递;
    2. 切片会随着元素数量的增加,进行扩容,改变其底层数组的指向;

    这篇文章我们将会顺着讲解切片的底层数据结构和扩容机制

    切片的底层数据结构

    我们可以在src/runtime/slice.go下看到切片slice的底层数据结构:

    1. type slice struct {
    2. array unsafe.Pointer
    3. len int
    4. cap int
    5. }

    我们可以发现,切片slice的基础定义很简单:

    • 指针类型的array,指向底层数组
    • int类型的len,存储切片的长度
    • int类型的cap,存储切片的容量

    由于在slice结构体中直接定义了len、cap字段,因此我们平常使用len(slice)和cap(slice),其时间复杂度为O(1),不需要遍历整个切片。

    扩容机制

    Go语言切片的扩容机制由基本机制和调整机制组成

    1. func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
    2. .....
    3. newcap := oldCap
    4. doublecap := newcap + newcap
    5. if newLen > doublecap {
    6. newcap = newLen
    7. } else {
    8. const threshold = 256
    9. if oldCap < threshold {
    10. newcap = doublecap
    11. } else {
    12. // Check 0 < newcap to detect overflow
    13. // and prevent an infinite loop.
    14. for 0 < newcap && newcap < newLen {
    15. // Transition from growing 2x for small slices
    16. // to growing 1.25x for large slices. This formula
    17. // gives a smooth-ish transition between the two.
    18. newcap += (newcap + 3*threshold) / 4
    19. }
    20. // Set newcap to the requested cap when
    21. // the newcap calculation overflowed.
    22. if newcap <= 0 {
    23. newcap = newLen
    24. }
    25. }
    26. }
    27. ......
    28. }

    基本机制:

    1. 如果新切片的长度大于旧切片的二倍容量,就直接令新切片的容量等于新切片的长度
    2. 如果旧切片的容量小于256,进行二倍扩容
    3. 如果就切片的容量大于等于256,按照newcap+=(newcap+3*threshold)/4公式进行扩容,扩容速度减缓,向1.25倍进行过渡。

    在基本扩容规则的基础上,还会考虑元素类型与内存分配规则,对实际的扩张值做一些微调。从这一个基本规则中我们可以看出Go语言对slice性能和空间使用率的思考:

    • 当切片较小时,采用较大的扩容倍速,可以避免频繁地扩容,从而减少内容分配次数和数据拷贝的代价。
    • 当切片较大时,采用较小的扩容倍速,主要是为了避免空间浪费。

     因此,使用append()向slice添加一个元素的实现步骤如下:

    1. 假如slice容量够用,则将新元素追加进去,slice.len++,返回原slice
    2. 原slice容量不够,则将slice先扩容,扩容后得到新slice
    3. 将新元素追加进新slice,slice.len++,返回新的slice。

    总结:

    切片是go语言中常用的容器,由于其扩容特性,容易发生一些不容易被发现的错误,这就需要我们对切片的底层扩容机制有足够的了解,知道什么时候切片会进行扩容操作和如果高效地使用切片进行数据的存储和处理。

    练习验证代码

    1. package main
    2. import "fmt"
    3. type student struct {
    4. name string
    5. age int
    6. address string
    7. }
    8. func main() {
    9. //256以下的二倍扩容
    10. slice := make([]int, 255)
    11. fmt.Println(len(slice), cap(slice))
    12. newSlice := append(slice, 2)
    13. fmt.Println(len(newSlice), cap(newSlice))
    14. }
    15. /*func main() {
    16. //结构体指针类型切片cap为1024的1.5倍扩容
    17. array := make([]*student, 1024)
    18. fmt.Println(len(array), cap(array))
    19. slice := array
    20. slice = append(slice, &student{name: "wang", age: 12, address: "河南"})
    21. fmt.Println(len(slice), cap(slice))
    22. }*/
    23. /*func main() {
    24. //结构体类型切片cap为1024的1.6倍扩容
    25. array := make([]student, 1024)
    26. fmt.Println(len(array), cap(array))
    27. slice := array
    28. slice = append(slice, student{name: "wang", age: 12, address: "河南"})
    29. fmt.Println(len(slice), cap(slice))
    30. }*/
    31. //func main() {
    32. // //int类型的cap为1024的1.5倍扩容
    33. // array := make([]int, 1024)
    34. // fmt.Println(len(array), cap(array))
    35. // slice := array
    36. // slice = append(slice, 1)
    37. // fmt.Println(len(slice), cap(slice))
    38. //}
  • 相关阅读:
    汇舟问卷:想要挣钱?海外问卷调查不容错过!
    十五、Django之编辑员工和删除员工
    Python爬取小说
    C#内存管理
    Intellij Debugger slow: Method breakpoints may dramatically slow down debugging
    [Spring Cloud] GateWay自定义过滤器/结合Nacos服务注册中心
    【暑期每日一题】洛谷 P7398 [COCI2020-2021#5] Šifra~
    解释Python中的上下文管理器(with语句)的作用和用法
    分享一个制作AI视频的好工具
    吐槽 B 站收费,是怪它没钱么?
  • 原文地址:https://blog.csdn.net/wangye135/article/details/136620991