接下来我们就要进入另一个非常非常重要的篇章——Composite Types(复合类型)。这一章,我们需要讨论 4 种复合类型:
- array (数组)
- slice (切片)
- map(映射)
- struct(结构体)
数组将是第一个我们需要正式学习的复合类型。 slice 和 map 前面其实我们都已经接触过了,不过那时候只是浅浅尝辄止。
1. 数组
数组,是一种有固定长度的同一类元素的有序集合。划一下重点:
- 有固定长度
- 同一类元素
- 有序
学过 C/C++ 的朋友对数组肯定是非常的熟悉了,定义数组,那一定是有一个大小的(不要和我说 std::vector,我们讲的不是一个东西)。在 Go 里,数组也一定是有固定大小的。比如[5]int
类型,就表示长度为 5 的数组,每个元素都是 int
类型。
2. 声明和初始化数组
2.1 语法规则
- 方法一
声明数组的语法如下:
var a [N]type
其中,N 表示常量数字,type 就是数组元素的类型了。
- 方法二
还有一种办法,那就是自动类型推导了:
var a := [N]type{e1, e2, e3, ..., en}
上面这种办法要求使用一个*数组字面量来初始化变量 a. 这种方法的好处就是声明的时候还指定了值。
- 方法四
数组字面量有一个特性,它可以不必显式的写数组的长度大小,而是以 ...
代替:
var a := [...]type{e1, e2, e3, ..., en}
这样数组字面量里有几个元素,这个数组长度就是多少啦。
- 方法五
你甚至还可以给任意位置的元素初始化值:
var a := [100]string{40:"hello", 50:"world"}
上面表示声明一个大小为 100 的 string 数组,并将下标为 40 的元素初始化为 “hello”,下标为 50 的元素初始化为 “world”.
好了,说了这么多,还是来几个例子比较实在。
2.2 实例
下面是数组常见的初始化方法,可谓是灵活多变啊。但无论怎样,都脱离不了数组有固定长度这个事实。
package main
import "fmt"
func main() {
var a [3]string
a[0] = "Allen"
a[1] = "Luffy"
a[2] = "Zoro"
fmt.Printf("a = %v, len(a) = %v\n", a, len(a))
var b [4]int = [4]int{1, 2, 3, 4}
fmt.Printf("b = %v, len(b) = %v\n", b, len(b))
var c = [3]float64{1, 3.3}
fmt.Printf("c = %v, len(c) = %v\n", c, len(c))
d := [3]string{"Sanji", "Nami", "Robin"}
fmt.Printf("d = %v, len(d) = %v\n", d, len(d))
e := [...]int{0, 1, 2, 3}
fmt.Printf("e = %v, len(e) = %v\n", e, len(e))
f := [3]string{0: "Chopper", 1: "Brook"}
fmt.Printf("f = %v, len(f) = %v\n", f, len(f))
g := [3]string{2: "Frank", 0: "Chopper", 1: "Brook"}
fmt.Printf("g = %v, len(g) = %v\n", g, len(g))
h := [...]int{10: 100}
fmt.Printf("h = %v, len(h) = %v\n", h, len(h))
var i [3]int = [...]int{1, 2, 3}
fmt.Printf("i = %v, len(i) = %v\n", i, len(i))
}
最后输出结果如下:
a = [Allen Luffy Zoro], len(a) = 3
b = [1 2 3 4], len(b) = 4
c = [1 3.3 0], len(c) = 3
d = [Sanji Nami Robin], len(d) = 3
e = [0 1 2 3], len(e) = 4
f = [Chopper Brook ], len(f) = 3
g = [Chopper Brook Frank], len(g) = 3
h = [0 0 0 0 0 0 0 0 0 0 100], len(h) = 11
i = [1 2 3], len(i) = 3
3. 数组之间比较
Go 的数组另一个厉害的地方,它可以直接进行比较。不过不能比较大小,只能比较是否相等。
package main
import "fmt"
func main() {
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
d := [3]int{1, 3}
fmt.Printf("a = %v\nb = %v\nc = %v\nd = %v\n", a, b, c, d)
fmt.Println("a == b:", a == b) // a == b: true
fmt.Println("a == c:", a == c) // a == c: false
// fmt.Println(c == d) // compile error
// fmt.Println(a > b, a > c) // compile error
}
上面这个例子,数组 a, b, c 是相同的类型([2]int
),因此是可以直接比较的。两个类型相同的数组,只有所有元素一一比较都相等时,最后才算相等。就像上面的例子,数组 a 和 b 相等,但是 a 和 c 不相等。
类型不一样的两个数组是无法进行比较的,编译就会报错,比如上面的拿 c 和 d 比较。
最后,比较大小也是不可以的,编译也会报错,即使数组类型相同也不行!
4. 数组之间的赋值运算
4.1 数组间赋值
Go 中的数组和其它语言不太一样,Go 的数组的复制,会创建一个新的数组,并把元素挨个复制到新数组里去:
x := [...]int{9, 9, 9}
fmt.Printf("x = %v\n", x) // x = [9 9 9]
y := x
fmt.Printf("y = %v\n", y) // y = [9 9 9]
y[0] = 1
y[1] = 2
y[2] = 3
fmt.Printf("x = %v\n", x) // x = [9 9 9]
fmt.Printf("y = %v\n", y) // y = [1 2 3]
上面这个例子将 x
赋值给 y
后,再尝试修改 y
中的元素,发现对原始的 x
没有任何影响。
再看另一个例子,它将数组类型做函数的参数:
package main
import "fmt"
func f(a [3]int) {
fmt.Printf("a = %v\n", a) // a = [9 9 9]
a[0] = 1
a[1] = 2
a[2] = 3
fmt.Printf("a = %v\n", a) // a = [1 2 3]
}
func main() {
x := [...]int{9, 9, 9}
fmt.Printf("x = %v\n", x) // x = [9 9 9]
f(x)
fmt.Printf("x = %v\n", x) // x = [9 9 9]
}
这个特性说明了 Go 的数组之间的赋值是低效的,因此在 Go 里使用数组的场合并不多。但是为什么我们还要学它呢?一方面是确实有一些场合会使用它,另一方面是为了后面学习 slice 做铺垫。
4.2 指向数组的指针(pointer to an array)
如果你嫌上面的例子复制数组元素的开销很大,别忘记了,我们学过了 Go 的指针。
我们将 4.1 中的例子改写一下:
x := [...]int{9, 9, 9}
fmt.Printf("x = %v\n", x) // x = [9 9 9]
y := &x
fmt.Printf("y = %v\n", *y) // *y = [9 9 9]
y[0] = 1
y[1] = 2
y[2] = 3
fmt.Printf("x = %v\n", x) // x = [1 2 3]
fmt.Printf("y = %v\n", *y) // *y = [1 2 3]
我们发现,使用指向数组的指针后,通过指针就可以修改原始数组中的值了。再来改写 4.1 中的第二个例子:
package main
import "fmt"
func f(a *[3]int) {
fmt.Printf("*a = %v\n", *a) // *a = [9 9 9]
a[0] = 1
a[1] = 2
a[2] = 3
fmt.Printf("*a = %v\n", *a) // *a = [1 2 3]
}
func main() {
x := [...]int{9, 9, 9}
fmt.Printf("x = %v\n", x) // x = [9 9 9]
f(&x)
fmt.Printf("x = %v\n", x) // x = [1 2 3]
}
5. 总结
- 掌握数组的各种声明和初始化方法
- 数组是可以比较相等,但是不能比较大小
- 只有同类型的数组才可以比较
- 数组之间的赋值,是按元素逐一复制的
仅管指针提供了对原始数组直接操作的办法,也节省了数组间复制的开销,但是数组用起来还是不太方便。在很多语言里,都提供了一种可以改变长度大小的数组,在需要的时候,容量可以动态增长,也可以收缩。
其实,这就是我们后面要说的 Slice —— 切片。