Go - 数组

数组是一个由固定长度的特定类型元素组成的序列,它可以保存特定数量的元素,可以由零个或多个元素组成,但它不能增长或收缩。

Go中数组关键点描述:

  • Go语言的数组是一种值类型,虽然数组的元素可以被修改,但是数组本身的赋值和函数传参都是以整体复制的方式处理的。
  • 数组的长度是数组类型的组成部分,因此,不同长度或不同类型的数据组成的数组都是不同的类型。(注意:在C#中,数组类型是不包含数组的长度的,而Go中,数组的长度直接作为了数组类型的一部分,所以在Go语言中很少直接使用数组(不同长度的数组因为类型不同无法直接赋值))
  • 数组中的元素索引从0开始。
  • 在Go语言中,数组类型是切片和字符串等结构的基础。

Go语言实现中非0大小数组的长度不得超过2GB,因此需要针对数组元素的类型大小计算数组的最大长度范围([]uint8最大2GB,[]uint16最大1GB,依此类推,但是[]struct{}数组的长度可以超过2GB)。

数组的定义和初始化方式

方式一:指定数组长度

第一种方式是定义一个数组变量的最基本的方式,数组的长度明确指定,数组中的每个元素都以零值初始化。

var a [3]int

方式二:指定数组元素

第二种方式可以在定义的时候顺序指定全部元素的初始化值,数组的长度根据初始化元素的数目自动计算。

var b = [...]int{1, 2, 3}
//或
var b2 = [3]int{4, 5, 6}

方式三:使用索引初始化元素

第三种方式是以索引的方式来初始化数组的元素,元素的初始化值出现顺序可以随意指定,数组的长度以出现的最大的索引为准,没有明确初始化的元素依然用零值初始化。

var c = [...]int{2: 3, 1: 2}

上述语句将会创建如下数组:

[0 2 3]

方式四:使用索引初始化和指定元素初始化的混合形式

var d = [...]int{1, 2, 4: 5, 6}
4:5

因此,上述语句将会创建如下数组:

[1 2 0 0 5 6]

数组的读取

Go语言中数组是值语义。一个数组变量即表示整个数组,它并不是隐式地指向第一个元素的指针(例如C语言的数组),而是一个完整的值。当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。如果数组较大的话,数组的赋值也会有较大的开销。为了避免复制数组带来的开销,可以传递一个指向数组的指针,但是数组指针并不是数组。

var a = [...]int{1, 2, 3}
var b = &a //b是指向数组的指针

数组指针类型除类型和数组不同之外,通过数组指针操作数组的方式和通过数组本身的操作类似,而且数组指针赋值时只会复制一个指针。

上述代码中,b是指向数组a的指针,通过b访问数组中元素的写法和a是类似的。还可以通过for range来迭代数组指针指向的数组元素。

//通过数组指针访问数组元素的方式和通过数组类似
fmt.Println(b[0], b[1])
//通过for range来迭代数组指针指向的数组元素
for i, v := range b {
	fmt.Println(i, v)
}

因为数组的长度是数组类型的组成部分,可以将数组看作一个特殊的结构体,结构的字段名对应数组的索引,同时结构体成员的数目是固定的。

获取数组的长度和容量

内置函数len()可以用于计算数组的长度。

内置cap()函数可以用于计算数组的容量。

不过对数组类型来说,len()和cap()函数返回的结果始终是一样的,都是对应数组类型的长度。

使用len()函数返回数组的长度(它包含的元素个数)。

var myArr [3]int = [3]int{1, 2}
fmt.Println(len(myArr))		//输出:3

声明数组时,长度设置的是几,len()函数就返回几,而不用管其中的元素是否赋值(没有显式设置值的元素将使用默认值)

数组的遍历

使用for…range循环遍历数组(推荐)

使用for range方式迭代的性能可能会更好一些,因为这种迭代可以保证不会出现数组越界的情形,每轮迭代对数组元素的访问时可以省去对下标越界的判断。

语法格式:

for index,value := range myArray{
	//循环体
}

格式说明:

  • index:保存每个元素索引的变量
  • value:保存每个元素值的变量
  • myArray:正在处理的数组

示例:

for index, value := range notes {
	fmt.Println(index, value)
}

如果在遍历的过程中,不想使用其中声明的变量index或value,那么可以使用空白标识符(_)来代替变量名。这将导致Go丢弃该值,而不会产生编译器错误。

也可以直接省略元素的值:

for i := range a {
	fmt.Println(i, a[i])
}

还可以忽略迭代的下标:

var t [5][0]int
for range t {
	fmt.Println("hello")
}

[5][0]int

使用for循环遍历数组(不推荐)

for i := 0; i < len(a); i++ {
	fmt.Println(i, a[i])
}

数组相关的优化

除了上文中的为了避免复制数组带来的开销,使用数组的指针来访问数组之外,在进行其他操作时还可以借助空数组来减少内存的占用。

空数组指的是长度为0的数组。

var d [0]int
var e = [0]int{}
var f = [...]int{}

var t [5][0]int

例如,用于通道的同步操作:

c1 := make(chan [0]int)
go func() {
	fmt.Println("c1")
	c1 <- [0]int{}
}()
<-c1

在这里,通道接收和发送操作只是用于消息的同步。对于这种场景,我们用空数组作为通道类型可以减少通道元素赋值时的开销。当然,一般更倾向于用无类型的匿名结构体代替空数组:

c2 := make(chan struct{})
go func() {
	fmt.Println("c2")
	//struct{}部分是类型,{}表示对应的结构体值
	c2 <- struct{}{}
}()
<-c2

使用“fmt”输出数组元素

var notes [3]string = [3]string{"AA", "BB", "CC"}
fmt.Println(notes)       //输出:[AA BB CC]
fmt.Printf("%#v", notes) //输出:[3]string{"AA", "BB", "CC"}
fmt.Printf("%T", notes)  //输出数组类型:[3]string