go 复杂类型之 Slice

Slice中文翻译为切片,容易和切片表达式的切片弄混,为了便于理解,这里统一使用Slice来表述。


go语言中的Slice和其他语言中的Slice有点不一样,go中的Slice是一个拥有相同类型元素的可变长度的序列。 我们先来看一下Slice的初始化。

声明和初始化

Slice的声明语法如下:

var name []Type
  • nameSlice的名称
  • TypeSlice的元素类型
package main

import "fmt"

func def_slice() {
    // 声明一个字符串类型 Slice,未初始化的 slice == nil
    var a []string

    // 声明一个整型 Slice 并初始化
    var b = []int{}

    // 声明一个布尔 Slice 并初始化
    var c = []bool{false, true}

    // 声明一个布尔 Slice 并初始化
    //var d = []bool{false, true}

    //使用make函数声明 Slice 并初始化
    e := make([]int, 3)

    fmt.Printf("a: %v\n", a)
    fmt.Printf("b: %v\n", b)
    fmt.Printf("c: %v\n", c)
    fmt.Printf("e: %v\n", e)

    fmt.Printf("a == nil ? %v\n", a == nil)
    fmt.Printf("b == nil ? %v\n", b == nil)
    fmt.Printf("c == nil ? %v\n", c == nil)
    fmt.Printf("e == nil ? %v\n", e == nil)

    // slice can only be compared to nil
    //fmt.Println(c == d)
}

func main() {
    def_slice()
}


// 输出
a: []
b: []
c: [false true]
e: [0 0 0]
a == niltrue
b == nilfalse
c == nilfalse
e == nilfalse

和数组一样,初始化的时候可以同时给Slice赋值,如果没有赋值,则为对应元素类型的默认零值。看完Slice的初始化,是不是感觉Slice和数组很像,区别也就是在声明初始化的时候不用指定元素长度。实际上Slice一开始就是为了解决数组个数固定,和传参值拷贝开销太大的问题而设计出来的,所以,你基本上可以把Slice看做为数组的 Plus 版本。

Slice在设计上实际是一个复合类型,运行时表现为一个struct,其内部结构如下:

type slice struct { 
    array unsafe.Pointer 
    len int 
    cap int 
}
  • array指向底层数组中第一个可访问元素的指针。
  • lenSlice的长度,即切片中的元素数目,可以使用内置的len()函数求长度。
  • capSlice的容量,即切片分配的底层内存空间可容纳的最大元素数目,可使用内置的cap()函数求切片的容量。

上面的图示为一个Slice的初始化过程,可以看到,go运行时为Slice创建了一个底层数组用来容纳Slice的值。Slice可以理解成一个对底层数组的包装,操作Slice,实际上就是对底层的数组进行操作。

Slice的长度很好理解,Slice有多少元素,Slice的长度就是多少,那为什么Slice的容量是干嘛的呢?这就涉及到Slice的自动扩容了。

自动扩容

go提供了内置函数 append(slice, element...) 可以为切片动态添加元素,append() 函数会返回一个新的切片,将其存储在变量中。

上图为使用append函数给 s1 添加一个元素的过程。可以看到,扩容后的Sclice s1 其底层数组指向了一个新的数组 arr2,len长度变为了 6,cap变为 10。

原因前面已经解释过了,Slice 底层数据是存储在数组中的,而数组的长度是不可变,之前的数组长度不够了,就要创建一个更大的数组来容纳Slice的元素。现在你理解这个cap的什么意思了吧。

但现在你可能又会问了,当底层数组的空间不够用,go 运行时会创建一个更大的数组,那到底多大呢?我们写个代码来测试一下:

package main

import "fmt"

func sliceAppend() {

    var numbers []int
    for i := 0; i < 10; i++ {
        // 使用append函数向numbers切片中追加元素
        // 注意append不支持数组,数组是不可变长度。
        numbers = append(numbers, i)

        // 打印结果,注意观察切片的容量cap的变化
        fmt.Printf("i=%d\tlen=%d\tcap=%d\t%v\n", i, len(numbers), cap(numbers), numbers)
    }
}

