GoLang中的切片扩容机制

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了GoLang中的切片扩容机制相关的知识,希望对你有一定的参考价值。

参考技术A [5]int 是数组,而 []int 是切片。二者看起来相似,实则是根本上不同的数据结构。

切片的数据结构中,包含一个指向数组的指针 array ,当前长度 len ,以及最大容量 cap 。在使用 make([]int, len) 创建切片时,实际上还有第三个可选参数 cap ,也即 make([]int, len, cap) 。在不声明 cap 的情况下,默认 cap=len 。当切片长度没有超过容量时,对切片新增数据,不会改变 array 指针的值。

当对切片进行 append 操作,导致长度超出容量时,就会创建新的数组,这会导致和原有切片的分离。在下例中

由于 a 的长度超出了容量,所以切片 a 指向了一个增长后的新数组,而 b 仍然指向原来的老数组。所以之后对 a 进行的操作,对 b 不会产生影响。

试比较

本例中, a 的容量为6,因此在 append 后并未超出容量,所以 array 指针没有改变。因此,对 a 进行的操作,对 b 同样产生了影响。

下面看看用 a := []int 这种方式来创建切片会是什么情况。

可以看到,空切片的容量为0,但后面向切片中添加元素时,并不是每次切片的容量都发生了变化。这是因为,如果增大容量,也即需要创建新数组,这时还需要将原数组中的所有元素复制到新数组中,开销很大,所以GoLang设计了一套扩容机制,以减少需要创建新数组的次数。但这导致无法很直接地判断 append 时是否创建了新数组。

如果一次添加多个元素,容量又会怎样变化呢?试比较下面两个例子:

那么,是不是说,当向一个空切片中插入 2n-1 个元素时,容量就会被设置为 2n 呢?我们来试试其他的数据类型。

可以看到,根据切片对应数据类型的不同,容量增长的方式也有很大的区别。相关的源码包括: src/runtime/msize.go , src/runtime/mksizeclasses.go 等。

我们再看看切片初始非空的情形。

可以看到,与刚刚向空切片添加5个int的情况一致,向有3个int的切片中添加2个int,容量增长为6。

需要注意的是, append 对切片扩容时,如果容量超过了一定范围,处理策略又会有所不同。可以看看下面这个例子。

具体为什么会是这样的变化过程,还需要从 源码 中寻找答案。下面是 src/runtime/slice.go 中的 growslice 函数中的核心部分。

GoLang中的切片扩容机制,与切片的数据类型、原本切片的容量、所需要的容量都有关系,比较复杂。对于常见数据类型,在元素数量较少时,大致可以认为扩容是按照翻倍进行的。但具体情况需要具体分析。

Golang Slice切片

1. 切片的定义和初始化

  • 切片是基于数组类型的封装,非常灵活,可以自动扩容
  • 切片是真正意义上的动态数组,而且是一个引用类型,切片指向一个底层数组

定义切片

var 切片名[] 切片类型

创建切片方式

package main

import "fmt"

func main() {
    // 1. 声明切片
    var s1 []int
    if s1 == nil { // 只声明没有初始化,所以是nil
        fmt.Println("为空")
    } else {
        fmt.Println("不为空")
    }

    // 2. := 自动推导
    s2 := []int{}

    // 3. make(切片类型, 长度, 容量)
    // 容量是可选的
    var s3 []int = make([]int, 0)

    fmt.Println(s1, s2, s3)

    // 4. 初始化赋值
    var s4 []int = make([]int, 0, 0)
    fmt.Println(s4)

    s5 := []int{1,2,3}
    fmt.Println(s5)

    // 5. 从数组切片
    arr := [5]int{1,2,3,4,5}
    var s6 []int
    s6 = arr[1:4]
    fmt.Println(s6)
}

2. 切片操作

操作 含义
s[n] 切片 s 中索引位置为 n 的项
s[:] 从切片 s 的索引位置 0 到 len(s)-1 处所获得的切片
s[low:] 从切片 s 的索引位置 low 到 len(s)-1 处所获得的切片
s[:high] 从切片 s 的索引位置 0 到 high 处所获得的切片,len=high
s[low:high] 从切片 s 的索引位置 low 到 high 处所获得的切片,len=high-low
s[low: high:max] 从切片 s 的索引位置 low 到 high 处所获得的切片,len=high-low, cap=max-low
len(s) 切片 s 的长度,总是<=cap()
cap(s) 切片 s 的容量,总是>=len(s)
package main

import "fmt"

func op01() {
    // 定义数组
    array := [...]int{1, 2, 3, 4, 5, 6, 7, 9, 0}

    // 切片
    fmt.Println(array[:])
    fmt.Println(array[1:5])
    fmt.Println(array[:6])
    fmt.Println(array[2:])
    fmt.Println(array[2])
}

func op02() {
    s1 := make([]int, 3, 5)
    fmt.Printf("s1: %v, len: %d, cap: %d", s1, len(s1), cap(s1))
}

func op03() {
    // 初始化一个数组
    arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8}

    s1 := arr[2:5]
    fmt.Println(s1)

    // 可以向后扩展,默认是不扩展,不可以向前扩展
    s2 := s1[2:7]
    fmt.Println(s2)
}

