数组是一段存储固定类型、固定长度 的连续内存空间,在声明时就需要确定其大小和存储类型。
go
[size]Type
数组的使用:
go
func useArray() {
// 声明数组
var a [3]string
// 通过数组下标为数组成员赋值
a[0] = "a1"
a[1] = "a2"
fmt.Println(a)
// 通过下标访问数组成员
fmt.Println(a[0])
// 使用初始化列表进行初始化
var b [2]int = [2]int{1, 2}
c := [2]bool{true, false}
d := [...]string{"a", "b", "c"}
fmt.Println(b, c, d)
}
使用...
,编译器会根据{}
内成员的数量来推算数组的大小,将声明语句转换成[size]Type
的类型。
Go语言的数组是值类型,而不是引用类型,当数组变量被赋值给一个新变量或者作为函数参数传递时,会进行复制。
go
func useArray() {
d := [...]string{"a", "b", "c"}
// 赋值变量时会进行复制
e := d
e[0] = "z"
// 传递给函数时也会进行复制
x := f1(e)
fmt.Println(d) // [a b c]
fmt.Println(e) // [z b c]
fmt.Println(x) // [y b c]
}
func f1(x [3]string) [3]string {
x[0] = "y"
return x
}
因为赋值时会进行复制,传参时也会进行复制,所以d、e、x
有各自的数据,修改x
的数据不会影响到e
,修改e
的数据也不会影响到d
。因为值复制会比较消耗内存,所以在开发中一般都是用切片,切片用的是引用复制。
Go的编译器和运行时都会检查数组越界,在索引是非正整数,或者索引超过了数组的下标访问范围时会报错。
数组底层结构
数组往往是一块连续的内存(虚拟内存),元素集合连续地排列在这块内存中,可以使用位置下标,快速访问元素。
比如[3]int16{1, 2, 3}
,因为[size]Type
类型中已经知道了存放的类型和元素的数量,2个字节存储一个元素,知道数组的起始地址1234,如果想要获取索引2位置的元素,直接通过起始地址 + 索引值 x 元素尺寸(1235 + 2 x 2 = 1239) 就能得到第2个元素的起始地址,再通过这个地址获取元素的值。
(图中的地址数字是随便写的,示意用)
Go语言数组的数据类型为:
go
// Array contains Type fields specific to array types.
type Array struct {
Elem *Type // element type
Bound int64 // number of elements; <0 if unknown yet
}
包含2个属性:数组的元素类型、数组中包含的元素个数。
如果在编译期间,无法确认数组的元素个数,编译器就会报错,
go
func f2(i int) {
a := [i]int{1, 2} // invalid array length icompiler
fmt.Println(a)
}
这段代码只有运行起来才知道i
是多少,光看代码是无法确认i
的数量的,不像[...]int{1, 2, 3}
直接能通过代码的内容推断出来有3个元素。
使用unsafe
的Pointer
函数可以获取变量的内存地址,使用Sizeof
函数可以获得变量所占的内存大小。关于上面已经说过的"使用位置下标,快速访问元素",可以看这个具体的例子:
go
func f3() {
a := [3]int16{500, 600, 700}
p := &a
fmt.Printf("指向数组的指针 %p \n", p) // 指向数组的指针 0xc000116032
eleSize := unsafe.Sizeof(a[0])
// 数组a 每个元素占 2 字节,一共有3个元素,所以数组a占6字节
fmt.Printf("每个元素占 %v 字节,数组a一共占 %v 字节\n", eleSize, unsafe.Sizeof(a)) // 每个元素占 2 字节,数组a一共占 6 字节
startLoc := unsafe.Pointer(&a[0])
fmt.Printf("数组的起始地址 %v \n", startLoc) // 数组的起始地址 0xc000116032
index2EleLoc := unsafe.Pointer(uintptr(startLoc) + 2*eleSize)
fmt.Printf("索引2的元素的起始地址 %v \n", index2EleLoc) // 索引2的元素的起始地址 0xc000116036
fmt.Println(*(*uintptr)(index2EleLoc)) // 根据索引2的元素的起始地址拿到索引2位置的元素 700
}
通过这个公式起始地址 + 索引值 x 元素尺寸,计算索引2位置的元素的位置并拿到该位置的元素。(图中为了展示直观,去掉了地址前面的位数)
参考地址
- The Go Programming Language Specification:go.dev/ref/spec
- 《深入Go语言------原理、关键技术与实战》by 历冰、朱荣鑫、黄迪璇