目录
1. Array数组
- 特点
-
内存是连续的
-
大小不能改变
-
值传递
- 初始化
arr1 := [3]int{1, 2, 3}
arr2 := [...]int{1, 2, 3}
两个行为完全相同,go编译器会推导数组大小。 [...]T 这种初始化方式只是一个语法糖
初始化的位置:
-
当元素数量<= 4 个时,直接在栈上初始化
-
当元素数量>4 个时,会将元素在静态区初始化,在运行时取出cp到栈上
- 访问
由于内存连续,cpu很容易计算索引(下标),可以通过下标快速访问
- 编译期与运行时工作
编译期做的工作(转换为直接读写内存):数组的初始化、越界的静态检查
运行时做的工作(中间代码生成期间):越界检查
- go数组和其他语言数组
c语言的数组变量是指向数组第一个元素的指针(实际是go的切片)
go的数组变量代表了整个数组,在传递时传递的是原数组的拷贝,可以理解为一种有序的struct
- 数组和链表对比
-
数组:内存连续的。有缓存优化,cpu的cache命中率会高,性能较高
-
链表:内存不连续
数组优于链表的:
-
内存空间占用少。链表节点会附加上一块或两块下一个节点的信息,但是数组在建立时就固定了
-
数组内的数据可随机访问。链表不具备随机访问性,链表在内存地址是分散的,必须通过上一节点找下一节点
-
查找速度块。因为内存地址连续性的
链表优于数组的:
-
插入与删除的操作方便
-
内存地址的利用率。如果无法一次性给出数组所需的要空间,就会提示内存不足,磁盘空间整理的原因之一在这里。而链表是分散的空间地址
-
链表的扩展性好。数组建立后所占用的空间大小是固定的,如果满了就要建一个更大空间的数组,而链表可以很方便的扩展
2. Slice切片
- 特点
-
内存连续
-
动态数组,长度不固定,可以自动扩容
-
扩容会分配新的内存并拷贝数据,所以地址会变
-
引用传递
- 切片结构体
Data uintptr 指向数组的指针
Len int 当前切片的长度
Cap int 当前切片的容量,即Data数组的大小
可以将切片理解成一片连续的内存空间加上长度与容量的标识
一个切片结构体变量值固定大小24字节
- 初始化
1、使用下标初始化
arr[0:3]
不会拷贝原数组或者原切片中的数据,它只会创建一个指向原数组的切片结构体,所以修改新切片的数据也会修改原切片
2、使用字面量初始化
slice := []int{1, 2, 3}
大部分工作会在编译期间完成
3、使用make关键字初始化
slice := make([]int, 10)
很多工作都需要运行时的参与。
如果遇到了比较小的对象会直接初始化在 Go 语言调度器里面的 P 结构中,而大于 32KB 的对象会在堆上初始化。
运行时函数 runtime.makeslice 计算切片占用的内存空间并在堆上申请一片连续的内存。计算方式:内存空间=切片中元素大小×切片容量
- 访问
使用 len 和 cap 获取长度和容量
访问元素索引会在中间代码生成期间转换成对地址的直接访问
- 追加和扩容
会区分是否返回值会覆盖原变量,追加的逻辑略有不同。
如果我们要覆盖原有的变量,就不需要担心切片发生拷贝影响性能,因为 Go 语言编译器已经对这种情况做出了优化。
runtime.growslice 函数为切片扩容,扩容会为切片分配新的内存空间并拷贝原切片中元素,所以扩容后切片的地址可能会变。
1、确定容量
策略:如果期望容量大于当前容量的两倍就会使用期望容量;如果当前切片的长度小于 1024 就会将容量翻倍;如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量
2、对齐内存
当数组中元素所占的字节大小为2/4/8 的倍数时,运行时会对齐内存,将待申请的内存向上取整,提高内存的分配效率并减少碎片
var arr []int64
arr = append(arr, 1, 2, 3, 4, 5)
执行上述代码时,会触发 runtime.growslice 函数扩容 arr 切片并传入期望的新容量 5,这时期望分配的内存大小为 40 字节;不过因为切片中的元素大小等于 sys.PtrSize,所以运行时会调用 runtime.roundupsize 向上取整内存的大小到 48 字节,所以新切片的容量为 48 / 8 = 6
- 拷贝
无论是编译期间拷贝还是运行时拷贝,都会通过 runtime.memmove 将整块内存的内容拷贝到目标的内存区域中。
相比于依次拷贝元素,runtime.memmove 能够提供更好的性能。需要注意的是,整块拷贝内存仍然会占用非常多的资源,在大切片上执行拷贝操作时一定要注意对性能的影响。
append([]int{}, choosed...)
copy(dest, src) // 注意dest切片要预先分配好内存
- 数组和切片对比
数组 切片
大小 不能改变 动态的,长度不固定,可以自动扩容
类型 值类型 指针类型
传递方式 值传递 引用传递
内存 固定连续 连续不固定,扩容后挪到一片新的连续内存空间
- 空切片与nil切片
空切片:
a := make([]int, 0)
nil切片:
var a []int
区别:空切片指向一个地址,nil切片没有具体指向的地址
切片本身都是占24字节内存的
- 切片的性能调优
-
预分配容量:减少不必要的扩容,扩容会进行内存分配和cp数据
-
append 时预分配容量:append 时预先分配足够的容量,避免多次使用 append
-
重用底层数组:减少内存分配和释放的开销
-
使用 sync.Pool 减少内存分配和释放的开销:使用 sync.Pool 重复使用之前分配的对象,避免频繁的内存分配和释放操作
- 数组和切片的相互转换
数组-->切片:
arr := [3]int{1, 2, 3}
slice := arr[:]
切片-->数组指针:(1.17开始支持)
// 实际是将原切片的每一项的地址cp到新的内存空间
// 所以slice和arr地址不同,但是它们的值指向相同地址
arr := (*[3]int)(slice)
切片-->数组:(1.20开始支持)
// 实际是将每一项的内容cp到新的内存空间
// slice和arr地址不同
arr := [3]int(slice)
3. String字符串
- 特点
连续的内存空间
- 数据结构
type StringHeader struct {
Data uintptr //8字节
Len int //8字节
}
一个字符串结构体固定大小16字节
- 字符串拼接
实际会使用copy 将输入的多个字符串拷贝到目标字符串所在的内存空间。新的字符串是一片新的内存空间,与原来的字符串也没有任何关联,一旦需要拼接的字符串非常大,拷贝带来的性能损失是无法忽略的。
- 类型转换
字符串和 []byte 中的内容虽然一样,但是字符串的内容是只读的,我们不能通过下标或者其他形式改变其中的数据,而 []byte 中的内容是可以读写的。
不过无论从哪种类型转换到另一种都需要拷贝数据,内存拷贝的性能损耗会随着字符串和 []byte 长度的增长而增长。