钱文翔的博客

《Go语言圣经》学习小记(1)

记录《Go语言圣经》学习笔记。(实为划重点)

顺序通信进程 ( communicating sequential processes ,缩写为CSP)。在CSP中,程序是一组中间没有共享状态的平行运行的处理过程,它们之间使用管道进行通信和控制同步。
Go语言的官方博客 https://blog.golang.org 会不定期发布一些Go语言最好的实践文章,包括当前语言的发展状态、未来 的计划、会议报告和Go语言相关的各种会议的主题等信息(译注: http://talks.golang.org/ 包含了 官方收录的各种报告的讲稿)。

第一章 入门

对map进行range循环时,其迭代顺序是不确定的,从实践来看,很可能每次运行都会有不一样的结果(译注:这是Go语言的设计者有意为之的,因为其底层实现不保证插入顺序和遍历顺序一致,也希望程序员不要依赖遍历时的顺序,所以干脆直接在遍历的时候做了随机化处理,醉了。补充:好像说随机序可以防止某种类型的攻击,虽然不太明白,但是感觉还蛮厉害的),来避免程序员在业务中依赖遍历时的顺序。
map是用make函数创建的数据结构的一个引用。当一个map被作为参数传递给一个函数时,函数接收到的是一份引用的拷贝,虽然本身并不是一个东西,但因为他们指向的是同一块数据对象(译注:类似于C++里的引用传递,实际上指针是另一个指针了,但内部存的值指向同一块内存),所以你在函数里对map里的值进行修改时,原始的map内的值也会改变。

第二章 程序结构

如果初始化表达式被省略,那么将用零值初始化该变量。 数值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(包括slice、map、chan和函数)变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。
var形式的声明语句往往是用于需要显式指定变量类型地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。
任何类型的指针的零值都是nil。。如果p != nil测试为真,那么p是指向某个有效变量。指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等。
每次调用new函数都是返回一个新的变量的地址,当然也可能有特殊情况:如果两个类型都是空的,也就是说类型的大小是0,例如struct{}和 [0]int,有可能有相同的地址(依赖具体的语言实现)(译注:请谨慎使用大小为0的类型,因为如果类型的大小为0的话,可能导致Go语言的自动垃圾回收器有不同的行为,具体请查看runtime.SetFinalizer函数相关文档)。

第三章 基础数据类型

一个字符串是一个不可改变的字节序列。内置的len函数可以返回一个字符串中的字节数目(不是rune字符数目),索引操作s[i]返回第i个字节的字节值,i必须满足0 ≤ i< len(s)条件约束。字符串的值是不可变的:一个字符串包含的字节序列永远不会被改变,当然我们也可以给一个字符串变量分配一个新字符串值。可以像下面这样将一个字符串追加到另一个字符串:

1
2
3
s := "left foot"
t := s
s += ", right foot"

这并不会导致原始的字符串值被改变,但是变量s将因为+=语句持有一个新的字符串值,但是t依然是包含原先的字符串值。
Go语言的常量有个不同寻常之处。虽然一个常量可以有任意有一个确定的基础类型,例如int或float64,或者是类似time.Duration这样命名的基础类型,但是许多常量并没有一个明确的基础类型。编译器为这些没有明确的基础类型的数字常量提供比基础类型更高精度的算术运算。
无类型的常量不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。
例如math包中的Pi常量:

1
Pi  = 3.14159265358979323846264338327950288419716939937510582097494459 //http://oeis.org/A000796

第四章 符合数据类型

数组

在数组字面值中,如果在数组的长度位置出现的是“…”省略号,则表示数组的长度是根据初始化值的个数来计算。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。

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

也可以指定一个索引和对应值列表的方式初始化,就像下面这样:

1
2
3
4
5
6
7
8
9
10
type Currency int

const (
USD Currency = iota // 美元
EUR // 欧元
GBP // 英镑
RMB // 人民币
)
symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}
fmt.Println(RMB, symbol[RMB]) // "3 ¥"

如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的。不相等比较运算符!=遵循同样的规则。(斩风注: 这里的比较仅指相等和不等。大于小于不包含在此类数组长度类型必须相同)

1
2
3
4
5
6
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

slice

一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。(斩风注:slice可能是数组的中间部分。)长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。
复制一个slice只是对底层的数组创建了一个新的slice别名。
和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。不过标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较。
一个零值的slice等于nil。一个nil值的slice并没有底层数组。一个nil值的slice的长度和容量都是0,但是也有非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。与任意类型的nil值一样,我们可以用[]int(nil)类型转换表达式来生成一个对应类型slice的nil值。

1
2
3
4
var s []int    // len(s) == 0, s == nil
s = nil // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{} // len(s) == 0, s != nil

如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。
内置的append函数可能使用比appendInt更复杂的内存扩展策略。因此,通常我们并不知道append调用是否导致了内存的重新分配,因此我们也不能确认新的slice和原始的slice是否引用的是相同的底层数组空间。同样,我们不能确认在原先的slice上的操作是否会影响到新的slice。因此,通常是将append返回的结果直接赋值给输入的slice变量。

练习 4.3: 重写reverse函数,使用数组指针代替slice。

1
2
3
4
5
func reverseV2(s *[10]int) {
for i, j := 0, 9; i < j; i, j = i+1, j-1 {
(*s)[i], (*s)[j] = (*s)[j], (*s)[i]
}
}

练习 4.4: 编写一个rotate函数,通过一次循环完成镟转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func forwardRotate(s []int, x int) {
mid := x
k := mid
for i := 0; i < len(s); i++ {
s[i], s[k] = s[k], s[i]
k++
if i == mid {
if k == len(s) {
return
}
mid = k
} else if k == len(s) {
k = mid
}
}
}

练习 4.5: 写一个函数在原地完成消除[]string中相邻重复的字符串的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
func noRepeatSide(s []string) []string {
count := len(s)
for i := range s {
if i == len(s)-1 {
return s
}
if s[i] == s[i+1] {
s = append(s[:i], s[i+1:]...)
count--
}
}
return s
}

Map

虽然浮点数类型也是支持相等运算符比较的,但是将浮点数用做key类型则是一个坏的想法,正如第三章提到的,最坏的情况是可能出现的NaN和任何浮点数都不相等。对于V对应的value数据类型则没有任何的限制.
map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作.
map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它们的行为和一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常.在向map存数据前必须先创建map。
和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较。要判断两个map是否包含相同的key和value,我们必须通过一个循环实现.
如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用==或!=运算符进行比较.
Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。下面的代码中,Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构体,同时Circle类型被嵌入到了Wheel结构体。

1
2
3
4
5
6
7
8
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}

得益于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:

1
2
3
4
5
var w Wheel
w.X = 8 // equivalent to w.Circle.Point.X = 8
w.Y = 8 // equivalent to w.Circle.Point.Y = 8
w.Radius = 5 // equivalent to w.Circle.Radius = 5
w.Spokes = 20

为什么要嵌入一个没有任何子成员类型的匿名成员类型呢?答案是匿名类型的方法集。简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一个有简单行为的对象组合成有复杂行为的对象。组合是Go语言中面向对象编程的核心.