$ ./goBible --type=书籍 --name=Go 语言圣经(三)--chapter=Composite Type
1. Array
没什么好说的,Array 是定长的,而且是可以比较的。比较使用的是深比较,也就是每一个数组元素都要相等。
几乎用不上的类型。
2. Slice
这是第三次讲 slice 了,不过确实 go 中的 slice 包含了很多设计理念和 trick,值得多次讨论。
2.1 为什么 slice 不能用 == 进行比较
尤其是明明 array 可以比较,alice 却不行,为什么,就按照 array 那样,比较每一个元素不就可以了吗?
- 假设 == 使用的是浅比较。这个为什么不行比较好理解,毕竟连 array 都不屑用浅比较。不然以下情况 a 和 b 不相等过于不够 trivial 了。
a := []int{1, 2, 3} b := []int{1, 2, 3} // 很明显,a 和 b 相等才比较符合常识。编程语言不应该让人 confused。 // 尤其是 GoLang 已经有足够多的 confused 语法了,不要再有更多了
- 假设 == 使用的是深比较,依然不行,有以下两个原因:
-
slice of slices,会导致无限循环。比如以下代码:
a := make([]interface{}, 3) a[0] = a // 或者 type LoopSlices []LoopSlices a := make([]LoopSlices, 3) a[0] = a
如果使用的是深比较,就会从 a 里拿出第一个元素来比较,然后发现还是 a,然后又从 a 里拿出第一个元素,还是 a,就这样永远不会结束。
-
会导致 map 出现问题。对于引用类型,map 只会浅复制这个 key。因为 map 要求所有的 key 永远都不会改变。这样的话,当 match 的时候,map 要怎么去比对 key 呢:
- 深比较是不可能的了,因为这和 map 要求 key 不能改变的定义矛盾。
- 浅比较的话,确实 map 的问题解决了。但是如果 map 比对 key 使用浅比较,== 反而使用深比较,就会让程序员 confused。(这是 go 说的,不是我说的啊,我不觉得 confused,是 go 它自以为是觉得我会 confused)
-
所以 go 干脆直接禁止 map 使用 slice 当 key 了。然后由于上面的自以为是的理由,因为 map 不能用 slice 当 key,所以 go 认为要保持一致,他又觉得我会 confused。于是顺便把普通的 slice 之间的 == 比较也去掉了。
-
- 不过有一个例外,就是 slice 可以和 nil 比对来判断是不是 nil slice(下下节),
a := make([]int, 3); a == nil
是可以的。 - go 提供了一个深比较函数,bytes.Equal,但是只能对
[]byte
使用(其实 bytes.Equal 也就只是把参数转换成 string,然后在用 == 比较罢了)。如果想对其他的类型做深比较,就只能自己写 for loop 了。
2.2 吐槽
为了解释为什么 slice 不能用 == 来比较,要用如下的嵌套来解释,掘了:
1
|
2 -- | -- 1
| | -- 2
| | | -- 1
| | | -- 2
| |
| | -- 3
3
2.3 nil slice
nil 可以赋值给任意类型,slice 也不例外。当一个 slice 被赋值为 nil 值后,这个 slice 就变成了 nil slice。
- nil slice 的 len 为 0,cap 为 0,没有 undelying array。
- 注意 len 为 0,甚至 cap 为 0,也不能代表就是 nil slice。比如说:
- 前者不是 nil slice,后者才是。
- 不过好在,所有的函数对待 nil slice 和 no nil slice 是一样的方式。
3. Map
正如前面所说,map 的 key 必须是能够用 == 进行比较的。value 则随意。
此外,float 类型不太适合作为 key,虽然不会报错,但是 float 的 == 精度不高。比如说
var a float32 = 16777216
fmt.Println(a == a+1) // 想不到吧,是 true
float32 只有 6 bits decimal precision,float64 是 15 bits。
而且 float 还可能等于 NaN。而 NaN != NaN。
3.1 不存在的 key
查询不存在的 key 不会报错,而是返回 value type 的零值。
3.2 value 不可寻址
此外,map 的 value 不可以寻址,不管什么类型。
m := make(map[string]MyStruct)
m["hi"] = MyStruct{1}
_ := &m["hi"] // 报 panic
因为当插入新元素的时候,map 可能会改变 value 所在的地址。
3.3 nil map
map 也有 nil 值。
需要注意的是 nil map 不能插入元素,会报 panic。除此之外的其他操作都没问题。
3.4 比较
和 slice 一样,也不能用 == 来比较 map。
也和 slice 一样,也有特殊情况,可以和 nil 比较来判断是不是 nil map。
3.5 如何得到 set
当然,go 里没有 set。只是把 map 当做 set。一般是 map[<type>bool]
形式。value type 为 bool。因为不在 map 里的 key 当查阅的时候,value 会被自动初始化为 false。
不过要注意和一些 确实要用来储存 false 的值区分开。
3.6 slice,map 等作为 key
虽然不能用 slice,map 作为 key,但是可以定义一个 helper function,把 slice 转成比如字符串的形式,在对 key 进行各种操作前都对 key 调用这个 helper function。也就是将 map[key]
变成 map[helper(key)]
。
4. Struct
struct 里的 field 一般会把相关的 field 放在一起。
结构体的 field 也有导出和未导出之分。也是靠首字母来区分的。
4.1 结构体值直接取地址
其实并不是说结构体的值可以直接取指针,这也只是一个语法糖。
当声明 pp := &MyStruct{1, 2}
的时候,其实相当于声明以下:
pp := new(MyStruct)
*pp = MyStruct{1, 2}
所以并不是值本身占内存,再把地址赋值给变量。
只是一个语法糖,把值赋值给一个指针变量的地址罢了。
4.2 compareable
struct 是否是 compareable 要看 fileld 的类型,如果所有的 fields 都能用 == 来比较,那么该 struct 就是 compareable 的,vice versa。
如果这个 struct 是 compareable 的,那么就可以作为 map 的 key。
4.3 结构体嵌套,匿名字段
一个结构体可以嵌套另一个结构体,这个时候往往不写嵌套内的结构体的别名,这种没有名字的字段被叫做匿名字段。之后在讲方法的时候也会用到,那个时候就不一定是匿名结构体了,可以是各种类型,因为语法糖也适用于方法,你可以在外层直接调用内层的方法。
type Point struct {
X, Y int
}
type Circle struct {
Point
Radius int
}
var c = Circle{Point{0, 0}, 10} // 没有语法糖,只能嵌套这么写
c.X == c.Point.X // 匿名字段其实也不是没有名字,就是 type 的名字,但是可以省略
正如上面所说,匿名字段也是有名字的,由于结构体内不能重名。所以实际上造成了不能有两个同样类型的匿名字段。此外,如果匿名字段的类型是 unexported 的,在该 package 外面就只能用语法糖写法,而不能写不省略的写法了。
4.4 JSON
JSON 全称是 Javascript Object Notation。
具体没什么好讲的,json 很简单。这里主要讲标准库的 encoding/json 这个 package。
但是我发现很难简单讲清楚,所以请你参阅 go 圣经英文版的第 107 页。