文章来源:https://blog.thinkeridea.com/201901/go/shen_ru_pou_xi_slice_he_array.html
array
和 slice
看似相似,却有着极大的不同,但他们之间还有着千次万缕的联系 slice
是引用类型、是 array
的引用,相当于动态数组, 这些都是 slice
的特性,但是 slice
底层如何表现,内存中是如何分配的,特别是在程序中大量使用 slice
的情况下,怎样可以高效使用 slice
? 今天借助 Go
的 unsafe
包来探索 array
和 slice
的各种奥妙。
数组
slice
是在 array
的基础上实现的,需要先详细了解一下数组。
维基上如此介绍数组:
在计算机科学中,数组数据结构(英语:array data structure),简称数组(英语:Array),是由相同类型的元素(element)的集合所组成的数据结构,分配一块连续的内存来存储,利用元素的索引(index)可以计算出该元素对应的存储地址。 数组设计之初是在形式上依赖内存分配而成的,所以必须在使用前预先请求空间。这使得数组有以下特性:
1. 请求空间以后大小固定,不能再改变(数据溢出问题);
2. 在内存中有空间连续性的表现,中间不会存在其他程序需要调用的数据,为此数组的专用内存空间;
3. 在旧式编程语言中(如有中阶语言之称的C),程序不会对数组的操作做下界判断,也就有潜在的越界操作的风险(比如会把数据写在运行中程序需要调用的核心部分的内存上)。
根据维基的介绍,了解到数组是存储在一段连续的内存中,每个元素的类型相同,即是每个元素的宽度相同,可以根据元素的宽度计算元素存储的位置。 通过这段介绍总结一下数组有一下特性:
- 分配在连续的内存地址上
- 元素类型一致,元素存储宽度一致
- 空间大小固定,不能修改
- 可以通过索引计算出元素对应存储的位置(只需要知道数组内存的起始位置和数据元素宽度即可)
- 会出现数据溢出的问题(下标越界)
Go
中的数组如何实现的呢,恰恰就是这么实现的,实际上几乎所有计算机语言,数组的实现都是相似的,也拥有上面总结的特性。
Go
语言的数组不同于 C
语言或者其他语言的数组,C
语言的数组变量是指向数组第一个元素的指针; 而 Go
语言的数组是一个值,Go
语言中的数组是值类型,一个数组变量就表示着整个数组,意味着 Go
语言的数组在传递的时候,传递的是原数组的拷贝。
在程序中数组的初始化有两种方法 arr := [10]int{}
或 var arr [10]int
,但是不能使用 make
来创建,数组这节结束时再探讨一下这个问题。 使用 unsafe
来看一下在内存中都是如何存储的吧:
package main
import (
"fmt"
"unsafe"
)
func main() {
var arr = [3]int{
1, 2, 3}
fmt.Println(unsafe.Sizeof(arr))
size := unsafe.Sizeof(arr[0])
// 获取数组指定索引元素的值
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + 1*size)))
// 设置数组指定索引元素的值
*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + 1*size)) = 10
fmt.Println(arr[1])
}
这段代码的输出如下 (Go Playground):
12
2
10
首先说 12
是 fmt.Println(unsafe.Sizeof(arr))
输出的,unsafe.Sizeof
用来计算当前变量的值在内存中的大小,12
这个代表一个 int
有4个字节,3 * 4
就是 12
。
这是在32位平台上运行得出的结果, 如果在64位平台上运行数组的大小是 24
。从这里可以看出 [3]int
在内存中由3个连续的 int
类型组成,且有 12
个字节那么长,这就说明了数组在内存中没有存储多余的数据,只存储元素本身。
size := unsafe.Sizeof(arr[0])
用来计算单个元素的宽度,int
在32位平台上就是4个字节,uintptr(unsafe.Pointer(&arr[0]))
用来计算数组起始位置的指针,1*size
用来获取索引为1的元素相对数组起始位置的偏移,unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + 1*size))
获取索引为1的元素指针,*(*int)
用来转换指针位置的数据类型, 因为 int
是4个字节,所以只会读取4个字节的数据,由元素类型限制数据宽度,来确定元素的结束位置,因此得到的结果是 2
。
上一个步骤获取元素的值,其中先获取了元素的指针,赋值的时候只需要对这个指针位置设置值就可以了, *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + 1*size)) = 10
就是用来给指定下标元素赋值。
package main
import (
"fmt"
"unsafe"
)
func main() {
n:= 10
var arr = [n]int{}
fmt.Println(arr)
}
如上代码,动态的给数组设定长度,会导致编译错误 non-constant array bound n
, 由此推导数组的所有操作都是编译时完成的,会转成对应的指令,通过这个特性知道数组的长度是数组类型不可或缺的一部分,并且必须在编写程序时确定。
可以通过 GOOS=linux GOARCH=amd64 go tool compile -S array.go
来获取对应的汇编代码,在 array.go
中做一些数组相关的操作,查看转换对应的指令。
之前的疑问,为什么数组不能用 make
创建? 上面分析了解到数组操作是在编译时转换成对应指令的,而 make
是在运行时处理(特殊状态下会做编译器优化,make可以被优化,下面 slice
分析时来讲)。
slice
因为数组是固定长度且是值传递,很不灵活,所以在 Go
程序中很少看到数组的影子。然而 slice
无处不在,slice
以数组为基础,提供强大的功能和遍历性。
slice
的类型规范是[]T,slice
T元素的类型。与数组类型不同,slice
类型没有指定的长度。
slice
申明的几种方法:
s := []int{1, 2, 3}
简短的赋值语句var s []int
var
申明make([]int, 3, 8)
或make([]int, 3)
make
内置方法创建s := ss[:5]
从切片或者数组创建