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)
}
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]

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]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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'}

前面创建的切片 s 长度小于它的容量。我们可以增长切片的长度为它的容量:

s = s[:cap(s)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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)
}

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]

第一次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)
}
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]

这里验证一个问题,在[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)
}
len=3 cap=3 [1 2 3]
len=5 cap=5 [5 4 3 4 5]

要增加切片的容量必须创建一个新的、更大容量的切片,然后将原有切片的内容复制到新的切片。 整个技术是一些支持动态数组语言的常见实现。下面的例子将切片 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

循环中复制的操作可以由 copy 内置函数替代。copy 函数将源切片的元素复制到目的切片。 它返回复制元素的数目。

func copy(dst, src []T) int

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

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

t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t

一个常见的操作是将数据追加到切片的尾部。下面的函数将元素追加到切片尾部, 必要的话会增加切片的容量,最后返回更新的切片:

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
}

下面是 AppendByte 的一种用法:

p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}

类似 AppendByte 的函数比较实用,因为它提供了切片容量增长的完全控制。 根据程序的特点,可能希望分配较小的活较大的块,或则是超过某个大小再分配。

0x06 总结

  • 切片的长度和容量不同,长度表示左指针至右指针之间的距离,容量表示左指针至底层数组末尾的距离。
  • 切片的扩容机制,append的时候,如果长度增加后超过容量,则将容量增加2倍,同时变换了底层数组。
  • 切片append机制,是把slice后边数据逐个覆盖写入。
  • 切片copy机制,按其中较小的那个数组切片的元素个数进行复制。

0x07 参考

Golang中Slice的append详解
Golang数组与Slice,以及append函数的陷阱
Go指南_切片的长度与容量
Go 切片:用法和本质

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值