本文共 3398 字,大约阅读时间需要 11 分钟。
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) } }}
输出
01248163264128256512102412801704256035844608
可以发现当容<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() 操作。这种情况丝毫不影响原数组。转载地址:https://blog.csdn.net/weixin_39172380/article/details/89923853 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!