Statistics
5
Views
0
Downloads
0
Donations
Support
Share
Uploader

高宏飞

Shared on 2026-04-14

Author左书祺

No description

Tags
No tags
Publish Year: 2021
Language: 英文
File Format: PDF
File Size: 26.8 MB
Support Statistics
¥.00 · 0times
Text Preview (First 20 pages)
Registered users can read the full content for free

Register as a Gaohf Library member to read the complete e-book online for free and enjoy a better reading experience.

TURING 图灵原创 源自30万读者追更的博文 近200幅精美全彩配图 生动图解Go底层原理 设计与实切 左书祺(@Draven)著 回中国工信出版集团钏睥嘿
TURING 图灵原创 H3 E3 设计与实现 左书祺(@Draven)著 人民邮电出版社 北京
图书在版编目(CIP)数据 G。语言设计与实现/左书祺著.-北京:人民邮 电出版社,2021.11 (图灵原创) ISBN 978-7-115-57661-3 I.①g- n.①左…m.①程序语言一程序设计 IV.①TP312 中国版本图书馆CEP数据核字(2021)第213405号 内容提要 本书基于在读者之间广为传阅的同名开源电子书CG。语言设计与实现》,是难得一见的G。语言进阶 图书。 书中结合近200幅生动的全彩图片,配上详尽的文字剖析与精选源代码段,为读者奉上了异彩纷呈、 系统完善的G。语言解读。本书内容分为9章:调试源代码、编译原理、数据结构、语言特性、常用关键字、 并发编程、内存管理、元编程和标准库,几乎涵盖了 Go语言从编译到运行的方方面面。书中的代码片段 基于Go 1.15.通过阅读本书,读者不仅能够深入理解Go语言的实现细节,而且可以深刻认识设计背后的 原理,同时提升阅读源代码的技能. 本书适合所有G。语言工程师,以及有其他语言基础、想深入理解Go语言的开发者,此外,本书也适 合作为Go语言培训参考书。 ♦著 左书祺(@Draven) 责任编辑刘美英 责任印制周昇亮 ♦人民邮电出版社出版发行 北京市丰台区成寿寺路11号 邮编 100164 电子邮件 315@ptpress.com.cn 网址 https7/www.ptpress.com.cn 天津市豪迈印务有限公司印刷 ♦开本:800x1000 1/16 印张:2625 2021年11月第1版 字数:620千字 2021年11月天津第1次印刷 定价:139.80元 读者服务热线:(010)84084456-6009印装质量热线:(010)81055316 反盗版热线:(010)81055315 广告经菅许可证:京东市监广登字20170147号
, 1/■ —1— 刖舌 Go语言的历史和现状 Go语言诞生于2009年,发展到今天已经有十多年历史了。在G。语言诞生之前,大多数公司会 选择C和C++作为系统编程语言。这两门语言虽然历史悠久,但是它们的开发和编译速度一直为人 诟病;语言本身工具链的不完善也让开发体验非常糟糕。与此同时,随着计算机硬件的发展,多线程 编程变得越来越普遍,已有的多数编程语言在使用线程能力时很烦琐。在这种情形下,一门开发和 编译速度快、具有优异的现代工具链并且原生支持多线程编程的语言被谷歌的开发者带到了台前。 Go语言的出身让其从诞生之初就广受关注。它的三位主要创始人Robert Griesemer, Rob Pike 和Ken Thompson选择了极其简单的设计,对编程稍微有些经验的开发者就能在短时间内快速上手, 其内置的Goroutine和Channel等特性也可以让开发者轻松利用机器上的多个CPU。虽然Go语言本 身的出身和设计都堪称优秀,但是这门语言要想走进更多人的视野,被广大开发者熟知,仍然需要 一些契机。2010年前后,容器技术作为基础设施开始登上历史舞台。2013年,Docker作为明星级容 器开源项目发布,随后成为Go语言发展的重要助推器。Docker社区选择Go作为开发语言让更多人 看到了这门语言并认识到:Go有足够的能力实现生产级的应用程序。 目前,随着容器和云原生的大热,Go语言在国内外社区越来越受欢迎一Kubemetes、etcd和 Prometheus等著名开源框架都是使用Go语言开发的;近年来热门的微服务架构和云原生技术也为 G。语言社区注入了新活力。 Go语言吉祥物Gopher® 我的Go语言之路 在当前的日常开发工作中,我使用的主力编程语言就是Go。虽然Go语言没有Lisp系语言的开 ①Renee French, CC BY-SA3.0 (本书内文出现的所有Gopher的设计者为艺术家Renee French,封面上的Gopher也参 考了 Renee的设计)。
iv |前吉 发效率和强大的表达能力,却是一门非常容易使用并且适合大规模运用的工程语言,这也是我学习 和使用G。语言的主要原因。 我刚接触编程还是上初中那会儿。那时我沉迷于游戏,为了转移我的注意力,父母想办法弄来 了 C语言的编程书。初次接触编程确实令人震撼— 能够在一个黑框里操作计算机完成特定指令是 一件看起来很神奇的事情。 上大学之后我学习了 iOS客户端开发。那时候使用Objective-C在手机上编写小程序是相当有 成就感的事情。大学四年,除了学习客户端开发,我还学习了几种编程语言,比如Ruby、Lisp、 Haskell等。我到今天都非常喜欢Ruby,也深深地被Ruby社区所影响。 我是从2018年才真正开始学习和使用Go语言的。说实话,刚开始接触Go语言,我是有些排 斥和拒绝的,曾经一度认为Go语言中GOPATH的设计非常诡异一毕竟有使用Ruby的"快乐编 程”经历在先,一开始很难接受设计如此“简陋”的语言。当时,我对Go语言的总体认知是:简单 的语法导致表达能力低下,并且严重影响了开发效率。 然而,随着对G。语言的深入学习和理解,我的观念发生了质的改变。正是因为Go语言的简单 性,它编写的应用程序相对容易维护,哪怕是对这门语言不熟悉的开发者,也大概率能写出让其他 人看得懂的代码。而我一直认为,写出易于维护的代码是一件极其艰难的工作,我们应该珍惜每一 位能够写出易于测试、易于维护的代码的工程师。 G。语言不是一门完美的编程语言,它在选择使用运行时解决调度和内存管理等问题的同时,一 定会放弃执行上的部分性能。事实上,它的性能也确实无法与C++匹敌。但它可以在保证性能的前 提下,利用内置的代码格式化工具、依赖管理工具以及更快的编译速度解放工程师的生产力,让大 家有更多时间思考业务逻辑,而不是如何管理依赖和编译程序。 虽然目前Go语言还有很多问题,但是其本身以及周边工具的不断完善、社区活力的不断提升, 都让我坚定地认为这门语言未来的发展会越来越好。 写作缘由 目前市面上分析Go语言实现的图书较少,其中多数偏重于Go语言的基础和实战°分析Go语 言实现的博客不在少数,但它们存在以下两个问题: •大量博客成段展示源代码的实现细节,没有深入剖析背后的原理,可读性较差; •少量博客的质量较高,对Go语言的一些模块讲解得比较深入,但不够系统,无法形成足够丰富、 完整的内容。 除了上述两个原因,我认为,阅读Go语言源代码和理解Go语言发展史是帮助我们深入理解 Go语言最有效的途径。本书致力于将这一途径变得更高效。 本书基于我的开源电子书《Go语言设计与实现》,是我深入学习Go的过程中对这门语言底层设 计与实现原理的全部心得与体会。不少读者见证了电子书的诞生过程,其间大家一起学习,一起讨
前言I V 论。回复大家的问题一方面帮助我个人精进了 G。语言知识,另一方面还纠正了内容中的不少细节问 题。借着纸质书出版的机会,感谢大家! 写作理念 在讲解语言设计与实现的书中,这本可能是你见过的最易读的图书之一。本书的写作遵循以下 理念,以期竭尽所能为大家提供高质量的内容和良好的阅读体验。 •以图配文:全书包含近200幅全彩配图,核心知识点以源代码+解释文字+配图的方式展示。色 彩丰富和清晰明了的配图能够提供更多上下文,帮助大家快速理解不同模块之间的关系和作用, 进而深刻理解Go语言的实现细节。 •精简源代码:删减源代码中的无关细节,精准分析核心代码的实现逻辑,帮助大家翻越阅读Go语 言源代码的障碍。 •注重演进:分析Go语言社区中贡献者对相关特性的讨论,并通过追踪提交了解代码的更新过程,- 言以蔽之,通过历史的演进和社区讨论剖析设计背后的决策和原因,让大家知其然,更知其所以然。 王要内容 本书共计9章内容:调试源代码、编译原理、数据结构、语言特性、常用关键字、并发编程、 内存管理、元编程和标准库,几乎涵盖了 Go语言从编译到运行的方方面面。书中的代码片段基于 Go 1.15o大家可以按照准备工作、基础知识、核心知识和进阶知识的划分顺序来学习。下面以表式 思维导图的方式展示了本书的主要内容。
vi |前言 各章内容简单介绍如下。 •第1章 调试源代码:介绍调试和编译Go语言源代码和中间代码的方法。 •第2章 编译原理:按照从词法分析、语法分析、类型检查、中间代码生成到机器码生成的顺序, 介绍Go语言源代玛的编译过程,为我们理解Go语言关键字和语言特性的实现打下基础。 •第3章 数据结构:介绍Go语言中最常见的容器数据结构,其中包括数组、切片、哈希表和字符 串,会深入介绍切片的复制和扩容、哈希表的读写以及字符串的拼接等常见操作。 •第4章 语言特性:介绍Go语言中的函数调用惯例、接口的实现原理、反射的三大法则以及具体 实现。 •第5章 常用关键字:介绍使用Go语言常用关键字时会遇到的一些现象,从编译原理和运行时两 个角度分析它们的具体实现。 •第6章 并发编程:介绍Go语言并发编程中常用的结构和概念,例如上下文、同步原语、计时 器、Channel和调度器等■<> •第7章内存管理:内存管理是编程语言的重要组成部分,本章会分别介绍堆空间和栈空间的内 存管理,前者会从内存分配器和垃圾收集器两个维度介绍。 •第8章 元编程:介绍Go语言的元编程能力,教大家通过插件系统和代码生成达到使用更少代码 实现更多功能的目的。 •第9章 标准库:介绍Go语言的常见标准库,涉及JSON解析、HTTP请求和响应处理、数据库 操作,通过学习标准库了解Go语言更多的使用技巧。 通过阅读本书,读者不仅能够深入理解Go语言的实现细节,而且可以深刻认识设计背后的原 因,同时提升阅读源代码的技能。 目标读者 •学过Go语言、想要理解其背后设计与实现的开发者; •有过其他语言开发经验、想要学习Go语言的开发者。 互动与勘误 如果你对本书内容有疑问,可通过图灵社区本书主页e提交勘误;如果想跟我互动,可以前往开 源电子书《Go语言设计与实现》官方博客draveness.me/golang/的对应章节留言,我会尽快回复= ① 请见:ituring.cn/book/29110
目录 第1章调试源代码................... 1 1.1 Go语言源代码................... 1 1.2编译源代码......................2 1.3中间代码........................3 1.4小结............................4 编译原理..................... 5 2.1编译过程........................5 2.1.1预备知识................... 5 2.1.2编译四阶段................. 7 2.1.3编译器入口................ 10 2.1.4 小结..................... 11 2.1.5延伸阅读.................. 12 2.2词法分析和语法分析.............12 2.2.1词法分析.................. 12 2.2.2语法分析.................. 17 2.2.3 小结......................26 2.2.4延伸阅读.................. 26 2.3类型检查............. 26 2.3.1强弱类型........ 26 2.3.2静态类型与动态类型…........ 27 2.3.3执行过程.................. 28 2.3.4 小结......................34 2.4中间代码生成...................34 2.4.1 概述..................... 34 2.4.2配置初始化................ 35 2.4.3遍历和替换................ 37 2.4.4 SSA 生成................. 40 2.4.5 小结..................... 44 2.5机器码生成.....................44 2.5.1指令集架构................ 44 2.5.2机器码生成................ 45 2.5.3 小结......................49 2.5.4 延伸阅读.................. 50 翳3章数据结襁.................... 51 3.1数组...........................51 3.1.1 概述......................51 3.1.2初始化....................52 3.1.3访问和赋值................ 55 3.1.4 小结......................57 3.1.5延伸阅读.................. 58 3.2切片...........................58 3.2.1数据结构.................. 58 3.2.2 初始化....................59 3.2.3访问元素.................. 63 3.2.4追加和扩容................ 64 3.2.5复制切片.................. 67 3.2.6 小结......................68 3.2.7延伸阅读.................. 68 3.3哈希表.........................68 3.3.1 原理.................. 69 3.3.2数据结构.................. 72 3.3.3初始化....................74 3.3.4读写操作.................. 77
viii | 前言 3.3.5 小结......................88 3.3.6延伸阅读.................. 88 3.4字符串........................88 3.4.1 够结构.................. 89 3.4.2解析过程.................. 89 3.4.3 拼接......................91 3.4.4类型转换.................. 93 3.4.5 小结......................95 3.4.6延伸阅读.................. 95 第4章语言特性.................... 96 4.1 函数调用.......................96 4.1.1调用惯例.................. 96 4.1.2参数传递................. 101 4.1.3 小结.....................104 4.1.4延伸阅读................. 104 4.2 接口.......................... 105 4.2.1 概述..................... 105 4.2.2数据结构................. 1H 4.2.3类型转换................. 113 4.2.4类型断言................. 116 4.2.5动态派发................. 118 4.2.6 小结..................... 122 4.2.7延伸阅读................. 122 4.3 反射.......................... 123 4.3.1三大法则................. 124 4.3.2类型和值................. 127 4.3.3更新变量................. 129 4.3.4实现协议................. 130 4.3.5方法调用................. 132 4.3.6 小结..................... 135 4.3.7延伸阅读................. 135 篥5章常用关蹙字................ 136 5.1 for 和 range.....................................136 5.1.1 现象.................... 137 5.1.2经典循环.................140 5.1.3范围循环................. 141 5.1.4 小结.................... 147 5.2 select............................................... 148 5.2.1 现象.................... 148 5.2.2数据结构................. 151 5.2.3实现原理................. 151 5.2.4 小结.................... 160 5.2.5延伸阅读................ 161 5.3 defer.................................................仙 5.3.1 现象.................... 161 5.3.2数据结构.................163 5.3.3执行机制.................164 5.3.4堆中分配.................164 5.3.5栈上分配.................168 5.3.6开放编码.................169 5.3.7 小结................173 5.3.8延伸阅读.................174 5.4 panic 和 recover............................174 54.1 现象.................... 175 5.4.2数据结构.................177 5.4.3程序崩溃.................178 5.4.4崩溃恢复................. 179 5.4.5 小结.................... 181 5.4.6延伸阅读................. 181 5.5 make new.................................181 5.5.1 make......................................... 182 5.5.2 new........................................... ig3 5.5.3 小结.................... 184
前言I ix 第6章并发编程..................怡5 6.1 上下文.......................185 6.1.1设计原理................. 186 6.1.2默认上下文............... 187 6 1.3取消信号................. 188 6.1.4传值方法................. 192 6.1.5 小结..................... 192 6.1.6延伸阅读................. 192 6.2同步原语与锁................. 193 6.2.1基本原语................. 193 6.2.2扩展原语................. 209 6.2.3 小结.....................218 6.2.4延伸阅读................. 218 6.3计时器....................... 219 6.3.1设计原理................. 219 6.3.2数据结构................. 222 6.3.3状态机...................223 6.3.4触发计时器............... 229 6.3.5 小结.....................231 6.3.6延伸阅读................. 232 6.3.7历史变更................. 232 6.4 Channel..............................................232 6.4.1设计原理................. 232 6.4.2数据结构................. 234 6.4.3 创建 Channel............................. 235 6.4.4发送数据................. 237 6.4.5接收数据................. 240 6.4.6 关闭Channel............................. 245 6.4.7 小结.....................246 6.4.8延伸阅读................. 246 6.5 调度器....................... 246 6.5.1设计原理................. 247 6.5.2数据结构................. 255 6.5.3调度器启动............... 260 6.5.4 创建 Goroutine.......................... 261 6.5.5调度循环................. 266 6.5.6触发调度................. 269 6.5.7线程管理................. 274 6.5.8 小结.................... 276 6.5.9延伸阅读................. 276 6.6网络轮询器....................276 6.6.1设计原理................. 276 6.6.2数据结构................. 280 6.6.3多路复用................. 281 6.6.4 小结.....................288 6.6.5延伸阅谟................. 289 6.7系统监控......................289 6.7.1设计原理................. 289 6.7.2监控循环................. 289 6.7.3 小结.....................296 内存管理...................2S7 7.1内存分配器....................297 7.1.1设计原理................. 297 7.1.2内存管理组件..............304 7.1.3内存分配................. 317 7.1.4 小结.....................322 7.1.5延伸阅读................. 322 7.1.6历史变更................. 322 7.2垃圾收集器....................323 7.2.1设计原理................. 323 7.2.2演进过程................. 331 7.2.3实现原理................. 334 7.2.4 小结.....................358 7.2.5延伸阅读................. 358 7.3 栈空间管理....................358 7.3.1设计原理................. 359 7.3.2栈操作...................363
X I前言 7.3.3 小结.....................369 7.3.4延伸阅读................. 370 第8章元编程..................... 371 8.1 插件系统......................371 8.1.1设计原理................. 371 8.1.2 动态库...................373 8.1.3 小结.....................376 8.1.4 延伸阅读................. 376 8.2代码生成......................377 8.2.1设计原理................. 377 8.2.2代码生成................. 377 8.2.3 小结.....................382 第9章榇准库..................... 383 9.1 JSON..................................................383 9.1.1设计原理................. 383 9.1.2序列化...................385 9.1.3反序列化................. 389 9.1.4 小结.....................392 9.2 HTTP.................................................. 392 9.2.1设计原理................. 393 9.2.2 客户端...................395 9.2.3月艮务端...................402 9.2.4 小结.....................406 9.3数据库........................406 9.3.1设计原理................. 406 9.3.2 驱动接口................. 407 9.3.3 小结.....................410
第1章调试源代码 本书的目的不仅仅是从理论层面介绍Go语言的设计,还要深入Go语言的源代码逐行分析其实 现原理。而各位要想理解Go语言的实现原理,动手实践是必不可少的工作,也就是调试Go语言源 代码。 本章主要介绍调试Go语言源代码的方法,其中包括如何修改和编译源代码与中间代码的生成两 部分。 1.1 语言源代码 作为开源项目,Go语言的源代码很容易获取。Go语言有着非常复杂的项目结构和庞大的代码 库,今天的Go语言中差不多有150万行源代码,其中包含将近140万行的Go语言代码。我们可以 使用如下命令查看项目中代码的行数: $ cloc src 5988 text files. 5875 unique files. 1165 files ignored. github.com/AlDanial/cloc v 1.78 T=6.96 s (693.7 files/s, 274805.2 lines/s) Language files blank comment code Go 4199 139910 221375 1398357 Assembly 486 12784 19137 106699 C 64 718 562 4587 J SON 12 0 0 1712 SUM: 4828 154344 242395 1515787 随着Go语言的不断演进,整个代码库也会随着时间不断变化,所以上面的统计结果每天都会有 所不同。虽然该项目有着庞大的代码库,但要调试Go语言也不是不可能,只要我们掌握合适的方法 并且对Go语言的标准库有一定了解即可。下面介绍一些编译和调试Go语言的方法。
2 |第1章调试源代码 1.2编译源代码 假设我们想修改Go语言中常用方法fmt.Printin的实现,实现如下所示的功能:在打印字符串之 前先打印任意其他字符串。我们可以将该方法的实现修改成如下所示的代码片段,其中printin是 Go语言运行时提供的内置方法,它不需要依赖任何包即可向标准输出打印字符串: func Println(a ...interface{}) (n int, err error) { println("draven") return Fprintln(os.Stdout, a...) } 当我们修改了 Go语言的源代码项目后,可以使用仓库中提供的脚本来编译生成G。语言的二进 制文件以及相关工具链: $ ./src/make.bash Building Go cmd/dist using /usr/local/Cellar/go/1.14.2_1/libexec. (go1.14.2 darwin/amd64) Building Go toolchainl using /usr/local/Cellar/go/1.14.2_1 /libexec. Building Go bootstrap cmd/go (go_bootstrap) using Go toolchainl. Building Go toolchain2 using go_bootstrap and Go toolchainl. Building Go toolchain3 using go_bootstrap and Go toolchain2. Building packages and commands for darwin/amd64. Installed Go for darwin/amd64 in /Users/draveness/go/src/github.com/golang/go Installed commands in /Users/draveness/go/src/github.com/golang/go/bin ./src/make.bash脚本会编译Go语言的二进制文件、工具链以及标准库和命令并将源代码和 编译好的二进制文件移动到对应位置上°如上述代码所示,编译好的二进制文件会存储在$GOPATH/ src/github. com/golang/go/bin目录中,这里需要使用绝对路径来访问并使用它: $ cat main.go package main import "fmt" func main() { fmt.Println("Hello World") } $ $GOPATHZsrc/github.com/golang/go/bin/go run main.go draven Hello World 上述命令成功地调用了我们修改后的fmt.Println函数,而这时如果直接使用go run main, go.很可能会使用包管理器安装的Go语言二进制文件,得不到期望的结果。
1.3中间代码I 3 1.3中间代码 Go语言的应用程序在运行之前需要先编译成二进制文件,在编译过程中会经过中间代码生成阶 段。Go语言编译器的中间代码具有静态单赋值的特性,后文会介绍,这里我们只需要知道这是中间 代码的一种表示方式。 很多Go语言开发者知道可以使用下面的命令将Go语言的源代码编译成汇编语言,然后通过汇 编语言分析程序的具体执行过程: $ go build -gcflags -S main.go rel 22+4 t=8 os.(*file).close+0 M,* .main ST EXT size=137 args=0x0 locals=0x58 0x0000 00000 (main.go:5) TEXT .main(SB), ABIIntemal, $88-0 0x0000 00000 (main.go:5) MOVQ (TLS), CX 0x0009 00009 (main.go:5) CMPQ SP, 16(CX) rel 5+4 t=17 TLS+O rel 40+4 t-16 type.string+O rel 52+4 t«16 ”"..stmp_0+0 rel 64+4 t=16 os.Stdout+0 rel 71+4 t=16 go.itab.*os.File,io.Writer+0 rel 113+4 t=8 fmt.Fprintln+O rel 128+4 t=8 runtime.morestack_noctxt+0 然而上述汇编代码只是Go语言编译的结果,作为Go语言开发者,我们已经能够通过上述结果 分析程序的性能瓶颈,如果想了解Go语言更详细的编译过程,可以通过下面的命令获取汇编指令的 优化过程: $ GOSSAFUNC=main go build main.go # runtime dumped SSA to /usr/local/Cellar/go/1.14.2_1/libexec/src/runtime/ssa.html # command-line-arguments dumped SSA to ./ssa.html 上述命令会在当前文件夹下生成一个ssa.html文件,打开该文件后就能看到汇编代码优化的 每一个步骤,如图1-1所示。 下图中的HTML文件是可交互的,当我们点击网页上的汇编指令时,页面会使用相同的颜色在 SSA中间代码生成的不同阶段标识出相关代码行,方便开发者分析编译优化过程。
4 |第1章调试源代码 图1-1 SSA示例 1.4小结 掌握调试和自定义G。语言二进制文件的方法可以帮助我们快速验证对Go语言内部实现的猜 想,通过简单的println函数可以调试Go语言的源代码和标准库;而如果我们想研究源代码的详细 编译优化过程,可以使用上面提到的SSA中间代码深入研究Go语言的中间代码以及编译优化方式。 不过,只要我们想了解G。语言的实现原理,阅读源代码就是绕不开的过程。
第2章编译原理 Go语言是一门需要编译才能运行的编程语言,也就是说,代码在运行之前需要通过编译器生成 二进制机器码,包含二进制机器码的文件才能在目标机器上运行。如果我们想了解Go语言的实现原 理,理解其编译过程就是一个无法绕过的事情。 本章首先为大家构建了一个较高层面的视角— 从Go语言编译器执行的几个步骤出发,介绍理 解编译过程需要的一些预备知识、Go语言编译器的相关代码;随后会按照词法和语法分析、类型检 查、中间代码生成和机器码生成几个阶段分别介绍编译器不同阶段的实现原理。 2.1编译过程 本节将分两部分介绍编译过程相关内容,第一部分是预备知识,包括编译器中的一些常见术语, 如抽象语法树、静态单赋值和指令集;第二部分会从理论层面依次介绍编译过程的四个阶段并点出 G。语言编译器的入口。 2.1.1预备知识 想深入了解Go语言的编译过程,需要提前了解编译过程中涉及的一些术语和专业知识。这些知 识其实在日常工作和学习中一般用不到,但是对于理解编译过程和原理还是非常重要的。下面简单 挑选几个重要的概念进行介绍,以减轻后续章节的学习压力。 1.抽象语法树 抽象语法树(abstract syntax tree, AST )是源代码语法结构 的一种抽象表示,它用树状的方式表示编程语言的语法结构。抽 象语法树中的每一个节点都表示源代码中的一个元素,每一棵子 树都表示一个语法元素。以表达式2 * 3 + 7为例,编译器的语 法分析阶段会生成如图2-1所示的抽象语法树。 作为编译器常用的数据结构,抽象语法树抹去了源代码中一 些不重要的字符,如空格、分号、括号等。编译器在执行完语法 分析之后会输出一棵抽象语法树,这棵抽象语法树会辅助编译器进行语义分析,我们可以用它来确 定语法正确的程序是否存在一些类型不匹配问题。 图2-1简单表达式的抽象语法树
6 I第2章编译原理 2. 静态单赋值 静态单赋值(static single assignment, SSA )是中间代码的特性,如果中间代码具有SSA特性, 那么每个变量就只会被赋值一次。在实践中,我们通常用下标实现SSA,这里用下面的代码举个 例子: X := 1 x := 2 y := x 经过简单的分析就能发现,上述代码第一行的赋值语句X := 1不会起到任何作用。下面是具有 SSA特性的中间代码,显然,变量y_1和乂_1没有任何关系,所以在机器码生成时可以省去x := 1 的赋值,通过减少需要执行的指令优化这段代码: x_1 : = 1 x_2 := 2 y_1 := X_2 因为SSA的主要作用是对代码进行优化,所以它是编译器后端①的一部分。当然,代码编译领 域除了 SSA还有很多中间代码的优化方法。编译器生成代码的优化也是一个古老且复杂的领域,这 里就不展开介绍了。 3. 指令集 最后要介绍的预备知识是指令集气很多开发者遇到“在本地开发环境编译和运行正常的代码在 生产环境却无法正常工作”的情况,这种问题背后有多种原因,而不同机器使用不同指令集可能是 原因之一O 很多开发者使用x86_64的MacBook作为主要工作设备,在命令行中输入uname -m就能获得当 前机器的硬件信息: $ uname -m x86_64 x86是目前比较常见的指令集,除x86外,还有ARM等指令集,苹果最新的MacBook自研芯 片就使用了 ARM指令集。不同的处理器使用了不同的架构和机器语言,所以为了在不同的机器上运 行,很多编程语言需要将源代码根据架构翻译成不同的机器代码。 复杂指令集计算机(complex instruction set computer, CISC )和精简指令集计算机(reduced instruction set computer, RISC )是两种遵循不同设计理念的指令集,从名字就可以推测出二者的 区别。 ① 编译器一般分为前端和后端,其中前端主要负责将源代码翻译成与编程语言无关的中间表示,而后端主要负责目标 代码的生成和优化。 ② 指令集架构是计算机的抽象模型,也称架构或者计算机架构。
2.1编译过程I 7 •复杂指令集:通过增加指令的类型减少需要执行的指令数量。 ・精简指令集:使用更少的指令类型完成目标计算任务。 早期的CPU为了减少机器语言指令的数量.一般使用复杂指令集完成计算任务。两者并没有绝 对的优劣,只是在一些设计上的选择不同以达到不同的目的。我们会在本节最后详细介绍指令集架 构,读者也可以自主了解相关内容。 2.1.2编译四阶段 Go语言编译器的源代码在src/cmd/compile目录中,目录下的文件共同组成了 Go语言的编 译器。学过编译原理的人可能听说过编译器的 前端和后端,编译器前端一般承担着词法分析、 语法分析、类型检查和中间代码生成几部分工 作,而编译器后端主要负责目标代码的生成和 优化,也就是将中间代码翻译成目标机器能够 运行的二进制机器码,如图2-2所示。 根据前后端的工作,Go的编译器在逻辑上可以分成4个阶段:词法分析与语法分析、类型检 查、中间代码生成和最后的机器代码生成。下面简单介绍这4个阶段做的工作,后面的章节会详细 介绍每个阶段的具体内容。 1.词法分析与语法分析 所有编译过程其实都是从解析代码的源文件开始的。词法分析的作用就是解析源代码文件,它 将文件中的字符串序列转换成Token序列,方便后面的处理和解析。我们一般把执行词法分析的程 序称为词法分析器(lexer )o 语法分析的输入是词法分析器输出的Token序列。语法分析器会按照顺序解析Token序列,该 过程会将词法分析生成的Token按照编程语言定义好的文法(grammar)自下而上或自上而下地归 约,每一个Go源代码文件最终会被归纳成一个SourceFile结构® : SourceFile = Packageclause ";" { ImportDecl { TopLevelDecl . 词法分析会返回一个不包含空格、换行等字符的Token序列,例如package, json, import,(, io, ),...,而语法分析会把Token序列转换成有意义的结构体,即语法树: "json.go": SourceFile { PackageName: "json", ImportDecl: []Import{ "io", }, TopLevelDecl:... ①SourceFile表示一个Go语言源文件,它由package定义,由多条import语句以及顶层声明组成。
8 I第2章编译原理 Token到上述抽象语法树的转换过程会用到语法分析器,每棵抽象语法树都对应一个单独的Go 语言文件,这棵抽象语法树中包括当前文件属于的包名、定义的常量、结构体和函数等。从源文件 到抽象语法树的转换过程如图2-3所示。 图2-3从源文件到抽象语法树 语法解析过程中发生的任何语法错误都会被语法分析器发现并将消息打印到标准输出中,整个 编译过程也会随着错误的出现而中止。2.2节会详细介绍Go语言的文法、词法分析和语法分析过程。 Go语言的语法分析器使用的是LALR(1)®的文法,对分析器文法感兴趣的读者可以在延伸阅 读部分找到编译器文法的相关资料。 2.类型检查 当拿到一组文件的抽象语法树之后,Go语言的编译器会检查语法树中定义和使用的类型,类型 检查会按照以下顺序分别验证和处理不同类型的节点: (1) 常量、类型,函数名及其类型; (2) 变量的赋值和初始化; (3) 函教和闭包的主体; (4) 哈希表键值对的类型; (5) 导入函数体; (6) 外部声明。 通过遍历整棵抽象语法树,我们在每个节点上都会验证当前子树的类型,以保证节点不存在类 型错误。所有类型错误和不匹配都会在该阶段暴露出来,其中包括结构体对接口的实现。 类型检查阶段不止会验证节点的类型,还会展开和改写 一些内置函数,例如make关键字在该阶段会根据子树的结 构被替换成 runtime.makechan、runtime .makeslice 或者 runtime.makemap等函数②,如图2-4所示。 类型检查这一过程在整个编译流程中非常重要,Go语言 的很多关键字依赖类型检查期间的改写,2.3节会详细介绍这 些步骤。 ① 关于Go语B的文法是不是LALR(1)的讨论,见Google Groupso LALR的全称是Look-Ahead LR,大多数通用编程语言使用LALR的文法。 ② 因为makechan、makeslice, makemap等函数均属于runtime包,所以此处省略。为了简单起见,本书中的函 数均采用略写,只保留最后一部分。 图2Y 类型检查阶段对make 进行改写
2.1编译过程| 9 3.中间代码生成 当我们将源文件转换成了抽象语法树、对整棵树的语法进行解析和类型检查之后,就可以认为 当前文件中的代码不存在语法错误和类型错误问题了,Go语言的编译器就会将输入的抽象语法树转 换成中间代码。 在类型检查之后,编译器会通过cmd/compile/internal/gc.compileFunctions®编译整个 Go语言项目中的全部函数,这些函数会在一个编译队列中等待几个Goroutine的消费,并发执行的 Goroutine会将所有函数对应的抽象语法树转换成中间代码,如图2-5所示。 图2-5并发编译过程 由于Go语言编译器的中间代码使用了 SSA的特性,所以在该阶段我们能够分析出代码中的无 用变量和片段并对代码进行优化。2.4节会详细介绍中间代码的生成过程并简单介绍Go语言中间代 码的SSA特性。 4.机器码生成 Go语言源代码的src/cmd/compile/internal目录中包含了很多机器码生成相关的包,不同 类型的CPU使用了不同的包生成机器码,包括AMD64、ARM、ARM64、MIPS、MIPS64、ppc64、 s390x、x86 和 Wasm,其中比较有趣的是 Wasm (WebAssembly)②。 作为一种在栈虚拟机上使用的二进制指令格式,其设计的主要目标就是在Web浏览器上提供一 种具有高可移植性的目标语言。G。语言的编译器既然能够生成Wasm格式的指令,那么就能在主流 浏览器中运行: $ GOARCH=wasm GOOS=js go build -o lib.wasm main.go 我们可以使用上述命令将Go的源代码编译成能够在浏览器上运行的WebAssembly文件。当然, 除这种新兴的二进制指令格式外,Go语言经过编译还可以在几乎所有主流机器上运行,如图2%所 示,不过它的兼容性在除Linux和Darwin外的机器上可能还有一些问题,例如Go Plugin至今仍不 支持 Windows®o ① 大家可以通过**draveness.me/golang/trce/+方法名"直接访问本书所有方法对应的代码,例如cmd/compile/internal/ gc. compileFunctions 对应的链接为 draveness.me/golang/tree/cmd/compile/intemal/gc.compileFunctions0 ② WebAssembly是基于栈的虚拟机的二进制指令,简称Wasm。 ③ 参见 plugin: add Endows support #19282O