GoLang 学习笔记(九)

446 阅读6分钟
$ ./goBible --type=书籍 --name=Go 语言圣经(三)--chapter=Composite Type

1. Array

没什么好说的,Array 是定长的,而且是可以比较的。比较使用的是深比较,也就是每一个数组元素都要相等。
几乎用不上的类型。

2. Slice

这是第三次讲 slice 了,不过确实 go 中的 slice 包含了很多设计理念和 trick,值得多次讨论。

2.1 为什么 slice 不能用 == 进行比较

尤其是明明 array 可以比较,alice 却不行,为什么,就按照 array 那样,比较每一个元素不就可以了吗?

  1. 假设 == 使用的是浅比较。这个为什么不行比较好理解,毕竟连 array 都不屑用浅比较。不然以下情况 a 和 b 不相等过于不够 trivial 了。
    a := []int{1, 2, 3}
    b := []int{1, 2, 3}
    // 很明显,a 和 b 相等才比较符合常识。编程语言不应该让人 confused。
    // 尤其是 GoLang 已经有足够多的 confused 语法了,不要再有更多了
    
  2. 假设 == 使用的是深比较,依然不行,有以下两个原因:
    1. slice of slices,会导致无限循环。比如以下代码:

      a := make([]interface{}, 3)
      a[0] = a
      
      // 或者
      type LoopSlices []LoopSlices
      a := make([]LoopSlices, 3)
      a[0] = a
      

      如果使用的是深比较,就会从 a 里拿出第一个元素来比较,然后发现还是 a,然后又从 a 里拿出第一个元素,还是 a,就这样永远不会结束。

    2. 会导致 map 出现问题。对于引用类型,map 只会浅复制这个 key。因为 map 要求所有的 key 永远都不会改变。这样的话,当 match 的时候,map 要怎么去比对 key 呢:

      1. 深比较是不可能的了,因为这和 map 要求 key 不能改变的定义矛盾。
      2. 浅比较的话,确实 map 的问题解决了。但是如果 map 比对 key 使用浅比较,== 反而使用深比较,就会让程序员 confused。(这是 go 说的,不是我说的啊,我不觉得 confused,是 go 它自以为是觉得我会 confused)
    3. 所以 go 干脆直接禁止 map 使用 slice 当 key 了。然后由于上面的自以为是的理由,因为 map 不能用 slice 当 key,所以 go 认为要保持一致,他又觉得我会 confused。于是顺便把普通的 slice 之间的 == 比较也去掉了。

  3. 不过有一个例外,就是 slice 可以和 nil 比对来判断是不是 nil slice(下下节),a := make([]int, 3); a == nil 是可以的。
  4. 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。

  1. nil slice 的 len 为 0,cap 为 0,没有 undelying array。
  2. 注意 len 为 0,甚至 cap 为 0,也不能代表就是 nil slice。比如说:
    • image.png
    • image.png
    • 前者不是 nil slice,后者才是。
  3. 不过好在,所有的函数对待 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 页。