写点什么

Go 语言实战之切片的内部实现和基础功能

作者:山河已无恙
  • 2022 年 3 月 08 日
  • 本文字数:5679 字

    阅读完需:约 19 分钟

写在前面


  • 嗯,学习GO,所以有了这篇文章

  • 博文内容为《GO语言实战》读书笔记之一

  • 主要涉及切片相关知识


没事,只不过是恢复原状罢了,我本来就是一无所有的。 ——濑川初原《食灵零》


切片的内部实现和基础功能

切片是一种数据结构(类似于JavaArrayList),围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数append来实现的。这个函数可以快速且高效地增长切片。还可以通过对切片再次切片来缩小一个切片的大小。

因为切片的底层内存也是在连续块中分配的,所以切片还能获得索引迭代以及为垃圾回收优化的好处。

内部实现

切片是一个很小的对象,对底层数组进行了抽象,并提供相关的操作方法。切片有3个字段数据结构,这些数据结构包含Go语言需要操作底层数组的元数据

  • 指向底层数组指针

  • 切片访问的元素的个数(即长度)

  • 切片允许增长到的元素个数(即容量)

在这里插入图片描述

创建和初始化

Go语言中有几种方法可以创建和初始化切片。是否能提前知道切片需要的容量通常会决定要如何创建切片

make 和切片字面量

  • 如果只指定长度,那么切片的容量和长度相等

 // 其长度和容量都是 5 个元素 slice := make([]string, 5)
复制代码
  • 使用长度和容量声明整型切片

