Go语言实战(四) | 数组、切片和映射 -- 切片

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

切片与动态数组

切片是围绕着动态数组的概念构建的,可以按需自动增长或缩小,还可以通过对切片再次切片来缩小一个切片的大小。
切片的底层实现是数组,因此可以索引、迭代以及优化垃圾回收过程。

内部实现

切片是对底层数组进行了抽象,并添加了相关操作方法。
切片是包含三个字段的数据结构:array、len和cap:

  • array是指针类型,指向底层数组
  • len是int64类型,表示切片的长度,即当前允许访问的长度
  • cap是int64类型,表示切片的容量,即切片允许增长的最大值
    切片的内部实现

创建和初始化

切片有三种创建方式:

  • 使用make函数,函数的第一个参数是切片类型,第二个参数是切片长度,第三个参数是切片容量:
    slice := make([]string, 3)slice := make([]string, 3, 5)
  • 使用切片字面量:
    slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
  • 指定某一位置的元素值:slice := []string{99: ""}

nil切片 VS. 空切片
nil切片和空切片的长度和容量都是0,不同的是底层数组指针(array字段):nil切片的array字段是nil,空切片的array字段是一个地址指针,不过指向的是空数组。
nil切片用于描述一个不存在的切片,例如,函数要求返回一个切片但是发生异常的时候。

nil切片的表示

空切片在底层数组包含 0 个元素,也没有分配任何存储空间。想表示空集合时空切片很有用, 例如,数据库查询返回 0 个查询结果时。
空切片的表示

// 创建nil整型切片
var slice []int
// 创建空的整型切片
slice := make([]int, 0)

使用切片

我们可以像操作数组一样访问和修改切片元素。

slice := []int{10, 20, 30, 40, 50}
slice[1] = 100

使用切片创建切片

slice := []int{10, 20, 30, 40, 50}
// newSlice长度为2个元素,容量为4个元素
newSlice := slice[1:3]
slice与newSlice共享同一底层数组

切片之切片长度和容量的计算:
对底层数组容量为k的切片slice[i:j]来说,切片之切片的长度是j-i,即切片初始时可以访问的元素个数;容量是k-i,即与该切片相关联的所有元素的数量。

切片的增长

使用append()函数可以向切片中添加值(使得切片的长度变长,即切片中的len字段增加),返回一个包含修改结果的新切片。
切片的增长分为两种情况:

  1. 增长后的切片长度不大于切片容量
  2. 增长后的切片长度大于切片容量

对于前者,增长前后切片共享同一底层数组:

slice := []int{10, 20, 30, 40}
newSlice := slice[1:3]
newSlice = append(newSlice, 60)
append操作之后的底层数据

而当增长后的切片长度超出容量时,则返回的切片将会指向一个新建的底层数组,并拷贝原数组中的数据:

slice := []int{10, 20, 30, 40}
newSlice := append(slice, 50)

append 操作之后的新的底层数组

所以,内置函数 append 会首先使用可用容量。一旦没有可用容量,会分配一个新的底层数组。这导致很容易忘记切片间正在共享同一个底层数组。一旦发生这种情况,对切片进行修改,很可能会导致随机且奇怪的问题。对切片内容的修改会影响多个切片,却很难找到问 题的原因。
为此,可以在生成切片时将长度和容量设置为相同的值,这样后面进行append()操作时都会新建底层数组,而不是共享原切片的底层数组

slice := []int{10, 20, 30, 40}
newSlice := slice[2:3:3]  // 设置切片长度等于容量
newSlice = append(newSlice, 50)

迭代切片

迭代切片可以使用Golang中的range关键字和传统for循环两种方式。

可以使用range关键字

slice := []int{10, 20, 30, 40}
for index, value := range slice {
    fmt.Println("Index: %d; Value: %d: %d", index, value)
}

range关键字返回一个值时,返回的是Index;返回两个值时,第一个值是Index,第二个值是Value。

需要强调的是:range关键字返回的是每个元素的副本,而不是元素的引用:

slice := []int{10, 20, 30, 40}
for index, value := range slice {
    fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n", value, &value, &slice[index])
}
// Output :
// Value: 10 Value-Addr: C00001A0A0 ElemAddr: C0000160C0
// Value: 20 Value-Addr: C00001A0A0 ElemAddr: C0000160C8
// Value: 30 Value-Addr: C00001A0A0 ElemAddr: C0000160D0
// Value: 40 Value-Addr: C00001A0A0 ElemAddr: C0000160D8

可以看到,在for循环中value的地址和slice中元素的地址并不相同,且value地址在迭代过程中是不变的,即循环过程中是对同一变量的重复赋值。

使用传统for循环

slice := []int{10, 20, 30, 40}
for index := 2; index < len(slice); index++ {
    fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}

有两个特殊的内置函数 len 和 cap,可以用于处理数组、切片和通道。对于切片,函数 len 返回切片的长度,函数 cap 返回切片的容量。

多维切片

和数组一样,切片是一维的。也可以像数组一样,组合多个一维切片实现多维切片。

slice := [][]int{{10}, {100, 200}}
多维切片的底层数据存储

多维切片的增长

增长多维切片的内层切片和增长一维切片的操作相同:

slice := [][]int{{10}, {100, 200}}
slice[0] = append(slice[0], 20)
多维切片的内层切片增长

在函数间传递切片

上一小节中提到在函数间传递数组的时间代价和空间代价都是巨大的,为此提出可以在函数间传递数组指针,但是这么做是极其危险的,可能导致对数组数据的非法修改且错误难以追踪。
其实,最好的方式是使用本节介绍的切片在函数间传递数组。
在 64 位架构的机器上,一个切片需要 24 字节的内存:指针字段需要 8 字节,长度和容量字段分别需要 8 字节。在函数间传递 24 字节的数据会非常快速、简单。这也是切片效率高的地方。
同时,也可以通过设置生成切片的容量来控制在调用函数中可以访问的底层数组范围和增长范围,防止错误的增长数据。


函数调用之后两个切片指向同一个底层数组

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:小黑随笔

查看原文:Go语言实战(四) | 数组、切片和映射 -- 切片

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

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