4. 复合数据类型
4.1 数组
没有手动分配值初始化,编译器会给数组进行零初始化
如果在数组的长度位置出现的是“…”省略号,则表示数组的长度是根据初始化值的个数来计算。
q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"
数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。所以长度不同但底层类型相同的数组不能进行比较(==, !=等都算非法)
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int
可以在初始化的时候指定索引(位置)进行初始化,生成的数组大小为最后一个索引的值+1。在最后一个索引之前的索引的位置没有手动初始化赋值,那么会进行零初始化。因此除非必要数组依然很少用作函数参数;相反,我们一般使用slice来替代数组。
a := [...]int{2:10, 4:20}
fmt.Println(a) // "[0, 0, 10, 0, 20]"
fmt.Printf("%T", a) // "[5]int"
由于数组长度只要不一样,就属于不同的类型了,在一些情况处理起来会很麻烦。比如一个函数声明的时候有个参数是16大小的数组,那么在传参的时候就不能使用大小为8或者32的数组了。
4.2 切片
一个slice由三个部分构成:指针、长度和容量。
有点类型于:
type IntSlice struct {
ptr *int
len, cap int
}
多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。由于共享底层数据,所以一个切片的改动会影响另外一个切片的数据:
func main() {
arr := [...]int{1:10, 5:7, 9:10}
s1 := arr[:7]
s2 := arr[5:]
fmt.Println(arr, s1, s2)
//[0 10 0 0 0 7 0 0 0 10] [0 10 0 0 0 7 0] [7 0 0 0 10]
s1[6] = 66
fmt.Println(arr, s1, s2)
//[0 10 0 0 0 7 66 0 0 10] [0 10 0 0 0 7 66] [7 66 0 0 10]
}
和数组不同的是,slice之间不能比较,哪怕长度一样。slice唯一合法的比较操作是和nil比较。可以bytes.Equal函数来判断两个字节型slice是否相等。其他的类型还是要自己动手写一个出来。
func equal(x, y []string) bool {
if len(x) != len(y) {
return false
}
for i := range x {
if x[i] != y[i] {
return false
}
}
return true
}
如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。因为有不为nil的切片其长度也为空的。
可以利用append函数在切片中插入新的元素。append对于底层数组内存扩展采取的是翻倍,一旦容量不够,就扩充原理一倍的容量。但是通常我们并不知道append调用是否导致了内存的重新分配,因此我们也不能确认新的slice和原始的slice是否引用的是相同的底层数组空间。比如以下代码
func main() {
s1 := make([]int, 0, 5)
s1 = append(s1, 1, 2, 3, 4, 5)
s2 := append(s1, 6, 7, 8, 9, 10, 11)
s1[0] = 20
fmt.Println(s1, s2)
}
结果如下,可以看到s1和s2指向的底层数据不一样了
[20 2 3 4 5] [1 2 3 4 5 6 7 8 9 10 11]
4.3 map
一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别对应key和value。
但是map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作:
_ = &ages["bob"] // compile error: cannot take address of map element
禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。
Map的迭代顺序是不确定的。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。
map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它们的行为和一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常:
ages["carol"] = 21 // panic: assignment to entry in nil map
4.4 结构体
和map不同,结构体可以对成员取地址,然后通过指针访问。点操作符也可以和指向结构体的指针一起工作:
var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += " (proactive team player)"
// 相当于
(*employeeOfTheMonth).Position += " (proactive team player)"
如果结构体成员名字是以大写字母开头的,那么该成员就是导出的:
package test
type T struct{ a, b int } // a and b are not exported
type G struct{ A, B int } // a and b are not exported
package main
import (
"studygo/day12/test"
)
func main() {
t := test.T{a:10, b:20} //compile error
t := test.T{10, 20} //compile error
g := test.G{A:10, B:20}
}
如果结构体没有任何成员的话就是空结构体,写作struct{}。它的大小为0。空结构题会有一些特殊作用:Go语言–空结构体struct{}解析
如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的。
可比较的结构体类型和其他可比较的类型一样,可以用于map的key类型。注意,一定要是可比较的结构体
type address struct {
hostname string
port int
}
hits := make(map[address]int)
hits[address{"golang.org", 443}]++
嵌套结构体以及初始化
type Point struct {
X int
Y int
}
type Circle struct {
P Point
Radius int
}
type Wheel struct {
Circle // 匿名嵌套
Spokes int
}
func main() {
var w1 Wheel
fmt.Printf("%v\n", w1) // {{{0 0} 0} 0} 自动进行零值初始化
//w2 := Wheel{10, 20, 30, 40, 50 } // 这种声明初试化的方式不行
// 比较简洁的初试化方式
w2 := Wheel{Circle{Point{1, 2}, 3}, 4}
fmt.Printf("%v\n", w2) ////{{{1 2} 3} 4}
// 比较详细的初始化方式
w3 := Wheel{
Circle: Circle{ //匿名嵌套使用的是该类型的名字
P:Point{X:1, Y:2}, // 非匿名的用声明的那个名
Radius:3,
},
Spokes: 4,
}
fmt.Printf("%v\n", w3) //{{{1 2} 3} 4}
}