0%

从0到1学Golang之基础--Go 数组

《Go In Action》 中文版 《Go语言实战》 读书笔记

内部实现

数组是切片和映射的基础数据结构。

在Go语言中,数组是长度固定的数据类型,用于存储一段具有相同类型的元素的连续块。

数组存储的类型可以是内置类型,如整型或字符串,也可以是某种结构类型。

数组占用的内存是连续分配的。由于内存连续,CPU能把正在使用的数据缓存更久的时间。而且内存连续很容易计算索引,可以快速迭代数组里的所有元素。

特点:

  • 长度固定
  • 类型相同
  • 内存连续

声明和初始化

声明

数组声明的原则:

  1. 指明存储数据的类型
  2. 指明存储元素数量(指明数组长度)
1
2
#声明一个包含5个元素的整型数组
var array [5]int

数组一旦声明后,数组里存储的数据类型和数组长度就不能改变了。

当数组初始化时,数组内每个元素都初始化为对应类型的零值。

声明并初始化

使用数组字面量声明并初始化数组。数组字面量允许声明数组里元素的数量同时指定每个元素的值。

Go为我们提供了 := 操作符,可以让我们在创建数组的时候直接初始化。

1
2
# 声明并初始化
array:=[5]int{1,2,3,4,5}
自动推导数组长度

使用 ... 替代数组的长度,Go语言会根据初始化时数组元素的数量来确定该数组的长度。

1
2
# 容量由初始化值的数量决定
array:=[...]int{1,2,3,4,5}
用具体值初始化特定索引

使用指定索引的方式初始化特定索引值。

1
2
# 用具体值初始化索引为1和3的元素,其余元素保持零值
array:=[5]int{1:10,3:30}

使用数组

要访问数组里某个单独元素,使用索引操作符 [] 即可。因为内存是连续的,所以索引访问的效率非常高。

1
2
3
4
# 声明一个包含5个元素的整形数组
array:=[5]int{1,2,3,4,5}
# 修改索引为2的元素值
array[2]=80
遍历访问数组

使用 for 遍历访问数组

1
2
3
4
5
6
func main() {
array := [5]int{1, 2, 3, 4, 5}
for i := 0; i < 5; i++ {
fmt.Printf("索引:%d,值:%d\n", i, array[i])
}
}

使用 for range 遍历访问数组每个元素

1
2
3
4
5
6
func main() {
array := [5]int{1, 2, 3, 4, 5}
for k, v := range array {
fmt.Printf("索引:%d,值:%d\n", k, v)
}
}
数组相互赋值

同样类型的数组是可以相互赋值的,不同类型的不行,会编译错误。

数组变量的类型包括数组长度和每个元素的类型。只有这两部分都相同的数组,才是类型相同的数组,才能相互赋值。

1
2
3
4
5
6
7
8
9
10
func main() {
array := [5]int{1, 2, 3, 4, 5}
//# 相同数组类型,能够互相赋值
// var array1 [5]int = array
// fmt.Println(array1) //[1 2 3 4 5]

//不同数组类型,不能互相赋值
var array2 [4]int = array //cannot use array (type [5]int) as type [4]int in assignment
fmt.Println(array2)
}
指针数组

在数组的类型前面加 * 声明为指针数组组。

使用 * 运算符可以访问指针数组元素指针所指向的值。

1
2
3
4
5
6
7
8
9
10
11
func main() {
// # 声明指向整数的数组,用整形指针初始化索引为0和3的数组元素
array := [5]*int{0: new(int), 3: new(int)}

// # 为索引为0和3的元素赋值
*array[0] = 10
*array[3] = 40

fmt.Println(array)
// [0xc420070188 <nil> <nil> 0xc4200701b0 <nil>]
}

上面的示例要注意,我们只可以给索引0 和 3 赋值,因为只有它们分配了内存,才可以赋值。如果给其他索引赋值,运行时会提示无效内存或nil指针引用。要解决这个问题,需要先给这些索引分配内存,然后再进行赋值修改操作。

1
2
3
4
5
6
7
8
9
10
11
12
...
...
// # 为未分配内存的索引赋值,报错
*array[2] = 30
// panic: runtime error: invalid memory address or nil pointer dereference
// [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x2199]

// # 先分配内存再赋值
array[2] = new(int)
*array[2] = 35
fmt.Println(array)
// [0xc420074188 <nil> 0xc4200741d0 0xc4200741b0 <nil>]
指针数组相互赋值

复制数组指针,只会复制指针的值,而不会复制指针所指向的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
// # 声明第一个包含3个元素的指向字符串的指针数组
var array1 [3]*string

// # 声明第一个包含3个元素的指向字符串的指针数组
var array2 [3]*string
// # 使用字符串指针初始化这个数组
array2 = [3]*string{new(string), new(string), new(string)}

// # 使用颜色为每个元素赋值
*array2[0] = "red"
*array2[1] = "blue"
*array2[2] = "green"

// # 将array2 复制给 array1
array1 = array2

