go语言切片用法详解

lioney · · 137 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

图片.png

1. 定义

在Go语言中切片是一种数据结构,很便于使用和管理数据集合。切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数append来实现的。这个函数可以快速且高效地增长切片。还可以通过对切片再次切片来缩小一个切片的大小。因为切片的底层内存也是在连续块中分配的,所以切片还能获得在访问速度以及垃圾回收优化等方面的好处。

切片在Go语言的源码定义如下所示,由于其数据结构中有指向底层数组的指针,所以切片是一种引用类型。

    // src/runtime/slice.go
    type slice struct {  
      array unsafe.Pointer  
      len int  
      cap int  
    }

2. 内部实现

切片是一个很小的对象,对底层数组进行了抽象。切片的数据结构有3个字段,分别是指向底层数组的指针array,切片中元素的个数len(即长度)和切片的最大容量cap。如下图所示。
图片.png

3. 切片的创建

(1) 由数组创建

创建语法为array[b:e], 其中array表示数组名;b表示索引开始位,可以不指定,默认为0;e表示索引结束位,可以不指定,默认是len(array),[b:e]区间是“左闭右开”,即第一个元素是array[b],最后一个元素是array[e-1]。例如:

    // 创建有7个int类型元素的数组  
    var array = [...] int{0,1,2,3,4,5,6}  
    s1 := array[0:4]  
    s2 := array[:4]  
    s3 := array[2:]  
    fmt.Println(s1)  // [0 1 2 3]  
    fmt.Println(s2)  // [0 1 2 3]  
    fmt.Println(s3)  // [2 3 4 5 6]

(2) 通过内置函数make创建切片

切片使用之前需要make是因为make操作要完成切片底层数组的创建及初始化,由make创建的切片个元素被默认初始化位切片元素类型的默认值。例如:

    // len = 10, cap = 10
    a := make([]int, 10)

    // len = 10, cqp = 15
    b := make([]int, 10, 15)

    fmt.Println(a) // 结果为 [0 0 0 0 0 0 0 0 0 0]
    fmt.Println(b) // 结果为 [0 0 0 0 0 0 0 0 0 0]

    // 直接声明切片类型变量是没有意义的(使用前必须make)
    var c []int
    fmt.Println(c) // 结果为 []

(3) 通过切片字面量来创建切片

    // 创建长度和容量都是5个元素的字符串切片
    s1 := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
    
    // 注意和字符串数组区别([]和[...]), 下面是声明数组
    arr := [...]string{"Red", "Blue", "Green", "Yellow", "Pink"}
    
    // 创建长度和容量都是3个元素的整形切片
    s2 := []int{10, 20, 30}
    

(4) 使用索引创建切片(比较少用)

    // 创建字符串切片
    // 使用空字符初始化第100个元素
    s3 := [] string{99: ""}

(5) 创建空切片

    // 使用make创建空的整形切片
    s1 := make([]int, 0)

    // 使用切片字面量创建空的整形切片
    s2 := []int{}

空切片底层数组包含0个元素,也没有分配任何存储空间。不管使用nil切片(没有make的切片)还是空切片,对其调用内置函数的效果都是一样的。

4. 切片支持的操作

  • 内置函数len()返回切片长度
  • 内置函数cap()返回切片底层数组容量
  • 内置函数append()对切片追加元素
  • 内置函数copy()用于复制一个切片

示例如下:

    a := [...]int{0,1,2,3,4,5,6}
    b := make([]int, 2, 4)
    c := a[0:3]
    fmt.Println(len(b)) // 结果为 2
    fmt.Println(cap(b)) // 结果为 4
    // 切片b中添加元素
    b = append(b,1)
    fmt.Println(b)  // 结果为 [0 0 1]
    fmt.Println(len(b)) // 结果为 3
    // 切片b中添加切片
    b = append(b, c...)
    fmt.Println(b)  // 结果为 [0 0 1 0 1 2]
    fmt.Println(len(b)) // 结果为 6
    fmt.Println(cap(b)) // 结果为 8 底层数组发生拓展

    d := make([]int, 2, 2)
    copy(d, c)    // copy只会复制c和d中长度最小的
    fmt.Println(d)   // 结果为 [0 1]
    fmt.Println(len(d))  // 结果为 2
    fmt.Println(cap(d))  // 结果为 2

5. 切片的使用

(1) 赋值

对切片中的某个元素赋值和数组中元素赋值的方法完全一样。用[]操作符就可以改变某个元素的值,代码如下:

    // 创建一个整形切片
    // 其容量和长度都为5个元素
    s := []int{10, 20, 30, 40, 50}

    // 改变索引为1的元素的值
    s[1] = 200

(2) 对切片进行切片

切片之所以称为切片,是因为创建一个切片就是把底层数组切出一部分,代码如下:

    // 创建一个整形切片
    // 其容量和长度都为5个元素
    slice := []int{10, 20, 30, 40, 50}

    // 创建一个新切片
    // 其长度为2个元素,容量为4个元素
    newSlice := slice[1:3]
    fmt.Println(newSlice)   // 结果为 [20 30]

