1. 数组 array
数组是定长的,长度一旦定义,不能更改。
1.1. 如何声明?
声明数组,注意要指定长度。如果未指定长度可使用省略号,将会按长度初始化值的个数进行计算。
|
|
多维数组初始化
|
|
指定索引初始化
|
|
1.2. 零值
数组是值类型
声明数组后不赋值,则每个元素都被初始化零值,比如 int 类型会初始化为 0。
1.3. 特性
Go 语言中数组在初始化之后大小就无法改变,存储元素类型相同、但是大小不同的数组类型在 Go 语言看来也是完全不同的,只有两个条件都相同才是同一个类型。
|
|
两种不同的优化 cmd/compile/internal/gc.anylit
- 当元素数量小于或者等于 4 个时,会直接将数组中的元素放置在栈上;
- 当元素数量大于 4 个时,会将数组中的元素放置到静态区并在运行时取出;
1.4. 元素比较
需要满足两个条件
- 如果数组的元素类型可比较,则数组也可以比较
- 维数相同,含有元素个数相同
1.5. 遍历数组
|
|
1.6. 作为函数参数
调用 func instance(arr [5]int )
会拷贝数组
函数参数变量接收的是一个复制的副本,并不是原始调用的变量。因为函数参数传递的机制导致传递大的数组类型将是低效的,并且对数组参数的任何的修改都是发生在复制的数组上,并不能直接修改调用时原始的数组变量。
1.7. 编译初始化
编译器会在负责初始化字面量的 cmd/compile/internal/gc.anylit
函数中做两种不同的优化:
- 当元素数量小于或者等于 4 个时,会直接将数组中的元素放置在栈上;
- 当元素数量大于 4 个时,会将数组中的元素放置到静态区并在运行时取出拷贝到栈上;
2. 切片 slice
切片 slice 本身是没有数据的,是对底层 array 的一个 view
2.1. 如何声明?
声明为 nil 的空切片,底层数组指针 = nil,作为函数的实参也是 nil, 可以求长度,结果为 0
注意空切片可以 append 增加元素,可以动态扩展内存。同样为 nil 的map 不行,对空 map 增加键值对会 panic。
|
|
|
|
声明 len=10, cap=32 的 slice,会初始化 10 个零值
|
|
2.2. 取内容
多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。
|
|
切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice
|
|
x[m:n]切片操作对于字符串则生成一个新字符串,如果x是[]byte的话则生成一个新的[]byte。
2.3. 拼接 slice
… 可变参数列表, 在函数中可以 value ...int
用来解析列表, 在传递参数中使用 arr… 可以将数组解析成参数列表
|
|
2.4. 如何删除元素?
删除中间的元素
|
|
2.5. 作为函数参数
因为slice值包含指向第一个slice元素的指针 ( 后面介绍 slice 的源码),因此向函数传递slice将允许在函数内部修改底层数组的元素。
复制一个slice只是对底层的数组创建了一个新的 slice 别名
|
|
Go 语言只有值传递
|
|
在上面的例子中,切片值传递过去,使用 append 修改,改变了函数入参 s 的 len 和 cap,但没有改变 s1 的 len 和 cap。
3. 两者区别
- 数组需要指定长度
- 数组的长度是固定的, 切片是可变的
- 作为函数参数,数组是值拷贝,切片…..
4. 看看 slice 的结构
译期间的切片是 cmd/compile/internal/types.Slice
类型的,但是在运行时切片可以由如下的 reflect.SliceHeader
结构体表示
|
|
一个 slice
由三个部分构成:指针、长度和容量。
指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。
长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。
内置的len和cap函数分别返回slice的长度和容量。
slice 不能直接使用
==
!=
比较
底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。
|
|
5. 实例
循环
一种将slice元素循环向左旋转n个元素的方法是三次调用reverse反转函数,第一次是反转开头的n个元素,然后是反转剩下的元素,最后是反转整个slice的元素。(如果是向右循环旋转,则将第三个函数调用移到第一个调用位置就可以了。)
|
|
扩展用法
s1 对 arr 取值, s2 对 s1 取值"下标越界", 但 slice 是 view 视图, 实际取值是对原数组操作, 还是会得到正确的值, s2 中的 3:5 中 3 的位置是相对于 s1
即 slice 可以向后扩展, 不能越界, 不能向前扩展
|
|
对切片使用append
函数原型
|
|
虽然在扩容的时候 Go 语言一定会生成新的底层数组,但是它也同时生成了新的切片。它是把新的切片作为了新底层数组的窗口,而没有对原切片及其底层数组做任何改动。
请记住,在无需扩容时,append
函数返回的是指向原底层数组的新切片,而在需要扩容时,append
函数返回的是指向新底层数组的新切片。所以,严格来讲,“扩容”这个词用在这里虽然形象但并不合适。
顺便说一下,只要新长度不会超过切片的原容量,那么使用append
函数对其追加元素的时候就不会引起扩容。这只会使紧邻切片窗口右边的(底层数组中的)元素被新的元素替换掉
|
|
扩容规则
Go 1.18 对规则做了优化,以下规则仅适用于 <= 1.17
的版本,详细请见最新的笔记。
- 预估扩容后的容量
oldCap * 2 < cap
则 newCap = cap- 否则
- oldLen < 1024 翻倍扩容,即 newCap = oldCap*2
- oldLen >= 1024,扩容 1/4,即 newCap = oldCap*1.25
- 最后,进行内存对齐
需要多大内存?
内存管理模块会提前申请内存,分成不同规格管理起来。
|
|
例如通过 append 的得到 []int{1,2,3,4,5}
一共是 5*8=40 字节,查表可知最近接 40 的值是 48,即占用内存 48 字节,cap=6。
|
|
|
|
依然用上面的例子,class_to_size[size_to_class8[ n+a-1 ]]
,得出 48
|
|
数组反转
|
|
bytes.Equal
切片比较, bytes 提供了 []byte
类型的比较 , 若是其它类型, 自己封装函数
|
|