func main() {
    sliceAppend()
}


// 输出
i=0     len=1   cap=1   [0]
i=1     len=2   cap=2   [0 1]
i=2     len=3   cap=4   [0 1 2]
i=3     len=4   cap=4   [0 1 2 3]
i=4     len=5   cap=8   [0 1 2 3 4]
i=5     len=6   cap=8   [0 1 2 3 4 5]
i=6     len=7   cap=8   [0 1 2 3 4 5 6]
i=7     len=8   cap=8   [0 1 2 3 4 5 6 7]
i=8     len=9   cap=16  [0 1 2 3 4 5 6 7 8]
i=9     len=10  cap=16  [0 1 2 3 4 5 6 7 8 9]

在上面的代码中,声明了一个名为 numbersSlice,然后使用for循环,调用append()函数,向Slice中追加0-9的数字,仔细观察cap的值可以发现, 当Slice容量不足以容纳更多元素时,Slice的容量将翻倍, 即新的底层数组的长度翻倍。

那结果真的这么简单么?如果我们把上面代码中for循环的退出条件改为1000,你会发现,当切片容量为512时,下一次扩容时cap的值是848, 并不是我们所期待的1024

建议拿上面的代码去www.goplay.tools/,实际运行一下,印象会更深刻。

实际上,当切片的容量超过一定数值以后,扩容将不再翻倍,而是变成一点几倍,具体的数字由运行时决定的,这样的目的是为了减少内存的浪费发生。

切片表达式

看到这里,相信你对Slice已经有了一个基本的了解,接下来我们聊一聊切片表达式。和大多数语言一样,go也支持切片运算符,格式如下:

s[low:high:max]
  • s:数组或切片

  • low:数组或切片的第一个元素的下标。

  • high:数组或切片中的最后一个元素的位置。

  • max:底层数组或引用切片的最后一个位置,另外 max 可以省略,这时新切片的cap默认等于底层数组的长度。

    切片表达式中的lowhigh表示新切片的一个索引引用范围 (左包含,右不包含)。而新切片的长度len = high - low,容量cap = max - low

上图是一个使用切片表达式从Slice s1 创建Slice s2 的过程。可以清楚的看到,s1 和 s2 共用同一个底层数组 arr,此时,修改任何一个Slice的值都会影响到另一个,因为实际修改都是作用在底层数组上的。

package main

import (
    "fmt"
)

func main() {
    s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    fmt.Printf("s1:%v\n", s1)
    fmt.Printf("len(s1):%v\n", len(s1))
    fmt.Printf("cap(s1):%v\n\n", cap(s1))

    s2 := s1[1:6:8]
    fmt.Printf("s2:%v\n", s2)
    fmt.Printf("len(s2):%v\n", len(s2))
    fmt.Printf("cap(s2):%v\n\n", cap(s2))

    s2[0] = 100
    fmt.Printf("s1:%v\n", s1)
    fmt.Printf("s2:%v\n", s2)
}

// 输出
s1:[1 2 3 4 5 6 7 8 9 10]
len(s1):10
cap(s1):10

s2:[2 3 4 5 6]
len(s2):5
cap(s2):7

s1:[1 100 3 4 5 6 7 8 9 10]
s2:[100 3 4 5 6]

这是一个非常容易踩坑的地方,一定要多多注意。

另外值得一提的是,当你的Slice是基于切片表达式生成的,对它使用append操作,如果底层数组空间足够的话,同样会影响到原Slice

package main

import (
    "fmt"
)

func main() {
    s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    s2 := s1[1:6:8]

    s2 = append(s2, 200)
    fmt.Printf("s1:%v\n", s1)
    fmt.Printf("s2:%v\n", s2)
}

// 输出
s1:[1 2 3 4 5 6 200 8 9 10]
s2:[2 3 4 5 6 200]

