Skip to content
Sev7e0

go数组

go1 min read

数组作为主流编程语言中都存在的一种数据结构,其在编程语言中的地位不可或缺,对于存储长度固定且快速查找的需求下能够快速的满足编程需要。 在他不能满足时我们再会考虑链表,hash表等结构,不过很多情况下复杂的数据结构也是基于数组实现,例如java中的ArrayList以及go中的slice。

在go中数组、字符串、切片(slice)三种数据结构的底层原始数据都有相同的内存结构,在api层面由于go语言语法的限制而有着不同的操作。所以打算用三 篇文章记录分析一下三者的异同点,便于学习。

go中的数组

数组本省是一个固定长度的特定类型元素所组成的一个序列,数组可以为空或者多个元素。数组的长度是其本身组成的一部分。正因如此,不同的特定类型,或者不同的长度 的数组都是不同的类型。正因如此导致了数组不够灵活,在日常的开发中是用的较少,更多的是用的是数组中slice,就像在java中使用的List结构一样,主要原因还是其灵活,可以动态 调整大小,不需要程序员过多的关心越界、扩容的问题。

定义方式

1var a [3]int //定义长度为3的int类型数组,元素全部为0
2var b = [...]int{1, 2, 3} // 定义长度为3的int型数组, 元素为 1, 2, 3
3var c = [...]int{2: 3, 1: 2} // 定义长度为3的int型数组, 元素为 0, 2, 3
4var d = [...]int{1, 2, 4: 5, 6} // 定义长度为6的int型数组, 元素为 1, 2, 0, 0, 5, 6

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

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

第三种方式是以索引的方式来初始化数组的元素,因此元素的初始化值出现顺序比较随意。这种初始化方式和map[int]Type类型的初始化语法类似。数组的长度以出现的最大的索引为准,没有明确初始化的元素依然用0值初始化。

第四种方式是混合了第二种和第三种的初始化方式,前面两个元素采用顺序初始化,第三第四个元素零值初始化,第五个元素通过索引初始化,最后一个元素跟在前面的第五个元素之后采用顺序初始化

内存结构

还需要记住一点,go中的数组是一种值类型,数组的元素可以修改,但数组的本身的赋值和函数传参都是以整体复制的方式处理,也就意味着其赋值和传参都会产生一个新的数组。

当数组较大时这钟操作也会更加消耗资源,所以为了避免这种操作可以使用传递数组指针,但是数组指针并不是数组。

1var a = [...]int{1, 2, 3} // a 是一个数组
2var b = &a // b 是指向数组的指针
3
4fmt.Println(a[0], a[1]) // 打印数组的前2个元素
5fmt.Println(b[0], b[1]) // 通过数组指针访问数组元素的方式和数组类似
6
7for i, v := range b { // 通过数组指针迭代数组的元素
8 fmt.Println(i, v)
9}

其中b是指向a数组的指针,但是通过b访问数组中元素的写法和a类似的。还可以通过for range来迭代数组指针指向的数组元素。其实数组指针类型除了类型和数组不同之外,通过数组指针操作数组的方式和通过数组本身的操作类似,而且数组指针赋值时只会拷贝一个指针。但是数组指针类型依然不够灵活,因为数组的长度是数组类型的组成部分,指向不同长度数组的数组指针类型也是完全不同的。

可以将数组看作一个特殊的结构体,结构的字段名对应数组的索引,同时结构体成员的数目是固定的。内置函数len可以用于计算数组的长度,cap函数可以用于计算数组的容量。不过对于数组类型来说,len和cap函数返回的结果始终是一样的,都是对应数组类型的长度。

遍历数组

1for i := range a {
2 fmt.Printf("a[%d]: %d\n", i, a[i])
3}
4for i, v := range b {
5 fmt.Printf("b[%d]: %d\n", i, v)
6}
7for i := 0; i < len(c); i++ {
8 fmt.Printf("c[%d]: %d\n", i, c[i])
9}

前两种方式较为普遍,因为其不用担心数组经典的越界问题,省去了每次遍历判断是否越界的判断。

go中使用数组

数组不仅仅可以用于数值类型,还可以定义字符串数组、结构体数组、函数数组、接口数组、管道数组等等:

1// 字符串数组
2var s1 = [2]string{"hello", "world"}
3var s2 = [...]string{"你好", "世界"}
4var s3 = [...]string{1: "世界", 0: "你好", }
5
6// 结构体数组
7var line1 [2]image.Point
8var line2 = [...]image.Point{image.Point{X: 0, Y: 0}, image.Point{X: 1, Y: 1}}
9var line3 = [...]image.Point{{0, 0}, {1, 1}}
10
11// 图像解码器数组
12var decoder1 [2]func(io.Reader) (image.Image, error)
13var decoder2 = [...]func(io.Reader) (image.Image, error){
14 png.Decode,
15 jpeg.Decode,
16}
17
18// 接口数组
19var unknown1 [2]interface{}
20var unknown2 = [...]interface{}{123, "你好"}
21
22// 管道数组
23var chanList = [2]chan int{}

我们还可以定义一个空的数组:

1var d [0]int // 定义一个长度为0的数组
2var e = [0]int{} // 定义一个长度为0的数组
3var f = [...]int{} // 定义一个长度为0的数组

长度为0的数组在内存中并不占用空间。空数组虽然很少直接使用,但是可以用于强调某种特有类型的操作时避免分配额外的内存空间,比如用于管道的同步操作:

1c1 := make(chan [0]int)
2go func() {
3 fmt.Println("c1")
4 c1 <- [0]int{}
5}()
6<-c1

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

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

我们可以用fmt.Printf函数提供的%T或%#v谓词语法来打印数组的类型和详细信息:

1fmt.Printf("b: %T\n", b) // b: [3]int
2fmt.Printf("b: %#v\n", b) // b: [3]int{1, 2, 3}

在Go语言中,数组类型是切片和字符串等结构的基础。以上数组的很多操作都可以直接用于字符串或切片中。