slice 深入理解

数组是一种长度不可改变的集合,go数组不仅如此,还是一个值变量,也就是在赋值和函数调用采用值传递,会产生数组的全量拷贝,需要耗费内存和时间,基于上述原因,多数情况下会优先选择使用slice。关于slice使用在细节上还有许多需要仔细研究的地方。

make

make 是slice 重要创建方式。
make([]T, length, capacity)
可以简写为
slice1 := make([]T, length)

length 表示slice中已经使用的数据长度

capacity 表示slice中指针执行的数组容量,如果缺省capacity 和length大小相等。

上代码:

package main
import "fmt"
func main() {
    s := make([]int, 0)
    fmt.Printf("s =%v, len= %d, cap = %d\n", s, len(s), cap(s))
}

运行输出:
s =[], len= 0, cap = 0

从运行结果可以看出,slice是数组的包装,它含:

  • 指向数组的指针

  • slice 长度(slice当前数据个数)

  • slice容量(指向数组长度)

组合到一起成为slice,构成对数组引用。slice 具有数组所有特性,可以索引,遍历。

make函数用于初始化slice。

make([]T, length, capacity)

程序中,我们length 传值为0, capacity 缺省(缺省时cap = length),所以length= 0, capacity 也为零,slice 指向数组的指针为nil。我们一起看一下程序输出:

s =[], len= 0, cap = 0 就是length 传0值的结果。实际构造一个空slice。

capacity 传值大于零(注意:值必须大于等于 length),make 会分配一个长度为capcity 的数组。slice 指针等于数组首元素地址。数组中元素初始化为类型T的零值。

append

我们研究一下,append 到哪里?

上程序

package main

import "fmt"

func main() {
    s := make([]int, 0, 10)
    fmt.Printf("s=%v, len=%d, cap=%d\n", s, len(s), cap(s))
    s1 := append(s, 1)

    fmt.Printf("s1=%v, len=%d, cap=%d\n", s1, len(s1), cap(s1))
}

s1 := append(s, 1) 为甚要这样写呢? 目的是让大家注意apend 函数将数据apend到哪里?我们看一下运行结果

s=[], len=0, cap=10
s1=[1], len=1, cap=10

从运行结果看:

apend 函数生成新的slice,不改变原来的slice。也就是apend 增加到新的slice。这样子不是很容易说清楚,我们例子进一步说明问题:

package main

import "fmt"

func main() {
    a := [...]int{1, 2, 3, 4, 5, 5}
    s := a[2:4]
    s1 := append(s, 9)
    fmt.Printf("s=%v, len=%d cap =%d\n", s, len(s), cap(s))
    fmt.Printf("s1=%v, len=%d, cap=%d\n", s1, len(s1), cap(s1))
    fmt.Printf("a=%v, len=%d\n", a, len(a))
}
s=[3 4], len=2 cap =4
s1=[3 4 9], len=3, cap=4
a=[1 2 3 4 9 5], len=6

本例子增加一个数组,主要为了让大家看到append 到哪里了,实际上是append 到切片s指向的底层数组a。千语万语抵不上一张图更易于理解。


图1:append前示意图

如图1所示,s := a[2:4] ,创建一个slice,该slice *data=&a[2], len = 2, cap =len(a) -2,构造后,s 数组指针指向数组a[2]的地址。

图2:appned后示意图

如图2所示,执行s1 := append(s, 9),

判断append之后元素个数没有超过slice容量,本例就是没有超过4,就是 不需要扩容。

新建的slice仍然指向数组a,本例指向a[2]的地址,如图中黄色箭头。append之后将slice索引2的位置更改为9,也就是图中青色位置。

程序输出,大家看到a[5]的位置变成9, 这个是两个slice(s和s2)索引2的位置,也就是第三个元素位置。

分析完append 到哪里。我们继续看append的实现。

append实现分析

append 实现3个功能:

  1. 构造新的slice

  2. 扩容

  3. “增加”元素

1、2 、3都好理解,应该是slice 必备功能。唯一的疑问是“增加”为何要加引号。

表象上slice确实增加了元素,因为length 增加了。

因为本质append 是更改slice指向数组的对应索引位置的值。也就是单纯的数组索引访问。包装成append后避免了数组访问越界。

apend 实现分析完,剩下的问题是数组共享。虽然大部分不会影响我们的程序,但有时共享确实是错误的根源,在并发程序中尤其突出。共享很多时候会节约内存,有时情况却相反,比如一个大数组,做了一个小切片使用,如果仅有这个小切片引用数组,导致这个大数组不能释放。那么反而浪费内存。这些时候我们如何不共享呢。

这个时候最简单的办法是切片时指定容量为切片长度

我们是因为在原有数组切片或者在切片上切片引起的共享,如果切片时引入第三个参数, s:= a[1:2:1], 指定切片后容量,如果我们指定切片容量和我们切片长度一样。在append到s 的时候,会引起扩容,分配新数组给slice s。这时调用append函数就append 到新数组。

package main

import "fmt"

func main() {
    a := []int{2, 2}
    b := a[0:1:1]
    b = append(b, 3)
    fmt.Println(b, a)

    b1 := a[0:1]
    b1 = append(b1, 3)
    fmt.Println(b1, a)
}

运行结果

[2 3] [2 2]
[2 3] [2 3]

结果第一行对应 6、7、8、9 行代码, append 后没有更改原来的slice ,说明没有共享。

结果第二行 对应11、12 行代码,append后更改原来的slice,就是共享引起的问题。希望写程序引起注意。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容