func main() { // 其长度和容量都是 5 个元素 slice := make([]int, 3, 5) fmt.Println(slice)}============[Running] go run "d:\GolandProjects\code-master\demo\make.go"[0 0 0]
复制代码

剩余的2 个元素可以在后期操作中合并到切片,如果基于这个切片创建新的切片,新切片会和原有切片共享底层数组,也能通过后期操作来访问多余容量的元素。

  • 不允许创建容量小于长度的切片,

func main() { // 其长度和容量都是 5 个元素 slice := make([]int, 5, 3) fmt.Println(slice)}=================[Running] go run "d:\GolandProjects\code-master\demo\make.go"# command-line-argumentsd:\GolandProjects\code-master\demo\make.go:10:15: len larger than cap in make([]int)
复制代码

另一种常用的创建切片的方法是使用切片字面量,只是不需要指定[]运算符里的值。初始的长度和容量会基于初始化时提供的元素的个数确定.

  • 通过切片字面量来声明切片

slice:= [] string{"Red", "Blue", "Green", "Yellow", "Pink"}//其长度和容量都是 3 个元素slice := []int{10, 20, 30}
复制代码

当使用切片字面量时,可以设置初始长度和容量,创建长度和容量都是100 个元素的切片

  • 使用索引声明切片

// 使用空字符串初始化第 100 个元素slice := []string{99: ""}
复制代码
  • 声明数组和声明切片的不同

// 创建有 3 个元素的整型数组array := [3]int{10, 20, 30}// 创建长度和容量都是 3 的整型切片slice := []int{10, 20, 30}
复制代码

nil 和空切片

  • 创建nil切片:描述一个不存在的切片时

// 创建 nil 整型切片var slice []int
复制代码


在这里插入图片描述

  • 声明空切片:表示空集合时空切片很有用

// 使用 make 创建空的整型切片slice := make([]int, 0)// 使用切片字面量创建空的整型切片slice := []int{}
复制代码


不管是使用 nil 切片还是空切片,对其调用内置函数 append、len 和 cap 的效果都是一样的。

使用切片

赋值和切片

对切片里某个索引指向的元素赋值和对数组里某个索引指向的元素赋值的方法完全一样。使用[]操作符就可以改变某个元素的值

  • 使用切片字面量来声明切片

// 其容量和长度都是 5 个元素slice := []int{10, 20, 30, 40, 50}// 改变索引为 1 的元素的值slice[1] = 25
复制代码

切片之所以被称为切片,是因为创建一个新的切片就是把底层数组切出一部分

  • 使用切片创建切片

// 其长度和容量都是 5 个元素slice := []int{10, 20, 30, 40, 50}

复制代码
  • 使用切片创建切片,如何计算长度和容量

// 其长度和容量都是 5 个元素slice := []int{10, 20, 30, 40, 50}// 创建一个新切片// 其长度为 2 个元素,容量为 4 个元素newSlice := slice[1:3]
复制代码

对底层数组容量是k的切片 slice[i:j]来说

  • 长度: j - i = 2

  • 容量: k - i = 4

这里书里讲的个人感觉不太好理解,其实类似JavaString的subString,换句话讲,前开后闭(即前包后不包),切取原数组索引 1 到 3 的元素,这里的元素个数即为新的切片长度,切取的容量为原数组第一个切点到数组末尾(默认)。其实这里有第三个索引值,后面我们会讲.

在这里插入图片描述

我们有了两个切片,它们共享同一段底层数组,但通过不同的切片会看到底层数组的不同部分,这个和java里的List方法subList特别像,都是通控制索引来对底层数组进行切片,所以本质上,切片后的数组可以看做是原数组视图

  • 修改切片内容可能导致的结果

// 其长度和容量都是 5 个元素slice := []int{10, 20, 30, 40, 50}// 其长度是 2 个元素,容量是 4 个元素newSlice := slice[1:3]// 修改 newSlice 索引为 1 的元素// 同时也修改了原来的 slice 的索引为 2 的元素newSlice[1] = 35

复制代码
  • 表示索引越界的语言运行时错误

// 其长度和容量都是 5 个元素slice := []int{10, 20, 30, 40, 50}// 其长度为 2 个元素,容量为 4 个元素newSlice := slice[1:3]// 修改 newSlice 索引为 3 的元素// 这个元素对于 newSlice 来说并不存在newSlice[3] = 45
复制代码

切片增长相对于数组而言,使用切片的一个好处是,可以按需增加切片的容量。Go语言内置的 append函数会处理增加长度时的所有操作细节。

函数append总是会增加新切片的长度,而容量有可能会改变,也可能不会改变,这取决于被操作的切片的可用容量

  • 使用append向切片增加元素

package main
import ( "fmt")
func main() {// 其长度和容量都是 5 个元素slice := []int{10, 20, 30, 40, 50}// 创建一个新切片// 其长度为 2 个元素,容量为 4 个元素newSlice := slice[1:3]fmt.Println(newSlice)// 使用原有的容量来分配一个新元素// 将新元素赋值为 60newSlice = append(newSlice, 60)fmt.Println(newSlice)}
复制代码


[Running] go run "d:\GolandProjects\code-master\demo\hello.go"[20 30][20 30 60]
[Done] exited with code=0 in 1.28 seconds
复制代码


在这里插入图片描述

如果切片的底层数组没有足够的可用容量,append函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值.

package main
import ( "fmt")
func main() {// 其长度和容量都是 5 个元素slice := []int{10, 20, 30, 40, 50}// 使用原有的容量来分配一个新元素// 将新元素赋值为 60newSlice := append(slice, 60)fmt.Println(newSlice)}
复制代码


[Running] go run "d:\GolandProjects\code-master\demo\hello.go"[10 20 30 40 50 60]
[Done] exited with code=0 in 1.236 seconds

复制代码

函数append会智能地处理底层数组的容量增长。在切片的容量小于1000个元素时,总是会成倍地增加容量。一旦元素个数超过 1000,容量的增长因子会设为1.25,也就是会每次增加 25%的容量。随着语言的演化,这种增长算法可能会有所改变。

创建切片时的 3 个索引

通过第三个索引值设置容量,如果没有第三个索引值,默认容量是到数组最后一个。

package main
import ( "fmt")
func main() { // 创建字符串切片 // 其长度和容量都是 5 个元素 source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"} // 将第三个元素切片,并限制容量 // 其长度为 1 个元素,容量为 2 个元素 slice := source[2:3:4] fmt.Println(slice)}
复制代码

为了设置容量,从索引位置 2 开始,加上希望容量中包含的元素的个数(2),就得到了第三个值 4。

[Running] go run "d:\GolandProjects\code-master\demo\hello.go"[Plum]
[Done] exited with code=0 in 0.998 seconds
复制代码
  • 设置容量大于已有容量的语言运行时错误

[Running] go run "d:\GolandProjects\code-master\demo\hello.go"panic: runtime error: slice bounds out of range [::9] with capacity 5
复制代码

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

  • 设置长度和容量一样的好处

package main
import ( "fmt")
func main() { // 创建字符串切片 // 其长度和容量都是 5 个元素 source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"} // 将第三个元素切片,并限制容量 // 其长度为 1 个元素,容量为 1 个元素 slice := source[2:3:3] // 向 slice 追加新字符串 slice = append(slice, "Kiwi") fmt.Println(slice)}

复制代码

通过设置长度和容量一样,之后对数组的append操作都是复制原有元素新建的数组,实现了和原来数组完全隔离。

[Running] go run "d:\GolandProjects\code-master\demo\hello.go"[Plum Kiwi]
[Done] exited with code=0 in 1.286 seconds
复制代码

内置函数append也是一个可变参数的函数,如果使用...运算符,可以将一个切片的所有元素追加到另一个切片里

package main
import ( "fmt")
func main() { // 创建两个切片,并分别用两个整数进行初始化 s1 := []int{1, 2} s2 := []int{3, 4} // 将两个切片追加在一起,并显示结果 fmt.Printf("%v\n", append(s1, s2...))}
复制代码

使用 Printf 时用来显示 append 函数返回的新切片的值

[Running] go run "d:\GolandProjects\code-master\demo\hello.go"[1 2 3 4]
[Done] exited with code=0 in 1.472 second
复制代码

迭代切片

既然切片是一个集合,可以迭代其中的元素。Go语言有个特殊的关键字range,它可以配合关键字for来迭代切片里的元素

  • 使用for range迭代切片

package main
import ( "fmt")
func main() { // 创建一个整型切片 // 其长度和容量都是 4 个元素 slice := []int{10, 20, 30, 40} // 迭代每一个元素,并显示其值 for index, value := range slice {  fmt.Printf("Index: %d Value: %d\n", index, value) }}

复制代码

当迭代切片时,关键字range 会返回两个值。第一个值是当前迭代到的索引位置,第二个值是该位置对应元素值的一份副本

[Running] go run "d:\GolandProjects\code-master\demo\hello.go"Index: 0 Value: 10Index: 1 Value: 20Index: 2 Value: 30Index: 3 Value: 40
[Done] exited with code=0 in 1.543 seconds
复制代码

需要强调的是,range 创建了每个元素的副本,而不是直接返回对该元素的引用

  • range 提供了每个元素的副本

  • 使用空白标识符(下划线)来忽略索引值

for _, value := range slice { fmt.Printf("Value: %d\n", value)}
复制代码
  • 使用传统的for循环对切片进行迭代

package main
import ( "fmt")
func main() { // 创建一个整型切片 // 其长度和容量都是 4 个元素 slice := []int{10, 20, 30, 40} // 迭代每一个元素,并显示其值 for index := 2; index < len(slice); index++ {  fmt.Printf("Index: %d Value: %d\n", index, slice[index])  }}

复制代码

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

[Running] go run "d:\GolandProjects\code-master\demo\hello.go"Index: 2 Value: 30Index: 3 Value: 40
[Done] exited with code=0 in 1.235 seconds
复制代码

函数cap返回切片的容量

package main
import ( "fmt")
func main() { // 创建一个整型切片 // 其长度和容量都是 4 个元素 slice := []int{10, 20, 30, 40} // 迭代每一个元素,并显示其值 for index := cap(slice)-1; index >= 0; index-- {  fmt.Printf("Index: %d Value: %d\n", index, slice[index])  }}========================[Running] go run "d:\GolandProjects\code-master\demo\hello.go"Index: 3 Value: 40Index: 2 Value: 30Index: 1 Value: 20Index: 0 Value: 10
[Done] exited with code=0 in 1.372 seconds
复制代码

多维切片

  • 声明多维切片

// 创建一个整型切片的切片slice := [][]int{{10}, {100, 200}}
复制代码
  • 组合切片的切片

package main
import ( "fmt")
func main() { // 创建一个整型切片的切片 slice := [][]int{{10}, {100, 200}} // 为第一个切片追加值为 20 的元素 slice[0] = append(slice[0], 20) fmt.Print(slice)}
复制代码

Go语言里使用append函数处理追加的方式很简明:先增长切片,再将新的整型切片赋值给外层切片的第一个元素

[Running] go run "d:\GolandProjects\code-master\demo\hello.go"[[10 20] [100 200]][Done] exited with code=0 in 1.451 seconds
复制代码

在函数间传递切片

在函数间传递切片就是要在函数间以值的方式传递切片。由于切片的尺寸很小,在函数间复制传递切片成本也很低。让我们创建一个大切片,并将这个切片以值的方式传递给函数 foo

// 分配包含 100 万个整型值的切片slice := make([]int, 1e6)// 将 slice 传递到函数 fooslice = foo(slice)// 函数 foo 接收一个整型切片,并返回这个切片func foo(slice []int) []int {...return slice
复制代码

64位架构的机器上,一个切片需要24字节的内存:指针字段需要 8 字节,长度容量字段分别需要 8 字节

由于与切片关联的数据包含在底层数组里,不属于切片本身,所以将切片复制到任意函数的时候,对底层数组大小都不会有影响。复制时只会复制切片本身,不会涉及底层数组

在这里插入图片描述

在函数间传递 24 字节的数据会非常快速、简单。这也是切片效率高的地方。不需要传递指针和处理复杂的语法,只需要复制切片,按想要的方式修改数据,然后传递回一份新的切片副本。

发布于: 刚刚阅读数: 3
用户头像

CSDN博客专家,华为云云享专家,RHCE/CKA认证 2022.01.04 加入

Java 后端一枚,技术不高,前端、Shell、Python 也可以写一点.纯种屌丝,不热爱生活,热爱学习,热爱工作,喜欢一直忙,不闲着。喜欢篆刻,喜欢吃好吃的,喜欢吃饱了晒太阳。

评论

发布
暂无评论
Go语言实战之切片的内部实现和基础功能_Go 语言_山河已无恙_InfoQ写作平台