写在前面
嗯,学习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 整型切片
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
总是会增加新切片的长度
,而容量
有可能会改变,也可能不会改变,这取决于被操作的切片的可用容量
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
来迭代切片里的元素
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)
}
复制代码
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 字节
的数据会非常快速、简单。这也是切片效率高的地方。不需要传递指针和处理复杂的语法,只需要复制切片,按想要的方式修改数据,然后传递回一份新的切片副本。
评论