而当你添加的元素足够多,超出底层数组的容量范围了,根据我们前面讲到的,go 运行时会为其创建一个新的底层数组,这时候这个切片表达式生成的Slice就和原来的底层数组“解绑”了,后续两个 Slice 就没有任何关系了。

package main

import (
    "fmt"
)

func main() {
    s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    s2 := s1[1:6:8]

    s2 = append(s2, 200, 300, 400, 500)
    fmt.Printf("s1:%v\n", s1)
    fmt.Printf("s2:%v\n", s2)
}

// 输出
s1:[1 2 3 4 5 6 7 8 9 10]
s2:[2 3 4 5 6 200 300 400 500]

Slice 的赋值拷贝

 直接将一个切片赋值给另一个数组,前后两个切片共享底层数组,对一个切片的修改会影响另一个切片的内容。

在了解完前面的内容后,上面这句话对你来说应该是轻松理解了。

package main

import "fmt"

func main() {
    s1 := make([]int, 3)
    fmt.Println(s1)
    s2 := s1
    fmt.Println(s2)
    s2[0] = 100
    fmt.Println(s1)
    fmt.Println(s2)
}

// 输出
[0 0 0]
[0 0 0]  
[100 0 0]
[100 0 0]

因此,Go 提供了内置函数copy(),可以迅速地将一个Slice的数据复制到另外一个Slice空间中。copy()函数的使用格式如下:

copy(dst, src []Type)
  • dst:目标Slice
  • src:数据来源Slice
package main

import "fmt"

func slice_copy() {
    a := []int{1, 2, 3, 4, 5}
    b := make([]int, 5, 5)
    c := make([]int, 3, 3)
    d := make([]int, 8, 8)
    copy(b, a)
    copy(c, a)
    copy(d, a)
    fmt.Printf("Slice a:%v\n", a)
    fmt.Printf("Slice b:%v\n", b)
    fmt.Printf("Slice c:%v\n", c)
    fmt.Printf("Slice d:%v\n", d)
    fmt.Println()
    c[0] = 1000
    fmt.Printf("Slice a:%v\n", a)
    fmt.Printf("Slice b:%v\n", b)
    fmt.Printf("Slice c:%v\n", c)
    fmt.Printf("Slice d:%v\n", d)
}

func main() {
    slice_copy()
}

// 输出
Slice a:[1 2 3 4 5]
Slice b:[1 2 3 4 5]
Slice c:[1 2 3]
Slice d:[1 2 3 4 5 0 0 0]

Slice a:[1 2 3 4 5]
Slice b:[1 2 3 4 5]
Slice c:[1000 2 3]
Slice d:[1 2 3 4 5 0 0 0]

copy()函数的目标Slice需要提前声明初始化,并且一般最好和原Slice一样大小,否则就会出现上面代码所示Slice 被截断的情况。copy()函数产生的Slicer和原Slice完全不相干,改变其中一个,并不会影响另一个。

删除 Slice 中的元素

最后,Go 没有内置函数用于从Slice中删除元素。想要删除元素,可以使用append函数配合切片运算符 s[i:p] 来新建一个仅包含所需元素的Slice来实现。

package main

import "fmt"

func main() {
    a := []int{1, 2, 3, 4, 5}
    // 删除索引为2的元素
    a = append(a[:2], a[3:]...)
    fmt.Println(a)
}

// 输出
[1 2 4 5]
go
本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 3

我把他理解成动态数组

2个月前 评论

另外值得一提的是,当你的 Slice 是基于切片表达式生成的,对它使用 append 操作,如果底层数组空间足够的话,同样会影响到原 Slice。
而当你添加的元素足够多,超出底层数组的容量范围了,根据我们前面讲到的,go 运行时会为其创建一个新的底层数组,这时候这个切片表达式生成的 Slice 就和原来的底层数组 “解绑” 了,后续两个 Slice 就没有任何关系了。

这就很离谱了,关联和解绑 容易触坑,要注意

2个月前 评论
wuvikr (楼主) 2个月前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!