为什么要写这本书
因为Go语言在服务端的开发效率、服务性能有着不俗的表现,近几年,Go 的热度越来越高。国内外很多大公司都在大规模地使用Go。Google就不用说了,它是Go语言诞生的地方,其他公司如Meta(Facebook)、uber、腾讯、字节跳动、知乎、脉脉等都在拥抱和转向Go。用Go 语言开发的著名开源项目也非常多,如k8s、docker、etcd、consul,每个名字都是如雷贯耳。
随着市场对Go语言人才需求的不断增长,很多开发人员都从其他语言,如PHP、C 、Java等转投Go语言的怀抱。因为Go语言自身的特点和优势,这些转型的开发人员也能写出性能不错的代码。但是,由于没有深入系统地学习Go的底层原理,在某些场景下,因为不懂底层原理,无法快速定位问题、无法进行性能优化。
有些人说,语言并不重要,架构、技术选型这些才是根本。笔者觉得这个说法不完全对,架构、技术选型固然重要,但语言其实是开发人员每天都要打交道的东西,会用是远远不够的,只有用好、知其所以然才能更全面地发挥其威力。
笔者自己亲身经历的一个事故是关于Go 1.14之前调度器的一个坑:执行无限循环且没有函数调用的 goroutine无法被抢占,导致程序表现出死机。因为之前我对这个坑的原理已经非常熟悉了。所以在事故现场,时间就明确了原因。后续的工作就是排查问题代码,非常轻松。有些读者要问了,既然你知道了坑的原理,为何还会掉进去?我只能说Bug是必然存在的,只是发现的早晚而已。有Bug不可怕,怕的是发现Bug却无法定位出来。
当越来越多的开发人员都转向Go语言时,如何在众多求职者中脱颖而出便成了面试官和求职者共同面临的一个问题。早期从其他语言转过来的开发人员,以为只要简单学习Go语言,能写出可运行的代码就可以了。但现在竞争越来越激烈,懂原理和只会写代码的人马上就能被区分出来,那些抱残守缺,秉承会用就行的理念的求职者,除非你在其他方面有出色的能力,否则你在职场上的竞争力就会很低。
现在网上流传了很多看代码打印结果的题目,我想说的是,这是把人脑当成了编译器吗?面试不是背八股文,不是记语言点:不用记住Go语言里的运算符的优先级,不需要看出这个变量是否逃逸到了堆上,也不用背Go GC经历了哪些阶段……你只需要研究清楚它的原理,面试官问你什么问题就都能应对。
近一两年,笔者在中文世界论坛里发表了很多篇与Go源码阅读相关的文章,也是在写作本书的过程中做的功课。我通过看源码、做实验、请教大牛,对Go的理解逐渐加深。再去看很多文章就会感觉非常简单,为什么这些我都能掌握?因为我研究过,我知道原理是什么,所以也知道你想要说什么。
后,希望通过本书,能让你的Go水平真正上升一个台阶。
天道酬勤,与君共勉!
读者交流及本书勘误
由于篇幅有限,本书不可能涵盖Go的所有内容,但关键的内容都呈现出来了,读者有扩展阅读及资源获取需求,可加入猿媛之家读者服务QQ群(496588733)进行交流。
本书为读者提供了780分钟的Go核心知识点讲解,读者可登录网站https://golang.design/go- questions/获取,同时本书后续的勘误也将在该网站提供。读者也可以关注下方公众号进行批评指正。
本书的读者对象
无论你是面试官,还是求职者,这本书都能让你有所收获。另外,本书内容不仅仅是对面试有帮助,所有写Go的程序员都能从本书中有所收获。
致谢
在写作本书的过程中,和另一位学者欧长坤有很多交流讨论,欧长坤是在读博士,他对Go的理解非常深,他同时也是Go Contributor,我们的交流和讨论让我对很多问题有了更深入的理解,非常感谢。
我从Go夜读社区的分享中学到了很多东西。并且我本人也担任讲师,分享了三期Go相关的内容,很多观众都表示很有帮助。教是好的学,我本人的收获是多的。感谢Go夜读社区的发起者杨文和SIG小组成员。
另外,我和Go圈的很多博客作者也有很多交流,收获良多,在此一并感谢。
这两年,我在码农桃花源发表了很多文章,得到了很多读者的肯定,这也是我能不断写作的动力,感谢你们。
饶全成
前言
第1部分 语 言 基 础
第1章 逃逸分析/2
1.1 逃逸分析是什么/2
1.2 逃逸分析有什么作用/3
1.3 逃逸分析是怎么完成的/3
1.4 如何确定是否发生逃逸/4
1.5 Go与C/C 中的堆和栈是同一个概念吗/5
第2章 延迟语句/6
2.1 延迟语句是什么/6
2.2 延迟语句的执行顺序是什么/7
2.3 如何拆解延迟语句/9
2.4 如何确定延迟语句的参数/10
2.5 闭包是什么/11
2.6 延迟语句如何配合恢复语句/11
2.7 defer链如何被遍历执行/13
2.8 为什么无法从父goroutine恢复子goroutine的panic/18
第3章 数据容器/20
3.1 数组与切片/20
3.1.1 数组和切片有何异同/20
3.1.2 切片如何被截取/20
3.1.3 切片的容量是怎样增长的/23
3.1.4 切片作为函数参数会被改变吗/27
3.1.5 内建函数make和new的区别是什么/28
3.2 散列表map/29
3.2.1 map 是什么/29
3.2.2 map 的底层实现原理是什么/30
3.2.3 map 中的 key 为什么是无序的/50
3.2.4 map 是线程安全的吗/50
3.2.5 float类型可以作为map的key吗/50
3.2.6 map 如何实现两种 get 操作/52
3.2.7 如何比较两个 map 是否相等/53
3.2.8 可以对 map 的元素取地址吗/54
3.2.9 可以边遍历边删除吗/54
第4章 通道/55
4.1 CSP是什么/55
4.2 通道有哪些应用/56
4.3 通道的底结构/57
4.3.1 数据结构/57
4.3.2 创建过程/58
4.3.3 接收过程/60
4.3.4 发送过程/67
4.3.5 收发数据的本质/72
4.4 通道的关闭过程发生了什么/74
4.5 从一个关闭的通道里仍然能读出数据吗/75
4.6 如何优雅地关闭通道/76
4.7 关于通道的happens-before有哪些/79
4.8 通道在什么情况下会引起资源泄漏/81
4.9 通道操作的情况总结/81
第5章 接口/82
5.1 Go接口与C 接口有何异同/82
5.2 Go语言与鸭子类型的关系/82
5.3 iface和eface的区别是什么/84
5.4 值接收者和指针接收者的区别/86
5.4.1 方法/86
5.4.2 值接收者和指针接收者/87
5.4.3 两者分别在何时使用/89
5.5 如何用interface实现多态/89
5.6 接口的动态类型和动态值是什么/91
5.7 接口转换的原理是什么/93
5.8 类型转换和断言的区别是什么/96
5.9 如何让编译器自动检测类型是否实现了接口/101
第2部分 语 言 类 库
第6章 unsafe/104
6.1 如何利用unsafe包修改私有成员/104
6.2 如何利用unsafe获取slice和map的长度/105
6.3 如何实现字符串和byte切片的零复制转换/106
第7章 context/108
7.1 context是什么/108
7.2 context有什么作用/108
7.3 如何使用context/109
7.3.1 传递共享的数据/109
7.3.2 定时取消/111
7.3.3 防止 goroutine 泄漏/111
7.4 context底层原理是什么/112
7.4.1 接口/113
7.4.2 结构体/114
第8章 错误/124
8.1 接口error是什么/124
8.2 接口error有什么问题/125
8.3 如何理解关于error的三句谚语/126
8.3.1 视错误为值/126
8.3.2 检查并优雅地处理错误/128
8.3.3 只处理错误一次/130
8.4 错误处理的改进/131
第9章 计时器/133
9.1 Timer底层数据结构为什么用四叉堆而非二叉堆/133
9.2 Timer曾做过哪些重大的改进/134
9.3 定时器的使用场景有哪些/134
9.4 Timer/Ticker 的计时功能有多准确/134
9.5 定时器的实现还有其他哪些方式/137
第10章 反射/140
10.1 反射是什么/140
10.2 什么情况下需要使用反射/140
10.3 Go语言如何实现反射/140
10.3.1 types 和 interface/141
10.3.2 反射的基本函数/144
10.3.3 反射的三大定律/149
10.4 如何比较两个对象是否完全相同/149
10.5 如何利用反射实现深度拷贝/151
第11章 同步模式/154
11.1 等待组 sync.WaitGroup 的原理是什么/154
11.2 缓存池 sync.Pool/157
11.2.1 如何使用sync.Pool/157
11.2.2 sync.Pool 是如何实现的/162
11.3 并发安全散列表 sync.Map/174
11.3.1 如何使用 sync.Map/175
11.3.2 sync.Map 底层如何实现/176
第3部分 高 级 特 性
第12章 调度机制/184
12.1 goroutine 和线程有什么区别/184
12.2 Go sheduler 是什么/184
12.3 goroutine 的调度时机有哪些/186
12.4 M:N模型是什么/187
12.5 工作窃取是什么/187
12.6 GPM底层数据结构是怎样的/188
12.7 scheduler 的初始化过程是怎样的/193
12.8 主 goroutine 如何被创建/207
12.9 g0栈和用户栈如何被切换/212
12.10 Go schedule循环如何启动/217
12.11 goroutine如何退出/221
12.12 schedule循环如何运转/226
12.13 M如何找工作/227
12.14 系统监控sysmon后台监控线程做了什么/237
12.14.1 抢占进行系统调用的P/240
12.14.2 抢占长时间运行的P/243
12.15 异步抢占的原理是什么/247
第13章 内存分配机制/252
13.1 管理内存的动机是什么,通常涉及哪些组件/252
13.1.1 内存管理的动机/252
13.1.2 内存管理运行时的组件/252
13.1.3 内存的使用状态/253
13.2 Go语言中的堆和栈概念与传统意义上的堆和栈有什么区别/255
13.3 对象分配器是如何实现的/255
13.3.1 分配的基本策略/256
13.3.2 对象分配器的基本组件和层级/256
13.3.3 对象分配的产生条件和入口/259
13.3.4 大对象分配/261
13.3.5 小对象分配/262
13.3.6 微对象分配/264
13.4 页分配器是如何实现的/265
13.4.1 页的分配/265
13.4.2 跨度的分配/266
13.4.3 非托管对象与定长分配器/267
13.5 与内存管理相关的运行时组件还有哪些/269
13.5.1 执行栈管理/269
13.5.2 垃圾回收器和拾荒器/271
13.6 衡量内存消耗的指标有哪些/272
13.7 运行时内存管理的演变历程/278
13.7.1 演变过程/278
13.7.2 存在的问题/279
第14章 垃圾回收机制/280
14.1 垃圾回收的认识/280
14.1.1 垃圾回收是什么,有什么作用/280
14.1.2 根对象到底是什么/280
14.1.3 常见的垃圾回收的实现方式有哪些,Go语言使用的是什么/281
14.1.4 三色标记法是什么/281
14.1.5 STW是什么意思/282
14.1.6 如何观察 Go 语言的垃圾回收现象/283
14.1.7 有了垃圾回收,为什么还会发生内存泄漏/286
14.1.8 并发标记清除法的难点是什么/288
14.1.9 什么是写屏障、混合写屏障,如何实现/289
14.2 垃圾回收机制的实现细节/291
14.2.1 Go语言中进行垃圾回收的流程是什么/291
14.2.2 触发垃圾回收的时机是什么/292
14.2.3 如果内存分配速度超过了标记清除的速度怎么办/294
14.3 垃圾回收的优化问题/295
14.3.1 垃圾回收关注的指标有哪些/295
14.3.2 Go 的垃圾回收过程如何调优/295
14.3.3 Go的垃圾回收有哪些相关的API,其作用分别是什么/305
14.4 历史及演进/305
14.4.1 Go 历史各个版本在垃圾回收方面的改进/305
14.4.2 Go在演化过程中还存在哪些其他设计,为什么没有被采用/307
14.4.3 Go语言中垃圾回收还存在哪些问题/307
结束语/310