【go基础】5.基本数据结构之array, slice, string

目录

1. Array数组

- 特点

- 初始化

- 访问

- 编译期与运行时工作

- go数组和其他语言数组

- 数组和链表对比

2. Slice切片

- 特点

- 切片结构体

- 初始化

- 访问

- 追加和扩容

- 拷贝

- 数组和切片对比

- 空切片与nil切片

- 切片的性能调优

- 数组和切片的相互转换

3. String字符串

- 特点

- 数据结构

- 字符串拼接

- 类型转换


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命中率会高,性能较高
  • 链表:内存不连续

数组优于链表的:

  1. 内存空间占用少。链表节点会附加上一块或两块下一个节点的信息,但是数组在建立时就固定了
  2. 数组内的数据可随机访问。链表不具备随机访问性,链表在内存地址是分散的,必须通过上一节点找下一节点
  3. 查找速度块。因为内存地址连续性的

链表优于数组的:

  1. 插入与删除的操作方便
  2. 内存地址的利用率。如果无法一次性给出数组所需的要空间,就会提示内存不足,磁盘空间整理的原因之一在这里。而链表是分散的空间地址
  3. 链表的扩展性好。数组建立后所占用的空间大小是固定的,如果满了就要建一个更大空间的数组,而链表可以很方便的扩展        

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 长度的增长而增长。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值