Go 切片Slice的长度len与容量cap

0x00 简介
Go 语言之旅示例 切片的长度与容量

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

package main

import "fmt"

func main() {
    s := []int{2, 3, 5, 7, 11, 13}
    printSlice(s)

    // 截取切片使其长度为 0
    s = s[:0]
    printSlice(s)

    // 拓展其长度
    s = s[:4]
    printSlice(s)

    // 舍弃前两个值
    s = s[2:]
    printSlice(s)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []
len=4 cap=6 [2 3 5 7]
len=2 cap=4 [5 7]
1
2
3
4
0x01 程序运行
1,第一个输出为[2,3,5,7,11,13],长度为6,容量为6;

2,左指针和右指针同时指向s[0],所以长度为0,容量为6;

3,左指针指向s[0],右指针指向s[4],由于切片概念是只包含左边元素不包含右边元素,所以长度为4,但左指针在s[0]处,走过0个元素,所以容量仍然为6;

4,在经历步骤3切片后的基础上,左指针指向s[2],右指针指向最右边,所以长度为2,由于左指针走过两个元素,离最右边还剩4个元素,所以容量为4。


本文内容最难理解的是切片的容量,我们可以把容量当做成总长度减去左指针走过的元素值,比如:
s[:0] ——> cap = 6 - 0 =6;
s[2:] ——> cap = 6 - 2 = 4。

0x02 切片的内幕
一个切片是一个数组片段的描述。它包含了指向数组的指针,片段的长度, 和容量(片段的最大长度)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yci8EQBR-1585976257296)(https://blog.go-zh.org/go-slices-usage-and-internals_slice-struct.png)]
前面使用 make([]byte, 5) 创建的切片变量 s 的结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kR7U0t8j-1585976257297)(https://blog.go-zh.org/go-slices-usage-and-internals_slice-1.png)]
长度是切片引用的元素数目。容量是底层数组的元素数目(从切片指针开始)。 关于长度和容量和区域将在下一个例子说明。
我们继续对 s 进行切片,观察切片的数据结构和它引用的底层数组:

s = s[2:4]
1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tZTU83Aa-1585976257299)(https://blog.go-zh.org/go-slices-usage-and-internals_slice-2.png)]
切片操作并不复制切片指向的元素。它创建一个新的切片并复用原来切片的底层数组。 这使得切片操作和数组索引一样高效。因此,通过一个新切片修改元素会影响到原始切片的对应元素。

d := []byte{'r', 'o', 'a', 'd'}
e := d[2:]
// e == []byte{'a', 'd'}
e[1] = 'm'
// e == []byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'm'}
1
2
3
4
5
6
前面创建的切片 s 长度小于它的容量。我们可以增长切片的长度为它的容量:

s = s[:cap(s)]
1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t0aIOp7h-1585976257306)(https://blog.go-zh.org/go-slices-usage-and-internals_slice-3.png)]
切片增长不能超出其容量。增长超出切片容量将会导致运行时异常,就像切片或数组的索引超 出范围引起异常一样。同样,不能使用小于零的索引去访问切片之前的元素。

0x03 切片append
package main

import "fmt"

func main() {
    s := []int{2, 3, 5, 7, 11, 13}
    printSlice(s)

    // 截取切片使其长度为 0
    s = s[:0]
    printSlice(s)

    // 拓展其长度
    s = s[:5]
    printSlice(s)
    
    // 第一次增加
    s = append(s, 1)
    printSlice(s)
    
    // 第二次增加
    s = append(s, 1)
    printSlice(s)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d ptr=%p %v\n", len(s), cap(s), s, s)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
len=6 cap=6 ptr=0x450000 [2 3 5 7 11 13]
len=0 cap=6 ptr=0x450000 []
len=5 cap=6 ptr=0x450000 [2 3 5 7 11]
len=6 cap=6 ptr=0x450000 [2 3 5 7 11 1]
len=7 cap=12 ptr=0x44e030 [2 3 5 7 11 1 1]
1
2
3
4
5
第一次append的时候,长度没有超过容量,所以容量没有变。
第二次append的时候,长度超过了容量,这时容量会扩展到原来的2倍。
同时,增加后的切片地址和原来不同,也就是说:
append操作可能会导致原本使用同一个底层数组的两个Slice变量变为使用不同的底层数组。

0x04 切片append
package main

import "fmt"

func main() {
    s := []int{2, 3, 5, 7, 11, 13}
    printSlice(s)

    // 截取切片使其长度为 0
    s = s[:0]
    printSlice(s)

    // 拓展其长度
    s = s[:4]
    printSlice(s)
    
    // append后再切片
    s = append(s, 4)
    s = s[:6]
    printSlice(s)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []
len=4 cap=6 [2 3 5 7]
len=6 cap=6 [2 3 5 7 4 13]
1
2
3
4
这里验证一个问题,在[2 3 5 7]基础上append一个4后,在全容量获取切片,发现最后一个13还在,只是11被4替换了。
这说明append的写入是把slice后边数据逐个覆盖掉。

0x05 切片copy
用于将内容从一个数组切片复制到另一个数组切片。如果加入的两个数组切片不一样大,就会按其中较小的那个数组切片的元素个数进行复制。

package main

import "fmt"

func main() {
    slice1 := []int{1, 2, 3, 4, 5}
    slice2 := []int{5, 4, 3}
    slice3 := []int{5, 4, 3}

    copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
    printSlice(slice2)
    
    copy(slice1, slice3) // 只会复制slice3的3个元素到slice1的前3个位置
    printSlice(slice1)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
len=3 cap=3 [1 2 3]
len=5 cap=5 [5 4 3 4 5]
1
2
要增加切片的容量必须创建一个新的、更大容量的切片,然后将原有切片的内容复制到新的切片。 整个技术是一些支持动态数组语言的常见实现。下面的例子将切片 s 容量翻倍,先创建一个2倍 容量的新切片 t ,复制 s 的元素到 t ,然后将 t 赋值给 s :

t := make([]byte, len(s), (cap(s)+1)*2) // +1 in case cap(s) == 0
for i := range s {
        t[i] = s[i]
}
s = t
1
2
3
4
5
循环中复制的操作可以由 copy 内置函数替代。copy 函数将源切片的元素复制到目的切片。 它返回复制元素的数目。

func copy(dst, src []T) int
1
copy 函数支持不同长度的切片之间的复制(它只复制较短切片的长度个元素)。 此外, copy 函数可以正确处理源和目的切片有重叠的情况。

使用 copy 函数,我们可以简化上面的代码片段:

t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t
1
2
3
一个常见的操作是将数据追加到切片的尾部。下面的函数将元素追加到切片尾部, 必要的话会增加切片的容量,最后返回更新的切片:

func AppendByte(slice []byte, data ...byte) []byte {
    m := len(slice)
    n := m + len(data)
    if n > cap(slice) { // if necessary, reallocate
        // allocate double what's needed, for future growth.
        newSlice := make([]byte, (n+1)*2)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:n]
    copy(slice[m:n], data)
    return slice
}
1
2
3
4
5
6
7
8
9
10
11
12
13
下面是 AppendByte 的一种用法:

p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}
1
2
3
类似 AppendByte 的函数比较实用,因为它提供了切片容量增长的完全控制。 根据程序的特点,可能希望分配较小的活较大的块,或则是超过某个大小再分配。

0x06 总结
切片的长度和容量不同,长度表示左指针至右指针之间的距离,容量表示左指针至底层数组末尾的距离。
切片的扩容机制,append的时候,如果长度增加后超过容量,则将容量增加2倍,同时变换了底层数组。
切片append机制,是把slice后边数据逐个覆盖写入。
切片copy机制,按其中较小的那个数组切片的元素个数进行复制。
0x07 参考
Golang中Slice的append详解
Golang数组与Slice,以及append函数的陷阱
Go指南_切片的长度与容量
Go 切片:用法和本质
————————————————
版权声明:本文为CSDN博主「Shower稻草人」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u013474436/article/details/88770501

猜你喜欢

转载自blog.csdn.net/qq_32907195/article/details/114115353