深入学习Go-4 切片

切片,长度是不固定的,当容量不足时,进行动态扩容,所以又叫动态数组。

数据结构

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
  • array:指向底层数组的指针

  • len:切片长度

  • cap:切片容量,即底层数组的长度

初始化

可以使用make和new初始化切片,区别是make返回切片本身,new返回的是切片的地址值。例子如下:


func initial() {
    slice1 := make([]int, 5, 8)
    slice2 := new([]int)

    fmt.Println("slice1的类型是:", reflect.TypeOf(slice1))
    fmt.Println("slice2的类型是:", reflect.TypeOf(slice2))

    slice1 = append(slice1, 1)
    *slice2 = append(*slice2, 1)

    fmt.Println("slice1的值是:", slice1)
    fmt.Println("slice2的值是:", *slice2)
}

执行结果:

图片

切片值复制

Go语言中的切片在赋值、函数传参及函数返回值时也都是值复制。切片值复制指的是slice结构体的复制,因此指向底层数组的指针复制后仍然指向同一个数组地址。

func valueCopy() {
    slice1 := make([]int, 1, 4)
    slice2 := slice1
    fmt.Printf("slice1的地址: %p, 值: %v\n", &slice1, slice1)
    fmt.Printf("slice2的地址: %p, 值: %v\n", &slice2, slice2)
    
    slice1[0] = 1
    fmt.Println("修改slice1的值: ")
    fmt.Printf("slice1的地址: %p, 值: %v\n", &slice1, slice1)
    fmt.Printf("slice2的地址: %p, 值: %v\n", &slice2, slice2)
}

执行结果:

图片

从执行结果中可以看出,slice1和slice2的地址值不同,是两个不同的切片,将slice1的第一个元素赋值为1,slice2的第一个元素的值也变成了1。说明slice1与slice2的底层数组是同一个。

切片扩容

切片可以动态扩容,当切片底层数组空间不足时,会触发扩容。重新分配一块更大的内存,将原来底层数组的数据复制到新分配的底层数组上,然后将新的数据追加进去。

使用append函数向切片追加元素,当容量不足时,调用src/runtime/slice.go的growslice函数,核心代码如下:

func growslice(et *_type, old slice, cap int) slice {
    ......
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.cap < 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
            }
        }
    }
    ......
}

根据以上代码可以看出切片扩容的逻辑:

1,如果新申请的容量大于原来容量的2倍,则最终容量就是新申请的容量。

func capacity() {
    // 新申请的容量大于原来容量的2倍
    slice1 := make([]int, 1)
    fmt.Printf("slice1的长度: %d, 容量: %d\n", len(slice1), cap(slice1))
    
    slice1 = append(slice1, 1, 2)
    fmt.Println("扩容后: ")
    fmt.Printf("slice1的长度: %d, 容量: %d\n", len(slice1), cap(slice1))
}

执行结果:

图片

slice1原来的容量是1,追加2个元素后,容量是3,大于原来容量的2倍,所以最终容量就是3。

2,如果原来的容量小于1024,且新申请的容量小于原来容量的2倍,则最终容量就是原来容量的2倍。

func capacity() {
    // 原来的容量小于1024
    slice2 := make([]int, 1)
    fmt.Println("扩容前: ")
    fmt.Printf("slice2的长度: %d, 容量: %d\n", len(slice2), cap(slice2))
    
    slice2 = append(slice2, 1)
    fmt.Println("第一次扩容后: ")
    fmt.Printf("slice2的长度: %d, 容量: %d\n", len(slice2), cap(slice2))
    
    slice2 = append(slice2, 2)
    fmt.Println("第二次扩容后: ")
    fmt.Printf("slice2的长度: %d, 容量: %d\n", len(slice2), cap(slice2))
}

执行结果:

图片

3,如果原来的容量大于或等于1024,则最终容量是原始容量再增加原来的四分之一。由于涉及到内存对齐,所以 最终容量 >= 原始容量 + 原始容量*1/4。

func capacity() {
    // 原来的容量大于或等于1024
    slice3 := make([]int, 1024)
    fmt.Println("扩容前: ")
    fmt.Printf("slice3的长度: %d, 容量: %d\n", len(slice3), cap(slice3))
    
    slice3 = append(slice3, 1)
    fmt.Println("第一次扩容后: ")
    fmt.Printf("slice3的长度: %d, 容量: %d\n", len(slice3), cap(slice3))
    
    tmp := make([]int, 257)
    slice3 = append(slice3, tmp...)
    fmt.Println("第二次扩容后: ")
    fmt.Printf("slice3的长度: %d, 容量: %d\n", len(slice3), cap(slice3))
}

执行结果:

图片

slice3原始容量是1024,第一次扩容,追加一个元素后,容量变成了1024+1024/4=1280;第二次扩容,追加了257个元素后,容量变成了1696,大于1280+1280*1/4,进行了内存对齐。

总结

切片底层是结构体类型,有一个指向底层数组的指针,因此在传值时会指向同一个底层数组。

切片扩容的逻辑:

1,如果新申请的容量大于原来容量的2倍,则最终容量就是新申请的容量。

2,如果原来的容量小于1024,且新申请的容量小于原来容量的2倍,则最终容量就是原来容量的2倍。

3,如果原来的容量大于或等于1024,则最终容量是原始容量再增加原来的四分之一。由于涉及到内存对齐,所以 最终容量 >= 原始容量 + 原始容量*1/4。


更多【分布式专辑】【架构实战专辑】系列文章,请关注公众号

猜你喜欢

转载自blog.csdn.net/lonewolf79218/article/details/121669951