写点什么

Go 学习笔记之 Slice

发布于: 22 小时前
Go 学习笔记之 Slice

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

一个 slice 是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且 slice 的底层确实引用一个数组对象。

一个 slice 由三个部分构成:指针、长度和容量。

  • 指针指向第一个 slice 元素对应的底层数组元素的地址,要注意的是 slice 的第一个元素并不一定就是数组的第一个元素。

  • 长度对应 slice 中元素的数目;

  • 长度不能超过容量,容量一般是从 slice 的开始位置到底层数据的结尾位置。内置的 len 和 cap 函数分别返回 slice 的长度和容量。


表示一年中每个月份名字的字符串数组,还有重叠引用了该数组的两个 slice。数组这样定义:

months := [...]string{1: "January", /* ... */, 12: "December"}
复制代码

因此一月份是 months[1],十二月份是 months[12]。

通常,数组的第一个元素从索引 0 开始,但是月份一般是从 1 开始的,因此我们声明数组时直接跳过第 0 个元素,第 0 个元素会被自动初始化为空字符串。


slice 的切片操作 s[i:j],其中 0 ≤ i≤ j≤ cap(s),用于创建一个新的 slice,引用 s 的从第 i 个元素开始到第 j-1 个元素的子序列。新的 slice 将只有 j-i 个元素。如果 i 位置的索引被省略的话将使用 0 代替,如果 j 位置的索引被省略的话将使用 len(s)代替。因此,months[1:13]切片操作将引用全部有效的月份,和 months[1:]操作等价;months[:]切片操作则是引用整个数组。让我们分别定义表示第二季度和北方夏天月份的 slice,它们有重叠部分:



Q2 := months[4:7]summer := months[6:9]fmt.Println(Q2)     // ["April" "May" "June"]fmt.Println(summer) // ["June" "July" "August"]
复制代码

两个 slice 都包含了六月份。

append 函数

append 函数用于向 slice 追加元素:

var runes []runefor _, r := range "Hello, 世界" {    runes = append(runes, r)}fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"
复制代码


为了提高内存使用效率,新分配的数组一般略大于保存 x 和 y 所需要的最低大小。通过在每次扩展数组时直接将长度翻倍从而避免了多次内存分配,也确保了添加单个元素操的平均时间是一个常数时间。这个程序演示了效果:

func main() {    var x, y []int    for i := 0; i < 10; i++ {        y = appendInt(x, i)        fmt.Printf("%d cap=%d\t%v\n", i, cap(y), y)        x = y    }}
//每一次容量的变化都会导致重新分配内存和copy操作:0 cap=1 [0]1 cap=2 [0 1]2 cap=4 [0 1 2]3 cap=4 [0 1 2 3]4 cap=8 [0 1 2 3 4]5 cap=8 [0 1 2 3 4 5]6 cap=8 [0 1 2 3 4 5 6]7 cap=8 [0 1 2 3 4 5 6 7]8 cap=16 [0 1 2 3 4 5 6 7 8]9 cap=16 [0 1 2 3 4 5 6 7 8 9]
复制代码

让我们仔细查看 i=3 次的迭代。当时 x 包含了[0 1 2]三个元素,但是容量是 4,因此可以简单将新的元素添加到末尾,不需要新的内存分配。然后新的 y 的长度和容量都是 4,并且和 x 引用着相同的底层数组,如图 4.2 所示。

在下一次迭代时 i=4,现在没有新的空余的空间了,因此 appendInt 函数分配一个容量为 8 的底层数组,将 x 的 4 个元素[0 1 2 3]复制到新空间的开头,然后添加新的元素 i,新元素的值是 4。新的 y 的长度是 5,容量是 8;后面有 3 个空闲的位置,三次迭代都不需要分配新的空间。当前迭代中,y 和 x 是对应不同底层数组的 view。这次操作如图 4.3 所示。



内置的 append 函数可能使用比 appendInt 更复杂的内存扩展策略。

因此,通常我们并不知道 append 调用是否导致了内存的重新分配,因此我们也不能确认新的 slice 和原始的 slice 是否引用的是相同的底层数组空间。

同样,我们不能确认在原先的 slice 上的操作是否会影响到新的 slice。


作者:架构精进之路,十年研发风雨路,大厂架构师,CSDN 博客专家,专注架构技术沉淀学习及分享,职业与认知升级,坚持分享接地气儿的干货文章,期待与你一起成长

关注并私信我回复“01”,送你一份程序员成长进阶大礼包,欢迎勾搭。



Thanks for reading!

发布于: 22 小时前阅读数: 7
用户头像

坚持分享接地气儿的架构技术文章! 2018.02.26 加入

同名微信公众号「架构精进之路」,专注软件架构研究,技术学习与职业成长!坚持原创总结、沉淀和分享,希望能带给大家一些引导和启发,感谢各位的支持(关注、点赞、分享)!

评论

发布
暂无评论
Go 学习笔记之 Slice