• Go的切片:长度和容量


    原文链接:https://www.cnblogs.com/sunshineliulu/p/12244532.html

    虽然说 Go 的语法在很大程度上和 PHP 很像,但 PHP 中却是没有“切片”这个概念的,在学习的过程中也遇到了一些困惑,遂做此笔记。
    困惑1:使用 append 函数为切片追加元素后,切片的容量时变时不变,其扩容机制是什么?
    困惑2:更改切片的元素会修改其底层数组中对应的元素。为什么有些情况下更改了切片元素,其底层数组元素没有更改?

    一、切片的声明

    切片可以看成是数组的引用。在 Go 中,每个数组的大小是固定的,不能随意改变大小,切片可以为数组提供动态增长和缩小的需求,但其本身并不存储任何数据。

    /*
     * 这是一个数组的声明
     */
    var a [5]int //只指定长度,元素初始化为默认值0
    var a [5]int{1,2,3,4,5}
    
    /* 
     * 这是一个切片的声明:即声明一个没有长度的数组
     */
    // 数组未创建
    // 方法1:直接初始化
    var s []int //声明一个长度和容量为 0 的 nil 切片
    var s []int{1,2,3,4,5} // 同时创建一个长度为5的数组
    // 方法2:用make()函数来创建切片:var 变量名 = make([]变量类型,长度,容量)
    var s = make([]int, 0, 5)
    // 数组已创建
    // 切分数组:var 变量名 []变量类型 = arr[low, high],low和high为数组的索引。
    var arr = [5]int{1,2,3,4,5}
    var slice []int = arr[1:4] // [2,3,4]
    

    二、切片的长度和容量

    切片的长度是它所包含的元素个数。
    切片的容量是从它的第一个元素到其底层数组元素末尾的个数。
    切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。

    s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} // [0 1 2 3 4 5 6 7 8 9] len=10,cap=10
    s1 := s[0:5] // [0 1 2 3 4] len=5,cap=10
    s2 := s[5:] // [5 6 7 8 9] len=5,cap=5
    

      

    三、切片追加元素后长度和容量的变化

    1.append 函数

    Go 提供了内建的 append 函数,为切片追加新的元素。

    func append(s []T, vs ...T) []T
    

    append 的结果是一个包含原切片所有元素加上新添加元素的切片。

    下面分两种情况描述了向切片追加新元素后切片长度和容量的变化。
    Example 1:

    package main
    
    import "fmt"
    
    func main() {
        arr := [5]int{1,2,3,4,5} // [1 2 3 4 5]
        fmt.Println(arr)
    	
        s1 := arr[0:3] // [1 2 3]
        printSlice(s1)
        s1 = append(s1, 6)
        printSlice(s1)
        fmt.Println(arr)
    }
    
    func printSlice(s []int) {
        fmt.Printf("len=%d cap=%d %p %v
    ", len(s), cap(s), s, s)
    }
    

      

    执行结果如下:
    [1 2 3 4 5]
    len=3 cap=5 0xc000082030 [1 2 3]
    len=4 cap=5 0xc000082030 [1 2 3 6]
    [1 2 3 6 5]

    可以看到切片在追加元素后,其容量和指针地址没有变化,但底层数组发生了变化,下标 3 对应的 4 变成了 6。

    Example 2:

    package main
    
    import "fmt"
    
    func main() {
        arr := [5]int{1,2,3,4} // [1 2 3 4 0]
        fmt.Println(arr)
    	
        s2 := arr[2:] // [3 4 0]
        printSlice(s2)
        s2 = append(s2, 5)
        printSlice(s2)
        fmt.Println(arr)
    }
    
    func printSlice(s []int) {
        fmt.Printf("len=%d cap=%d %p %v
    ", len(s), cap(s), s, s)
    }
    

      

    执行结果如下:

    [1 2 3 4 0]
    len=3 cap=3 0xc00001c130 [3 4 0]
    len=4 cap=6 0xc00001c180 [3 4 0 5]
    [1 2 3 4 0]
    

      

    而这个切片在追加元素后,其容量和指针地址发生了变化,但底层数组未变。

    当切片的底层数组不足以容纳所有给定值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。

      

    2.切片的源代码学习

    Go 中切片的数据结构可以在源码下的 src/runtime/slice.go 查看。

    // go 1.3.16 src/runtime/slice.go:13
    type slice struct {
        array unsafe.Pointer
        len   int
        cap   int
    }
    

      

    可以看到,切片作为数组的引用,有三个属性字段:长度、容量和指向数组的指针。
    向 slice 追加元素的时候,若容量不够,会调用 growslice 函数,

    // go 1.3.16 src/runtime/slice.go:76
    func growslice(et *_type, old slice, cap int) slice {
        //...code
        
        newcap := old.cap
        doublecap := newcap + newcap
        if cap > doublecap {
    		newcap = cap
        } else {
            if old.len < 1024 {
                newcap = doublecap
            } else {
                // Check 0 < newcap to detect overflow
                // and prevent an infinite loop.
                for 0 < newcap && newcap < cap {
                    newcap += newcap / 4
                }
                // Set newcap to the requested cap when
                // the newcap calculation overflowed.
                if newcap <= 0 {
                    newcap = cap
                }
            }
        }
        
        // 跟据切片类型和容量计算要分配内存的大小
        var overflow bool
    	var lenmem, newlenmem, capmem uintptr
        switch {
            // ...code
        }
        
        // ...code...
        
        // 将旧切片的数据搬到新切片开辟的地址中
        memmove(p, old.array, lenmem)
        
        return slice{p, old.len, newcap}
    }
    

      

    从上面的源码,在对 slice 进行 append 等操作时,可能会造成 slice 的自动扩容。其扩容时的大小增长规则是:

    • 如果切片的容量小于 1024,则扩容时其容量大小乘以2;一旦容量大小超过 1024,则增长因子变成 1.25,即每次增加原来容量的四分之一。
    • 如果扩容之后,还没有触及原数组的容量,则切片中的指针指向的还是原数组,如果扩容后超过了原数组的容量,则开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组。

    上面的两个例子中,切片的容量均小于 1024 个元素,所以扩容的时候增长因子为 2,每增加一个元素,其容量翻番。
    Example2 中,因为切片的底层数组没有足够的可用容量,append() 函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值,所以原数组没有变化,不是我想象中的[1 2 3 4 5],

    3.切片扩容的内部实现

    扩容1:切片扩容后其容量不变

    slice := []int{1,2,3,4,5}
    // 创建新的切片,其长度为 2 个元素,容量为 4 个元素
    mySlice := slice[1:3]
    // 使用原有的容量来分配一个新元素,将新元素赋值为 40
    mySlice = append(mySlice, 40)
    

      

    执行上面代码后的底层数据结构如下图所示:

    扩容2:切片扩容后其容量变化

    // 创建一个长度和容量都为 5 的切片
    mySlice := []int{1,2,3,4,5}
    // 向切片追加一个新元素,将新元素赋值为 6
    mySlice = append(mySlice, 6)
    

      

    执行上面代码后的底层数据结构如下图所示:

    四、小结

    1. 切片是一个结构体,保存着切片的容量,长度以及指向数组的指针(数组的地址)。
    2. 尽量对切片设置初始容量值,以避免 append 调用 growslice,因为新的切片容量比旧的大,会开辟新的地址,拷贝数据,降低性能。
  • 相关阅读:
    【转】快速Redhat AS4和AS5升级至Centos系统
    linux 控制cpu利用率,已经远程访问linux和文件传输
    linux windows启动问题
    Skyline TEP5.1.3二次开发入门——初级(五)
    Skyline TEP5.1.3二次开发入门——初级(四)
    Skyline TEP5.1.3二次开发入门——初级(三)
    Skyline TEP5.1.3二次开发入门——初级(六)
    基于Skyline的TEP5.1.3实现对矢量SHP文件的加载和渲染
    如何在WPF中嵌入Skyline提供的三维控件
    共享一些可以通过网络访问的MPT地址
  • 原文地址:https://www.cnblogs.com/xull0651/p/14067809.html
Copyright © 2020-2023  润新知