对切片进行切片后,其内存中的分布如下图所示:
图片.png
计算切片的长度和容量:
对底层数组容量是k的切片slice[i:j]来说
切片长度:j - i, 切片容量:k - i
例如,对于图2中的切片newSlice, 长度为3 - 1 = 2, 容量为5 - 1 = 4。

(3) 修改切片内容

    // 创建一个整形切片
    // 其容量和长度都为5个元素
    slice := []int{10, 20, 30, 40, 50}

    // 创建一个新切片
    // 其长度为2个元素,容量为4个元素
    newSlice := slice[1:3]

    // 修改newSlice索引为1的元素,同时也修改了原来的slice索引为2的元素
    newSlice[1] = 32
    fmt.Println(newSlice)   // 结果为 [20 32]
    fmt.Println(slice)   // [10 20 32 40 50]

切片只能访问到其长度内的元素。试图访问超出其长度的元素将会导致语言运行时异常。

(4) 切片增长

相对于数组而言,使用切片的一个好处是可以通过append按需增加切片的容量。
要使用append,需要一个被操作的切片和一个要追加的值,当append调用返回时,会返回一个包含修改结果的新切片。

    // 创建一个整形切片
    // 其容量和长度都为5个元素
    slice := []int{10, 20, 30, 40, 50}

    // 创建一个新切片
    // 其长度为2个元素,容量为4个元素
    newSlice := slice[1:3]
    // append前
    fmt.Println(newSlice)   // 结果为 [20 30]
    fmt.Println(slice)   // [10 20 30 40 50]
    // append后
    newSlice = append(newSlice, 300, 400)
    fmt.Println(newSlice)   // 结果为 [20 30 300 400] (发生变化)
    fmt.Println(slice)   // [10 20 30 300 400] (原来的位置上的元素被覆盖)

我们发现,当newSlice执行append后,slice也发生了变化,append操作前后,底层数组如下图所示。

图片.png
由于newSlice在底层数组里还有额外的容量可用,所以在append的时不会创建新的底层数组,而是继续在原来的底层数组上操作,但由于newSlice和原始的slice共享一个底层数组,所以slice中索引为3和4的元素的值也被改动了
我们再来看另外一种特殊的情况,代码如下。
    // 创建一个整形切片
    // 其容量和长度都为5个元素
    slice := []int{10, 20, 30, 40, 50}

    // 创建一个新切片
    // 其长度为4个元素!!!容量为4个元素
    newSlice := slice[1:5]
    // append前
    fmt.Println(newSlice)   // 结果为 [20 30 40 50]
    fmt.Println(slice)   // [10 20 30 40 50]
    // append后
    newSlice = append(newSlice, 300, 400)
    fmt.Println(newSlice)   // 结果为 [20 30 40 50 300 400] (发生变化)
    fmt.Println(cap(newSlice))  // 结果为8,表明newSlice底层数组的容量拓展为原来的2倍
    fmt.Println(slice)   // [10 20 30 40 50] (没有发生变化!!!)

和上段代码不一样的是,我们在这段代码修改了newSlice的长度,使newSlice的容量被全部占满,当再次进行append操作得时候,newSlice的元素发生了变化,而slice的元素没有没有发生,主要原因是,当append操作时发现当前底层的容量不够用时,会创建一个新的底层数组,将现有数组的值复制到新数组里,此时newSlice和slice不在共享同一个底层数组,因此对newSlice执行append后,slice的元素不会发生变化。, 底层数组的变化如下图所示。

图片.png
而且,append函数会智能地处理底层数组的容量增长。在切片容量小于1000个元素时,总是成倍地增加容量。一旦元素超过1000,扩容因子会设为1.25,也就是每次增加25%的容量。

(5) 切片使用的一个重要细节

如果创建切片时设置切片容量和长度一样,就可以强制让新切片的第一个append操作创建新的底层数组,与原有的底层数组分离。新切片与原有的底层数组分离后,可以安全地进行后续操作。

(6) 在函数间传递切片

在64位架构的机器上,一个切片需要24字节的内存(3个字段)。由于切片关联的数据都包含在底层数组里,不属于切片本身,所以将切片复制到任意函数的时时候,只复制切片本身,不会涉及底层数组。
在函数间传递24字节的数据会非常简单快速。这也是切片效率高的地方。不需要传递指针和复杂的语法,只需要复制切片,按想要的方式修改数据,然后返回一份新的切片副本。示例代码如下。

    package main
    // 函数foo接收一个整形切片,并返回这个切片
    func foo(slice []int) [] int {
        // ...
        return slice
    }

    func main() {
        // 分配100万个整形值的切片
        slice := make([]int, 1e6)

        // 将slice传递到函数foo
        slice = foo(slice)
    }

切片用法的演示就到这里了~~~

我是lioney,年轻的后端攻城狮一枚,爱钻研,爱技术,爱分享。
个人笔记,整理不易,感谢阅读、点赞和收藏。
文章有任何问题欢迎大家指出,也欢迎大家一起交流后端各种问题!

有疑问加站长微信联系

本文来自:Segmentfault

感谢作者:lioney

查看原文:go语言切片用法详解

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:812540095

137 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传