继《Mastering Go》和《Concurrency in Go》[1]之后,这是我精读的第 3 本 Go 主题的英文书了。全书 390+ 页,从开始读到全部读完,快 2 个月了,😓。
前不久曹大连接发了几个关于《100 mistakes》的视频,多猜他大都是看看标题,看看代码,就知道要说什么了,并且很快就跳过去,速度飞快。我开始设想的是除了读懂内容,还想练习一下英语阅读,慢就慢吧。不过,我过后也确实加快了速度,毕竟人家半小时的进度我要两周,稍微有点离谱。
简单谈一下这本书:全书“凑”了 100 个关于 Go 的错误。有些是非常经典且常见的错误,例如在 for 循环中保存迭代变量的指针、并发 append slice 等等,书中做了非常详细的讲述。另外有一些错误则见得不多,有凑数的嫌疑,例如很多错误是不知道 xxx、不懂 xxx……读来稍微有点别扭。还有一些瑕疵的地方是第 8 章关于 M 的描述是错误的……
关于书名,作者还找了几个为什么要从 mistakes 中学习的理由:我们印象最深的知识点一定是在犯错的场景下学到的。
Tell me and I forget. Teach me and I remember. Involve me and I learn.
我们最近正在组织这本书的翻译,估计明年 5 月左右能上市,不过还是建议大家读读英文版。
以下是我在读书的过程中所做的一些笔记,记下我认为今后可能会遇到的坑。
Go 很简单,但不容易掌握
Go is simple but not easy.
简单意味着易懂,Go 语法基本上花 2 小时就能全部看完。但是要想掌握它、写好它却不容易。比如,goroutine 和 channel 该简单了吧,但是使用 channel 出错的 case 数不胜数。
之前有篇讲 Concurrency bugs 的论文《Understanding Real-World Concurrency Bugs in Go》[2]说:尽管人们普遍认为通过 channel 来传递消息更少出错误,但是论文里研究的 bug 表明,正好相反,用 mutex 才更少出错。
The bigger the interface, the weaker the abstraction
Rob Pike说:The bigger the interface, the weaker the abstraction。当一个接口的方法越多,它的抽象能力越弱。像接口 Reader/Writer 为何很强大,因为它们就只有一个方法。
他还说:Don’t design with interfaces, discover them. 意思就是只有在实现过程中发现需要 interface 时才需要定义。是自下而上的过程,而非相反。
net 包和 net/http 包并没有层级关系
可以认为是两个不同的包,它们仅仅是文件位置有层级关系而已。
包名要反映这个包能提供什么能力,而不是它包含了哪些内容。
函数名反映它做了什么,而不是怎么做。虽然命名一直是编程界的难题,但不断尝试好的命名也是必要的。日常的 util, common, base 这些包名其实并不好。任何对外暴露的内容:包、函数、方法、变量都应该给出说明。
nil slice 的几个特点
不分配内存。对于一个函数的返回值而言,返回 nil slice 比 emtpy slice 要更好。
在 marshal 时,nil slice 是 null,而 empty slice 是 []。因此在使用相关库函数时,要特别注意这两者的区别。
nil slice 和 empty slice 不 equal。
以下代码中前 2 个是 nil slice,后两个不是。
copy 函数拷贝的元素数量是 min(len(dst), len(min))
初始化 map 时,指定一个长度
它能给 runtime 以提示,这样后续可以减少重新分配元素的开销。并且要注意:这个长度并不是说 map 只能放这么多元素,这里面有一个公式会计算。
map 的 buckets 数只会增,不会降。所以当在流量冲击后,map 的 buckets 数扩容到了一个新高度,之后即使把元素都删除了也无济于事。内存占用还是在,因为基础的 buckets 占用的内存不会少。
关于这一点,之前专门写过一篇Go map 竟然也会发生内存泄漏?[3]去讲,私以为比书里讲得更详细。
不要边遍历 map 边写入 key
在遍历 map 的过程中,新写入的 key 可能被遍历出来,也可能不被遍历出来,可能会与预期的行为不符,因此不要边遍历边写入。
下面这个例子输出的结果不确定:
func main() {
m := map[int]bool{
0: true,
1: false,
2: true,
}
for k, v := range m {
if v {
m[10+k] = true
}
}
fmt.Println(m)
}
break 可以作用于 for, select, switch
break 只能跳出一重循环,因此要注意,break 是否跳到了你预想的地方。可以用 break with label 来解决。毕竟标准库里也这样用了:
for 循环加指针,老司机也会掉的坑
在 for range