func main() {
    op01()
    op02()
    op03()
}

3. append()函数

向 slice 尾部添加数据,返回新的 slice 对象

package main

import "fmt"

func main() {
    // 空切片
    var s001 []int

    s001 = append(s001, 1)
    s001 = append(s001, 3)
    s001 = append(s001, 6)
    fmt.Println(s001)

    // 初始化内存的切片
    s002 := make([]int, 5)
    s002 = append(s002, 5, 8, 9)
    fmt.Println(s002)

    // 初始化默认值的切片
    s003 := []int{1, 3, 6}
    s003 = append(s003, 4, 99, 2)
    fmt.Println(s003)
}
package main

import "fmt"

func main() {
    // 初始化一个数组
    arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8}

    // 初始化一个从数组切片的slice
    s1 := arr[2:]
    fmt.Println(arr)
    fmt.Println(s1)

    // 修改s1切片的值
    // view(视图)操作,会修改原数组的值
    s1[0] = 100
    fmt.Println(s1)
    fmt.Println(arr)
}

// 输出结果
// [0 1 2 3 4 5 6 7 8]
// [2 3 4 5 6 7 8]
// [100 3 4 5 6 7 8]
// [0 1 100 3 4 5 6 7 8]
package main

import "fmt"

func main() {
    arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
    s1 := arr[2:6]
    fmt.Println(s1)
    s2 := s1[3:5]
    fmt.Println(s2)

    // 映射到底层是覆盖操作
    s3 := append(s2, 10)
    fmt.Println(s3)
    fmt.Println(arr)
    fmt.Println()

    // 底层去自动扩容, 此时打印的是原数组
    s4 := append(s3, 11)
    fmt.Println(s4)
    fmt.Println(arr)

    s5 := append(s4, 12)
    fmt.Println(s5)
    fmt.Println(arr)
}
package main

import "fmt"

func main() {
    var a []int = []int{1, 2, 3}
    var b []int = []int{4, 5, 6}

    a = append(a, 7, 8)
    fmt.Println(a)

    // append无法直接添加一个切片,需要使用...将切片打散
    a = append(a, b...)
    fmt.Println(a)
}

4. copy()函数

在两个 slice 间复制数据,两个 slice 可指向同一底层数组

package main

import "fmt"

func main() {
    arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}
    s1 := arr[8:] // [8 9 10 11 12 13 14]
    s2 := arr[:5] // [0 1 2 3 4]
    fmt.Println(s1, s2)

    // 从左侧开始覆盖
    copy(s1, s2)
    fmt.Println(s1, s2) // [0 1 2 3 4 13 14] [0 1 2 3 4]
    fmt.Println(arr)    // [0 1 2 3 4 5 6 7 0 1 2 3 4 13 14]
}

5. 切片扩容机制

用 make()创建切片,可以指定容量,当容量存储到上限,底层会自动扩容

package main

import "fmt"

func main() {
    var a []int = make([]int, 5, 10)
    fmt.Printf("a=%v, len=%d, cap=%d
", a, len(a), cap(a))

    a[0] = 10
    a[1] = 20
    fmt.Printf("a=%v, len=%d, cap=%d
", a, len(a), cap(a))

    a = append(a, 30)
    fmt.Printf("a=%v, len=%d, cap=%d
", a, len(a), cap(a))

    for i := 0; i <= 8; i++ {
        a = append(a, i)
        fmt.Printf("for a=%v, len=%d, cap=%d
", a, len(a), cap(a))

    }
    fmt.Printf("a=%v, len=%d, cap=%d
", a, len(a), cap(a))
}

切片再切片

package main

import "fmt"

func main() {
    a := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    fmt.Printf("a=%v, len=%d, cap=%d
", a, len(a), cap(a))
    b := a[1:3]
    fmt.Printf("b=%v, len=%d, cap=%d
", b, len(b), cap(b))

    // 切片再切片
    b = b[:cap(b)]
    fmt.Printf("b=%v, len=%d, cap=%d
", b, len(b), cap(b))
}

空切片的扩容机制

package main

import "fmt"

func main() {
    var a []int
    fmt.Printf("a=%v, len=%d, cap=%d
", a, len(a), cap(a))

    a = append(a, 10)
    fmt.Printf("a=%v, len=%d, cap=%d
", a, len(a), cap(a))
    a = append(a, 20)
    fmt.Printf("a=%v, len=%d, cap=%d
", a, len(a), cap(a))
    a = append(a, 30)
    fmt.Printf("a=%v, len=%d, cap=%d
", a, len(a), cap(a))
    a = append(a, 40)
    fmt.Printf("a=%v, len=%d, cap=%d
", a, len(a), cap(a))
    a = append(a, 50)
    fmt.Printf("a=%v, len=%d, cap=%d
", a, len(a), cap(a))
    a = append(a, 60)
    fmt.Printf("a=%v, len=%d, cap=%d
", a, len(a), cap(a))
}

以上是关于GoLang中的切片扩容机制的主要内容,如果未能解决你的问题,请参考以下文章

Golang:map的定义操作排序以及map切片

Golang:map的定义操作排序以及map切片

Golang:map的定义操作排序以及map切片

Golang:map的定义操作排序以及map切片

Golang Slice切片

go切片遍历