深入理解GO语言slice类型的容量管理

Slice

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。

Slice的特征

首先让我们定义一个slice

	var s []int

这里我们定义slice的元素类型为int,其实slice的元素可以是任何类型[]T,
其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

在刚开始接触Go的时候并没有太多关注过slice如何管理它的容量(capability),只知道我可以向一个slice任意增减元素。

首先我们来看一个空切片的长度和容量

func main() {
	var s []int
	detail(s)
}

//打印slice长度和容量的函数
func detail(s []int) {
	fmt.Printf("length: %d	cap: %d \n", len(s), cap(s))
}

输出

length: 0	cap: 0 

就是说,一个初始切片的长度和容量都是0。那么疑问就来了:既然容量为0那我能向slice中成功添加内容吗?
我们来试试:

func main() {
	var s []int
	s = append(s, 1)
	detail(s)
}

输出

length: 1	cap: 1

可以看到slice自动地为我们扩容了。为了观察slice如何管理容量,接下来我们试着向slice中连续添加10个元素,并时刻关注它的长度和容量之间的关系。

func main() {
	var s []int
	for i := 0; i < 10; i++ {
		s = append(s, 1)
		detail(s)
	}
}

输出

length: 1	cap: 1 
length: 2	cap: 2 
length: 3	cap: 4 
length: 4	cap: 4 
length: 5	cap: 8 
length: 6	cap: 8 
length: 7	cap: 8 
length: 8	cap: 8 
length: 9	cap: 16 
length: 10	cap: 16 

这次我们似乎发现了slice管理容量的规律:当元素超过自身容量时,将容量翻倍

为了一探究竟:我们来稍微改造一下我们的函数:
我们定义一个值sCap用来观察切片s的容量,s的容量随着元素个数变化。当s的容量变化时,将它打印出来:

func main() {
	var s []int
	var sCap int
	for i := 0; i < 500; i++ {
		s = append(s, 1)
		if cap(s) != sCap {
			fmt.Println(sCap)
			sCap = cap(s)
		}
	}
}

输出

0
1
2
4
8
16
32
64
128
256
512
1024
1280
1704
2560
3584
4608


可以发现当容<1024时,以2倍递增。容量超过1024时,容量变为原来的1.25倍,当原切片的长度(以下简称原长度)大于或等于1024时,Go 语言将会以原容量的 1.25倍作为新容量的基准(以下新容量基准)。新容量基准会被调整(不断地与1.25相乘),直到结果不小于原长度与要追加的元素数量之和(以下简称新长度)。最终,新容量往往会比新长度大一些,当然,相等也是可能的

另外,如果我们一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容 量就会以新长度为基准。注意,与前面那种情况一样,最终的新容量在很多时候都要比新容 量基准更大一些。更多细节可参见runtime包中 slice.go 文件里的growslice及相关函数 的具体实现。

append 的实现只是简单的在内存中将旧 slice 复制给新 slice

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}
func growslice(et *_type, old slice, cap int) slice {

	...
	
	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
			}
		}
	}
	
	...
}

更新

修改函数,观察在slice容量变化时内存地址的变化情况

func main() {
	var s []int
	var sCap int
	for i := 0; i < 100; i++ {
		fmt.Printf("addr: %p\t cap: %v\t len: %v \n", s, sCap, len(s))
		s = append(s, 1)
		sCap = cap(s)
	}
}

输出

addr: 0x0	 cap: 0	 len: 0 
addr: 0xc000016090	 cap: 1	 len: 1 
addr: 0xc000098000	 cap: 2	 len: 2 
addr: 0xc0000180a0	 cap: 4	 len: 3 
addr: 0xc0000180a0	 cap: 4	 len: 4 
addr: 0xc00001c080	 cap: 8	 len: 5 
addr: 0xc00001c080	 cap: 8	 len: 6 
addr: 0xc00001c080	 cap: 8	 len: 7 
addr: 0xc00001c080	 cap: 8	 len: 8 
addr: 0xc00009c000	 cap: 16	 len: 9 
addr: 0xc00009c000	 cap: 16	 len: 10 
addr: 0xc00009c000	 cap: 16	 len: 11 
addr: 0xc00009c000	 cap: 16	 len: 12 
addr: 0xc00009c000	 cap: 16	 len: 13 
addr: 0xc00009c000	 cap: 16	 len: 14 
addr: 0xc00009c000	 cap: 16	 len: 15 
addr: 0xc00009c000	 cap: 16	 len: 16 
addr: 0xc00009e000	 cap: 32	 len: 17 
...

当容量变化时,地址也发生了变化,它不再是原来的旧切片。
之所以生成了新的切片,是因为原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况丝毫不影响原数组。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值