// # 复制之后,两个数组指向同一组字符串
fmt.Println(array1)
fmt.Println(array2)
// [0xc420074030 0xc420074040 0xc420074050]
// [0xc420074030 0xc420074040 0xc420074050]
}

多维数组

数组本身只有一个维度,可以组合多个数组创建多维数组。

声明多维数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
// # 声明一个二维数组
var array1 [4][2]int
fmt.Println(array1) // [[0 0] [0 0] [0 0] [0 0]]

// # 使用数组字面量来声明并初始化一个二维数组
array2 := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
fmt.Println(array2) // [[10 11] [20 21] [30 31] [40 41]]

// # 声明并初始化外层数组中索引为1 和 3 的元素
array3 := [4][2]int{1: {20, 21}, 3: {40, 41}}
fmt.Println(array3) // [[0 0] [20 21] [0 0] [40 41]]

// # 声明并初始化外层数组和内层数组的当个元素
array4 := [4][2]int{1: {0: 20}, 3: {1: 41}}
fmt.Println(array4) // [[0 0] [20 0] [0 0] [0 41]]
}

声明多维数组时,第一维度的数组长度也可以使用 ... 来根据数组值的个数来确定,但后面维度不能使用 ...来自动推测。

1
2
3
4
5
func main() {
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}}
fmt.Println(b) // [[1 1] [2 2] [3 3]]
fmt.Println(len(b)) // 3
}
访问二维数组元素

为了访问单个元素,需要反复组合使用 [] 运算符。

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
// # 声明一个二维数组
var array1 [4][2]int
fmt.Println(array1) // [[0 0] [0 0] [0 0] [0 0]]

// # 设置指定元素的值
array1[1][0] = 5
fmt.Println(array1) // [[0 0] [5 0] [0 0] [0 0]]

// # 获取指定位置的元素
fmt.Println(array1[1][0]) //5
}
多维数组相互赋值

多维数组的类型包括每一维度的藏毒以及最终存储在元素中的数据的类型。只要类型一致,就可以将多维数组互相赋值。

1
2
3
4
5
6
7
func main() {
array1 := [2][2]int{{1, 2}, {3, 4}}
var array2 [2][2]int

array2 = array1
fmt.Println(array2) // [[1 2] [3 4]]
}
在函数间传递数组

在函数间传递变量时,总是以的形式传递。如果变量是个数组,那么就会整个复制,并传递给函数,如果数组非常大,对于内存是一个很大的开销。

1
2
3
4
5
6
7
8
9
10
func main() {
array := [5]int{1: 2, 3: 4}
modify(array)
fmt.Println(array) //[0 2 0 4 0]
}

func modify(a [5]int) {
a[1] = 3
fmt.Println(a) // [0 3 0 4 0]
}

如示例,数组是复制的,原来的数组没有修改。如果复制的数组非常大,对内存是一个非常大的浪费。

可以通过传递数组的指针的方式来在函数间传递大数组。

使用指针在函数间传递大数组

传递数组的指针,复制的大小只是一个数组类型的指针大小。

1
2
3
4
5
6
7
8
9
10
func main() {
array := [5]int{1: 2, 3: 4}
modify(&array)
fmt.Println(array) //[0 3 0 4 0]
}

func modify(a *[5]int) {
a[1] = 3
fmt.Println(*a) // [0 3 0 4 0]
}

如示例,通过传递数组的指针,会发现原来的数组也被修改了。要意识到的是,因为传递的是指针,所以如果改变指针指向的值,会改变共享的内存。

使用切片能更好的处理内存共享问题。

这里注意,数组的指针和指针数组是两个概念,数组的指针是 *[5]int,指针数组是 [5]*int,注意 * 的位置。


总结概括

数组知识点
  • 数组是值类型,赋值和传参会复制整个数组,而不是指针。
  • 数组长度必须是常量,且是类型的组成部分。[2]int[3]int 是不同类型。
  • 支持 "==""!=" 操作符,因为内存总是被初始化过的。
  • 指针数组 [n]*T,数组指针 *[n]T
  • 值拷贝行为会造成性能问题,通常会建议使用 slice,或数组指针。
声明并初始化
1
2
3
4
5
func main() {
// a := [3]int{1, 2} //未初始化元素值未数组类型零值
// b := [...]int{1, 2, 3, 4} //通过初始化值确定数组长度
// c := [5]int{2: 10, 4: 20} // 使用索引号初始化元素
}
多维数组
1
2
3
4
func main() {
d := [2][3]int{{1, 2, 3}, {4, 5, 6}} //多维数组
e := [...][2]int{{1, 1}, {2, 2}, {3, 3}, {4, 4}} //第 2 维度不能用 "..."
}
内置函数 len 和 cap 都返回数组长度 (元素数量)
1
2
3
4
func main() {
f := [2]int{}
fmt.Println(len(f), cap(f)) // 2 2
}
如有疑问或需要技术讨论,请留言或发邮件到 service@itfanr.cc