您的位置 首页 golang

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

写在前面


  • 嗯,学习 GO ,所以有了这篇文章
  • 博文内容为 《GO语言实战》 读书笔记之一
  • 主要涉及切片相关知识

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


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

切片 是一种数据结构(类似于 Java ArrayList ),围绕 动态数组 的概念构建的,可以按需自动增长和缩小。 切片的动态增长是通过内置函数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-arguments
d:\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

这里书里讲的个人感觉不太好理解,其实类似 Java String的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)
// 使用原有的容量来分配一个新元素
// 将新元素赋值为 60
newSlice = 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}
// 使用原有的容量来分配一个新元素
// 将新元素赋值为 60
newSlice := 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: 10
Index: 1 Value: 20
Index: 2 Value: 30
Index: 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])
  }
}

  

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

 [Running] go run "d:\GolandProjects\code-master\demo\hello.go"
Index: 2 Value: 30
Index: 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: 40
Index: 2 Value: 30
Index: 1 Value: 20
Index: 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 传递到函数 foo
slice = foo(slice)
// 函数 foo 接收一个整型切片,并返回这个切片
func foo(slice []int) []int {
...
return slice
} 
  

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

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

在这里插入图片描述

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

文章来源:智云一二三科技

文章标题:Go语言实战之切片的内部实现和基础功能

文章地址:https://www.zhihuclub.com/97228.shtml

关于作者: 智云科技

热门文章

网站地图