Go语言圣经中文版

在上个世纪70年代,贝尔实验室的Ken Thompson和Dennis M. Ritchie合作发明了UNIX操作 系统,同时Dennis M. Ritchie为了解决UNIX系统的移植性问题而发明了C语言,贝尔实验室 的UNIX和C语言两大发明奠定了整个现代IT行业最重要的软件基础(目前的三大桌面操作系 统的中Linux和Mac OS X都是源于UNIX系统,两大移动平台的操作系统iOS和Android也都是 源于UNIX系统。C系家族的编程语言占据统治地位达几十年之久)。在UNIX和C语言发明40 年之后,目前已经在Google工作的Ken Thompson和Rob Pike(他们在贝尔实验室时就是同 事)、还有Robert Griesemer(设计了V8引擎和HotSpot虚拟机)一起合作,为了解决在21世 纪多核和网络化环境下越来越复杂的编程问题而发明了Go语言。
展开查看详情

1.

2.gopl 目录 前言 0 Go语言起源 0.1 Go语言项目 0.2 本书的组织 0.3 更多的信息 0.4 致谢 0.5 入门 1 Hello, World 1.1 命令行参数 1.2 查找重复的行 1.3 GIF动画 1.4 获取URL 1.5 并发获取多个URL 1.6 Web服务 1.7 本章要点 1.8 程序结构 2 命名 2.1 声明 2.2 变量 2.3 赋值 2.4 类型 2.5 包和文件 2.6 作用域 2.7 基础数据类型 3 整型 3.1 浮点数 3.2 复数 3.3 布尔型 3.4 字符串 3.5 常量 3.6 2

3.gopl 复合数据类型 4 数组 4.1 Slice 4.2 Map 4.3 结构体 4.4 JSON 4.5 文本和HTML模板 4.6 函数 5 函数声明 5.1 递归 5.2 多返回值 5.3 错误 5.4 函数值 5.5 匿名函数 5.6 可变参数 5.7 Deferred函数 5.8 Panic异常 5.9 Recover捕获异常 5.10 方法 6 方法声明 6.1 基于指针对象的方法 6.2 通过嵌入结构体来扩展类型 6.3 方法值和方法表达式 6.4 示例: Bit数组 6.5 封装 6.6 接口 7 接口是合约 7.1 接口类型 7.2 实现接口的条件 7.3 flag.Value接口 7.4 接口值 7.5 sort.Interface接口 7.6 http.Handler接口 7.7 error接口 7.8 3

4.gopl 示例: 表达式求值 7.9 类型断言 7.10 基于类型断言识别错误类型 7.11 通过类型断言查询接口 7.12 类型分支 7.13 示例: 基于标记的XML解码 7.14 补充几点 7.15 Goroutines和Channels 8 Goroutines 8.1 示例: 并发的Clock服务 8.2 示例: 并发的Echo服务 8.3 Channels 8.4 并发的循环 8.5 示例: 并发的Web爬虫 8.6 基于select的多路复用 8.7 示例: 并发的字典遍历 8.8 并发的退出 8.9 示例: 聊天服务 8.10 基于共享变量的并发 9 竞争条件 9.1 sync.Mutex互斥锁 9.2 sync.RWMutex读写锁 9.3 内存同步 9.4 sync.Once初始化 9.5 竞争条件检测 9.6 示例: 并发的非阻塞缓存 9.7 Goroutines和线程 9.8 包和工具 10 包简介 10.1 导入路径 10.2 包声明 10.3 导入声明 10.4 包的匿名导入 10.5 4

5.gopl 包和命名 10.6 工具 10.7 测试 11 go test 11.1 测试函数 11.2 测试覆盖率 11.3 基准测试 11.4 剖析 11.5 示例函数 11.6 反射 12 为何需要反射? 12.1 reflect.Type和reflect.Value 12.2 Display递归打印 12.3 示例: 编码S表达式 12.4 通过reflect.Value修改值 12.5 示例: 解码S表达式 12.6 获取结构体字段标识 12.7 显示一个类型的方法集 12.8 几点忠告 12.9 底层编程 13 unsafe.Sizeof, Alignof 和 Offsetof 13.1 unsafe.Pointer 13.2 示例: 深度相等判断 13.3 通过cgo调用C代码 13.4 几点忠告 13.5 附录 14 附录A:原文勘误 14.1 附录B:作者译者 14.2 附录C:译文授权 14.3 附录D:其它语言 14.4 5

6.gopl Go语言圣经(中文版) Go语言圣经 《The Go Programming Language》 中文版本,仅供学习交流之用。 项目主页:http://github.com/golang-china/gopl-zh 项目主页:http://bitbucket.org/golang-china/gopl-zh 原版官网:http://gopl.io 在线预览: http://gopl-zh.b0.upaiyun.com/ http://docs.ruanjiadeng.com/gopl-zh/ http://shifei.me/gopl-zh/ http://2goo.info/media/html/gopl-zh-gh-pages/ http://docs.plhwin.com/gopl-zh/ http://gopl-zh.simple-is-best.tk/ https://docs.hacknode.org/gopl-zh/ 译者序 在上个世纪70年代,贝尔实验室的Ken Thompson和Dennis M. Ritchie合作发明了UNIX操作 系统,同时Dennis M. Ritchie为了解决UNIX系统的移植性问题而发明了C语言,贝尔实验室 的UNIX和C语言两大发明奠定了整个现代IT行业最重要的软件基础(目前的三大桌面操作系 统的中Linux和Mac OS X都是源于UNIX系统,两大移动平台的操作系统iOS和Android也都是 源于UNIX系统。C系家族的编程语言占据统治地位达几十年之久)。在UNIX和C语言发明40 年之后,目前已经在Google工作的Ken Thompson和Rob Pike(他们在贝尔实验室时就是同 事)、还有Robert Griesemer(设计了V8引擎和HotSpot虚拟机)一起合作,为了解决在21世 纪多核和网络化环境下越来越复杂的编程问题而发明了Go语言。从Go语言库早期代码库日志 可以看出它的演化历程(Git用 git log --before={2008-03-03} --reverse 命令查看): 前言 6

7.gopl 从早期提交日志中也可以看出,Go语言是从Ken Thompson发明的B语言、Dennis M. Ritchie 发明的C语言逐步演化过来的,是C语言家族的成员,因此很多人将Go语言称为21世纪的C语 言。纵观这几年来的发展趋势,Go语言已经成为云计算、云存储时代最重要的基础编程语 言。 在C语言发明之后约5年的时间之后(1978年),Brian W. Kernighan和Dennis M. Ritchie合作 编写出版了C语言方面的经典教材《The C Programming Language》,该书被誉为C语言程 序员的圣经,作者也被大家亲切地称为K&R。同样在Go语言正式发布(2009年)约5年之后 (2014年开始写作,2015年出版),由Go语言核心团队成员Alan A. A. Donovan和K&R中的 Brian W. Kernighan合作编写了Go语言方面的经典教材《The Go Programming Language》。Go语言被誉为21世纪的C语言,如果说K&R所著的是圣经的旧约,那么D&K所 著的必将成为圣经的新约。该书介绍了Go语言几乎全部特性,并且随着语言的深入层层递 进,对每个细节都解读得非常细致,每一节内容都精彩不容错过,是广大Gopher的必读书 目。大部分Go语言核心团队的成员都参与了该书校对工作,因此该书的质量是可以完全放心 的。 同时,单凭阅读和学习其语法结构并不能真正地掌握一门编程语言,必须进行足够多的编程 实践——亲自编写一些程序并研究学习别人写的程序。要从利用Go语言良好的特性使得程序 模块化,充分利用Go的标准函数库以Go语言自己的风格来编写程序。书中包含了上百个精心 挑选的习题,希望大家能先用自己的方式尝试完成习题,然后再参考官方给出的解决方案。 该书英文版约从2015年10月开始公开发售,其中日文版本最早参与翻译和审校(参考致谢部 分)。在2015年10月,我们并不知道中文版是否会及时引进、将由哪家出版社引进、引进将 由何人来翻译、何时能出版,这些信息都成了一个秘密。中国的Go语言社区是全球最大的Go 语言社区,我们从一开始就始终紧跟着Go语言的发展脚步。我们应该也完全有能力以中国Go 前言 7

8.gopl 语言社区的力量同步完成Go语言圣经中文版的翻译工作。与此同时,国内有很多Go语言爱好 者也在积极关注该书(本人也在第一时间购买了纸质版本,亚马逊价格314人民币。补充:国 内也即将出版英文版,价格79元)。为了Go语言的学习和交流,大家决定合作免费翻译该 书。 翻译工作从2015年11月20日前后开始,到2016年1月底初步完成,前后历时约2个月时间(在 其它语言版本中,全球第一个完成翻译的,基本做到和原版同步)。其中,chai2010翻译了 前言、第2 ~ 4章、第10 ~ 13章,Xargin翻译了第1章、第6章、第8 ~ 9章,CrazySssst翻译了 第5章,foreversmart翻译了第7章,大家共同参与了基本的校验工作,还有其他一些朋友提供 了积极的反馈建议。如果大家还有任何问题或建议,可以直接到中文版项目页面提交Issue, 如果发现英文版原文在勘误中未提到的任何错误,可以直接去英文版项目提交。 最后,希望这本书能够帮助大家用Go语言快乐地编程。 2016年 1月 于 武汉 前言 “Go是一个开源的编程语言,它很容易用于构建简单、可靠和高效的软件。”(摘自Go语言官 方网站:http://golang.org ) Go语言由来自Google公司的Robert Griesemer,Rob Pike和Ken Thompson三位大牛于2007 年9月开始设计和实现,然后于2009年的11月对外正式发布(译注:关于Go语言的创世纪过 程请参考 http://talks.golang.org/2015/how-go-was-made.slide )。语言及其配套工具的设计 目标是具有表达力,高效的编译和执行效率,有效地编写高效和健壮的程序。 Go语言有着和C语言类似的语法外表,和C语言一样是专业程序员的必备工具,可以用最小的 代价获得最大的战果。 但是它不仅仅是一个更新的C语言。它还从其他语言借鉴了很多好的 想法,同时避免引入过度的复杂性。 Go语言中和并发编程相关的特性是全新的也是有效的, 同时对数据抽象和面向对象编程的支持也很灵活。 Go语言同时还集成了自动垃圾收集技术用 于更好地管理内存。 Go语言尤其适合编写网络服务相关基础设施,同时也适合开发一些工具软件和系统软件。 但 是Go语言确实是一个通用的编程语言,它也可以用在图形图像驱动编程、移动应用程序开发 和机器学习等诸多领域。目前Go语言已经成为受欢迎的作为无类型的脚本语言的替代者: 因 为Go编写的程序通常比脚本语言运行的更快也更安全,而且很少会发生意外的类型错误。 Go语言还是一个开源的项目,可以免费获编译器、库、配套工具的源代码。 Go语言的贡献 者来自一个活跃的全球社区。Go语言可以运行在类UNIX系统—— 比如 Linux、FreeBSD、OpenBSD、Mac OSX——和Plan9系统和Microsoft Windows操作系统之 上。 Go语言编写的程序无需修改就可以运行在上面这些环境。 前言 8

9.gopl 本书是为了帮助你开始以有效的方式使用Go语言,充分利用语言本身的特性和自带的标准库 去编写清晰地道的Go程序。 前言 9

10.gopl Go语言起源 编程语言的演化跟生物物种的演化类似,一个成功的编程语言的后代一般都会继承它们祖先 的优点;当然有时多种语言杂合也可能会产生令人惊讶的特性;还有一些激进的新特性可能 并没有先例。通过观察这些影响,我们可以学到为什么一门语言是这样子的,它已经适应了 怎样的环境。 下图展示了有哪些早期的编程语言对Go语言的设计产生了重要影响。 Go语言有时候被描述为“C类似语言”,或者是“21世纪的C语言”。Go从C语言继承了相似的表 达式语法、控制流结构、基础数据类型、调用参数传值、指针等很多思想,还有C语言一直所 看中的编译后机器码的运行效率以及和现有操作系统的无缝适配。 但是在Go语言的家族树中还有其它的祖先。其中一个有影响力的分支来自Niklaus Wirth所设 计的Pascal语言。然后Modula-2语言激发了包的概念。然后Oberon语言摒弃了模块接口文件 和模块实现文件之间的区别。第二代的Oberon-2语言直接影响了包的导入和声明的语法,还 Go语言起源 10

11.gopl 有Oberon语言的面向对象特性所提供的方法的声明语法等。 Go语言的另一支祖先,带来了Go语言区别其他语言的重要特性,灵感来自于贝尔实验室的 Tony Hoare于1978年发表的鲜为外界所知的关于并发研究的基础文献 顺序通信进程 ( communicating sequential processes ,缩写为CSP。在CSP中,程序是一组中间没有共享状 态的平行运行的处理过程,它们之间使用管道进行通信和控制同步。不过Tony Hoare的CSP 只是一个用于描述并发性基本概念的描述语言,并不是一个可以编写可执行程序的通用编程 语言。 接下来,Rob Pike和其他人开始不断尝试将CSP引入实际的编程语言中。他们第一次尝试引 入CSP特性的编程语言叫Squeak(老鼠间交流的语言),是一个提供鼠标和键盘事件处理的 编程语言,它的管道是静态创建的。然后是改进版的Newsqueak语言,提供了类似C语言语 句和表达式的语法和类似Pascal语言的推导语法。Newsqueak是一个带垃圾回收的纯函数式 语言,它再次针对键盘、鼠标和窗口事件管理。但是在Newsqueak语言中管道是动态创建 的,属于第一类值, 可以保存到变量中。 在Plan9操作系统中,这些优秀的想法被吸收到了一个叫Alef的编程语言中。Alef试图将 Newsqueak语言改造为系统编程语言,但是因为缺少垃圾回收机制而导致并发编程很痛苦。 (译注:在Aelf之后还有一个叫Limbo的编程语言,Go语言从其中借鉴了很多特性。 具体请 参考Pike的讲稿:http://talks.golang.org/2012/concurrency.slide#9 ) Go语言的其他的一些特性零散地来自于其他一些编程语言;比如iota语法是从APL语言借鉴, 词法作用域与嵌套函数来自于Scheme语言(和其他很多语言)。当然,我们也可以从Go中 发现很多创新的设计。比如Go语言的切片为动态数组提供了有效的随机存取的性能,这可能 会让人联想到链表的底层的共享机制。还有Go语言新发明的defer语句。 Go语言起源 11

12.gopl Go语言项目 所有的编程语言都反映了语言设计者对编程哲学的反思,通常包括之前的语言所暴露的一些 不足地方的改进。Go项目是在Google公司维护超级复杂的几个软件系统遇到的一些问题的反 思(但是这类问题绝不是Google公司所特有的)。 正如Rob Pike所说,“软件的复杂性是乘法级相关的”,通过增加一个部分的复杂性来修复问题 通常将慢慢地增加其他部分的复杂性。通过增加功能、选项和配置是修复问题的最快的途 径,但是这很容易让人忘记简洁的内涵,即从长远来看,简洁依然是好软件的关键因素。 简洁的设计需要在工作开始的时候舍弃不必要的想法,并且在软件的生命周期内严格区别好 的改变和坏的改变。通过足够的努力,一个好的改变可以在不破坏原有完整概念的前提下保 持自适应,正如Fred Brooks所说的“概念完整性”;而一个坏的改变则不能达到这个效果,它 们仅仅是通过肤浅的和简单的妥协来破坏原有设计的一致性。只有通过简洁的设计,才能让 一个系统保持稳定、安全和持续的进化。 Go项目包括编程语言本身,附带了相关的工具和标准库,最后但并非代表不重要的是,关于 简洁编程哲学的宣言。就事后诸葛的角度来看,Go语言的这些地方都做的还不错:拥有自动 垃圾回收、一个包系统、函数作为一等公民、词法作用域、系统调用接口、只读的UTF8字符 串等。但是Go语言本身只有很少的特性,也不太可能添加太多的特性。例如,它没有隐式的 数值转换,没有构造函数和析构函数,没有运算符重载,没有默认参数,也没有继承,没有 泛型,没有异常,没有宏,没有函数修饰,更没有线程局部存储。但是,语言本身是成熟和 稳定的,而且承诺保证向后兼容:用之前的Go语言编写程序可以用新版本的Go语言编译器和 标准库直接构建而不需要修改代码。 Go语言有足够的类型系统以避免动态语言中那些粗心的类型错误,但是,Go语言的类型系统 相比传统的强类型语言又要简洁很多。虽然,有时候这会导致一个“无类型”的抽象类型概念, 但是Go语言程序员并不需要像C++或Haskell程序员那样纠结于具体类型的安全属性。在实践 中,Go语言简洁的类型系统给程序员带来了更多的安全性和更好的运行时性能。 Go语言鼓励当代计算机系统设计的原则,特别是局部的重要性。它的内置数据类型和大多数 的准库数据结构都经过精心设计而避免显式的初始化或隐式的构造函数,因为很少的内存分 配和内存初始化代码被隐藏在库代码中了。Go语言的聚合类型(结构体和数组)可以直接操 作它们的元素,只需要更少的存储空间、更少的内存写操作,而且指针操作比其他间接操作 的语言也更有效率。由于现代计算机是一个并行的机器,Go语言提供了基于CSP的并发特性 支持。Go语言的动态栈使得轻量级线程goroutine的初始栈可以很小,因此,创建一个 goroutine的代价很小,创建百万级的goroutine完全是可行的。 Go语言的标准库(通常被称为语言自带的电池),提供了清晰的构建模块和公共接口,包含 I/O操作、文本处理、图像、密码学、网络和分布式应用程序等,并支持许多标准化的文件格 式和编解码协议。库和工具使用了大量的约定来减少额外的配置和解释,从而最终简化程序 Go语言项目 12

13.gopl 的逻辑,而且,每个Go程序结构都是如此的相似,因此,Go程序也很容易学习。使用Go语 言自带工具构建Go语言项目只需要使用文件名和标识符名称, 一个偶尔的特殊注释来确定所有 的库、可执行文件、测试、基准测试、例子、以及特定于平台的变量、项目的文档等;Go语 言源代码本身就包含了构建规范。 Go语言项目 13

14.gopl 本书的组织 我们假设你已经有一种或多种其他编程语言的使用经历,不管是类似C、C++或Java的编译型 语言,还是类似Python、Ruby、JavaScript的脚本语言,因此我们不会像对完全的编程语言 初学者那样解释所有的细节。因为,Go语言的变量、常量、表达式、控制流和函数等基本语 法也是类似的。 第一章包含了本教程的基本结构,通过十几个程序介绍了用Go语言如何实现类似读写文件、 文本格式化、创建图像、网络客户端和服务器通讯等日常工作。 第二章描述了Go语言程序的基本元素结构、变量、新类型定义、包和文件、以及作用域等概 念。第三章讨论了数字、布尔值、字符串和常量,并演示了如何显示和处理Unicode字符。第 四章描述了复合类型,从简单的数组、字典、切片到动态列表。第五章涵盖了函数,并讨论 了错误处理、panic和recover,还有defer语句。 第一章到第五章是基础部分,主流命令式编程语言这部分都类似。个别之处,Go语言有自己 特色的语法和风格,但是大多数程序员能很快适应。其余章节是Go语言特有的:方法、接 口、并发、包、测试和反射等语言特性。 Go语言的面向对象机制与一般语言不同。它没有类层次结构,甚至可以说没有类;仅仅通过 组合(而不是继承)简单的对象来构建复杂的对象。方法不仅可以定义在结构体上, 而且, 可 以定义在任何用户自定义的类型上;并且, 具体类型和抽象类型(接口)之间的关系是隐式 的,所以很多类型的设计者可能并不知道该类型到底实现了哪些接口。方法在第六章讨论, 接口在第七章讨论。 第八章讨论了基于顺序通信进程(CSP)概念的并发编程,使用goroutines和channels处理并发 编程。第九章则讨论了传统的基于共享变量的并发编程。 第十章描述了包机制和包的组织结构。这一章还展示了如何有效地利用Go自带的工具,使用 单个命令完成编译、测试、基准测试、代码格式化、文档以及其他诸多任务。 第十一章讨论了单元测试,Go语言的工具和标准库中集成了轻量级的测试功能,避免了强大 但复杂的测试框架。测试库提供了一些基本构件,必要时可以用来构建复杂的测试构件。 第十二章讨论了反射,一种程序在运行期间审视自己的能力。反射是一个强大的编程工具, 不过要谨慎地使用;这一章利用反射机制实现一些重要的Go语言库函数, 展示了反射的强大用 法。第十三章解释了底层编程的细节,在必要时,可以使用unsafe包绕过Go语言安全的类型 系统。 每一章都有一些练习题,你可以用来测试你对Go的理解,你也可以探讨书中这些例子的扩展 和替代。 本书的组织 14

15.gopl 书中所有的代码都可以从 http://gopl.io 上的Git仓库下载。go get命令根据每个例子的导入路 径智能地获取、构建并安装。只需要选择一个目录作为工作空间,然后将GOPATH环境变量 设置为该路径。 必要时,Go语言工具会创建目录。例如: $ export GOPATH=$HOME/gobook # 选择工作目录 $ go get gopl.io/ch1/helloworld # 获取/编译/安装 $ $GOPATH/bin/helloworld # 运行程序 Hello, 世界 # 这是中文 运行这些例子需要安装Go1.5以上的版本。 $ go version go version go1.5 linux/amd64 如果使用其他的操作系统, 请参考 https://golang.org/doc/install 提供的说明安装。 本书的组织 15

16.gopl 更多的信息 最佳的帮助信息来自Go语言的官方网站,https://golang.org ,它提供了完善的参考文档,包 括编程语言规范和标准库等诸多权威的帮助信息。同时也包含了如何编写更地道的Go程序的 基本教程,还有各种各样的在线文本资源和视频资源,它们是本书最有价值的补充。Go语言 的官方博客 https://blog.golang.org 会不定期发布一些Go语言最好的实践文章,包括当前语言 的发展状态、未来的计划、会议报告和Go语言相关的各种会议的主题等信息(译注: http://talks.golang.org/ 包含了官方收录的各种报告的讲稿)。 在线访问的一个有价值的地方是可以从web页面运行Go语言的程序(而纸质书则没有这么便 利了)。这个功能由来自 https://play.golang.org 的 Go Playground 提供,并且可以方便地嵌 入到其他页面中,例如 https://golang.org 的主页,或 godoc 提供的文档页面中。 Playground可以简单的通过执行一个小程序来测试对语法、语义和对程序库的理解,类似其 他很多语言提供的REPL即时运行的工具。同时它可以生成对应的url,非常适合共享Go语言 代码片段,汇报bug或提供反馈意见等。 基于 Playground 构建的 Go Tour,https://tour.golang.org ,是一个系列的Go语言入门教程, 它包含了诸多基本概念和结构相关的并可在线运行的互动小程序。 当然,Playground 和 Tour 也有一些限制,它们只能导入标准库,而且因为安全的原因对一些 网络库做了限制。如果要在编译和运行时需要访问互联网,对于一些更复杂的实验,你可能 需要在自己的电脑上构建并运行程序。幸运的是下载Go语言的过程很简单,从 https://golang.org 下载安装包应该不超过几分钟(译注:感谢伟大的长城,让大陆的Gopher 们都学会了自己打洞的基本生活技能,下载时间可能会因为洞的大小等因素从几分钟到几天 或更久),然后就可以在自己电脑上编写和运行Go程序了。 Go语言是一个开源项目,你可以在 https://golang.org/pkg 阅读标准库中任意函数和类型的实 现代码,和下载安装包的代码完全一致。这样,你可以知道很多函数是如何工作的, 通过挖 掘找出一些答案的细节,或者仅仅是出于欣赏专业级Go代码。 更多的信息 16

17.gopl 致谢 Rob Pike和Russ Cox,以及很多其他Go团队的核心成员多次仔细阅读了本书的手稿,他们对 本书的组织结构和表述用词等给出了很多宝贵的建议。在准备日文版翻译的时候,Yoshiki Shibata更是仔细地审阅了本书的每个部分,及时发现了诸多英文和代码的错误。我们非常感 谢本书的每一位审阅者,并感谢对本书给出了重要的建议的Brian Goetz、Corey Kosak、 Arnold Robbins、Josh Bleecher Snyder和Peter Weinberger等人。 我们还感谢Sameer Ajmani、Ittai Balaban、David Crawshaw、Billy Donohue、Jonathan Feinberg、Andrew Gerrand、Robert Griesemer、John Linderman、Minux Ma(译注:中国 人,Go团队成员。)、Bryan Mills、Bala Natarajan、Cosmos Nicolaou、Paul Staniforth、 Nigel Tao(译注:好像是陶哲轩的兄弟)以及Howard Trickey给出的许多有价值的建议。我 们还要感谢David Brailsford和Raph Levien关于类型设置的建议。 我们从来自Addison-Wesley的编辑Greg Doench收到了很多帮助,从最开始就得到了越来越 多的帮助。来自AW生产团队的John Fuller、Dayna Isley、Julie Nahil、Chuti Prasertsith到 Barbara Wood,感谢你们的热心帮助。 Alan Donovan特别感谢:Sameer Ajmani、Chris Demetriou、Walt Drummond和Google公司 的Reid Tatge允许他有充裕的时间去写本书;感谢Stephen Donovan的建议和始终如一的鼓 励,以及他的妻子Leila Kazemi并没有让他为了家庭琐事而分心,并热情坚定地支持这个项 目。 Brian Kernighan特别感谢:朋友和同事对他的耐心和宽容,让他慢慢地梳理本书的写作思 路。同时感谢他的妻子Meg和其他很多朋友对他写作事业的支持。 2015年 10月 于 纽约 致谢 17

18.gopl 第一章 入门 本章介绍Go语言的基础组件。本章提供了足够的信息和示例程序,希望可以帮你尽快入门, 写 出有用的程序。本章和之后章节的示例程序都针对你可能遇到的现实案例。先了解几个Go程 序,涉及的主题从简单的文件处理、图像处理到互联网客户端和服务端并发。当然,第一章 不会解释细枝末节,但用这些程序来学习一门新语言还是很有效的。 学习一门新语言时,会有一种自然的倾向, 按照自己熟悉的语言的套路写新语言程序。学习Go 语言的过程中,请警惕这种想法,尽量别这么做。我们会演示怎么写好Go语言程序,所以, 请使用本书的代码作为你自己写程序时的指南。 入门 18

19.gopl 1.1. Hello, World 我们以现已成为传统的“hello world”案例来开始吧, 这个例子首次出现于1978年出版的C语言 圣经《The C Programming Language》(译注:本书作者之一Brian W. Kernighan也是 《The C Programming Language》一书的作者)。C语言是直接影响Go语言设计的语言之 一。这个例子体现了Go语言一些核心理念。 gopl.io/ch1/helloworld package main import "fmt" func main() { fmt.Println("Hello, 世界") } Go是一门编译型语言,Go语言的工具链将源代码及其依赖转换成计算机的机器指令(译注: 静态编译)。Go语言提供的工具都通过一个单独的命令 go 调用, go 命令有一系列子命令。 最简单的一个子命令就是run。这个命令编译一个或多个以.go结尾的源文件,链接库文件,并 运行最终生成的可执行文件。(本书使用$表示命令行提示符。) $ go run helloworld.go 毫无意外,这个命令会输出: Hello, 世界 Go语言原生支持Unicode,它可以处理全世界任何语言的文本。 如果不只是一次性实验,你肯定希望能够编译这个程序,保存编译结果以备将来之用。可以 用build子命令: $ go build helloworld.go 这个命令生成一个名为helloworld的可执行的二进制文件(译注:Windows系统下生成的可执 行文件是helloworld.exe,增加了.exe后缀名),之后你可以随时运行它(译注:在Windows 系统下在命令行直接输入helloworld.exe命令运行),不需任何处理(译注:因为静态编译, 所以不用担心在系统库更新的时候冲突,幸福感满满)。 Hello, World 19

20.gopl $ ./helloworld Hello, 世界 本书中, 所有的示例代码上都有一行标记,利用这些标记, 可以从gopl.io网站上本书源码仓库 里获取代码: gopl.io/ch1/helloworld 执行 go get gopl.io/ch1/helloworld 命令,就会从网上获取代码,并放到对应目录中(需要 先安装Git或Hg之类的版本管理工具,并将对应的命令添加到PATH环境变量中。序言已经提 及,需要先设置好GOPATH环境变量,下载的代码会放 在 $GOPATH/src/gopl.io/ch1/helloworld 目录)。2.6和10.7节有这方面更详细的介绍。 来讨论下程序本身。Go语言的代码通过包(package)组织,包类似于其它语言里的库 (libraries)或者模块(modules)。一个包由位于单个目录下的一个或多个.go源代码文件组 成, 目录定义包的作用。每个源文件都以一条 package 声明语句开始,这个例子里就 是 package main , 表示该文件属于哪个包,紧跟着一系列导入(import)的包,之后是存储在 这个文件里的程序语句。 Go的标准库提供了100多个包,以支持常见功能,如输入、输出、排序以及文本处理。比 如 fmt 包,就含有格式化输出、接收输入的函数。 Println 是其中一个基础函数,可以打印 以空格间隔的一个或多个值,并在最后添加一个换行符,从而输出一整行。 main 包比较特殊。它定义了一个独立可执行的程序,而不是一个库。在 main 里的 main 函 数 也很特殊,它是整个程序执行时的入口(译注:C系语言差不多都这样)。 main 函数所做 的事情就是程序做的。当然了, main 函数一般调用其它包里的函数完成很多工作, 比如, fmt.Println 。 必须告诉编译器源文件需要哪些包,这就是跟随在 package 声明后面的 import 声明扮演的角 色。hello world例子只用到了一个包,大多数程序需要导入多个包。 必须恰当导入需要的包,缺少了必要的包或者导入了不需要的包,程序都无法编译通过。这 项严格要求避免了程序开发过程中引入未使用的包(译注:Go语言编译过程没有警告信息, 争议特性之一)。 import 声明必须跟在文件的 package 声明之后。随后,则是组成程序的函数、变量、常量、 类型的声明语句(分别由关键字 func , var , const , type 定义)。这些内容的声明顺序并 不重要(译注:最好还是定一下规范)。这个例子的程序已经尽可能短了,只声明了一个函 数, 其中只调用了一个其他函数。为了节省篇幅,有些时候, 示例程序会省 略 package 和 import 声明,但是,这些声明在源代码里有,并且必须得有才能编译。 一个函数的声明由 func 关键字、函数名、参数列表、返回值列表(这个例子里的 main 函数 参数列表和返回值都是空的)以及包含在大括号里的函数体组成。第五章进一步考察函数。 Hello, World 20

21.gopl Go语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句。实际上,编译器会 主动把特定符号后的换行符转换为分号, 因此换行符添加的位置会影响Go代码的正确解析(译 注:比如行末是标识符、整数、浮点数、虚数、字符或字符串文字、关键 字 break 、 continue 、 fallthrough 或 return 中的一个、运算符和分隔符 ++ 、 - - 、 ) 、 ] 或 } 中的一个)。举个例子, 函数的左括号 { 必须和 func 函数声明在同一行上, 且位于末尾,不能独占一行,而在表达式 x + y 中,可在 + 后换行,不能在 + 前换行(译 注:以+结尾的话不会被插入分号分隔符,但是以x结尾的话则会被分号分隔符,从而导致编 译错误)。 Go语言在代码格式上采取了很强硬的态度。 gofmt 工具把代码格式化为标准格式(译注:这 个格式化工具没有任何可以调整代码格式的参数,Go语言就是这么任性),并且 go 工具中 的 fmt 子命令会对指定包, 否则默认为当前目录, 中所有.go源文件应用 gofmt 命令。本书中的 所有代码都被gofmt过。你也应该养成格式化自己的代码的习惯。以法令方式规定标准的代码 格式可以避免无尽的无意义的琐碎争执(译注:也导致了Go语言的TIOBE排名较低,因为缺 少撕逼的话题)。更重要的是,这样可以做多种自动源码转换,如果放任Go语言代码格式, 这些转换就不大可能了。 很多文本编辑器都可以配置为保存文件时自动执行 gofmt ,这样你的源代码总会被恰当地格 式化。还有个相关的工具, goimports ,可以根据代码需要, 自动地添加或删除 import 声 明。这个工具并没有包含在标准的分发包中,可以用下面的命令安装: $ go get golang.org/x/tools/cmd/goimports 对于大多数用户来说,下载、编译包、运行测试用例、察看Go语言的文档等等常用功能都可 以用go的工具完成。10.7节详细介绍这些知识。 Hello, World 21

22.gopl 1.2. 命令行参数 大多数的程序都是处理输入,产生输出;这也正是“计算”的定义。但是, 程序如何获取要处理 的输入数据呢?一些程序生成自己的数据,但通常情况下,输入来自于程序外部:文件、网 络连接、其它程序的输出、敲键盘的用户、命令行参数或其它类似输入源。下面几个例子会 讨论其中几个输入源,首先是命令行参数。 os 包以跨平台的方式,提供了一些与操作系统交互的函数和变量。程序的命令行参数可从os 包的Args变量获取;os包外部使用os.Args访问该变量。 os.Args变量是一个字符串(string)的切片(slice)(译注:slice和Python语言中的切片类 似,是一个简版的动态数组),切片是Go语言的基础概念,稍后详细介绍。现在先把切片s当 作数组元素序列, 序列的长度动态变化, 用 s[i] 访问单个元素,用 s[m:n] 获取子序列(译注: 和python里的语法差不多)。序列的元素数目为len(s)。和大多数编程语言类似,区间索引时, Go言里也采用左闭右开形式, 即,区间包括第一个索引元素,不包括最后一个, 因为这样可以 简化逻辑。(译注:比如a = [1, 2, 3, 4, 5], a[0:3] = [1, 2, 3],不包含最后一个元素)。比如 s[m:n]这个切片,0 ≤ m ≤ n ≤ len(s),包含n-m个元素。 os.Args的第一个元素,os.Args[0], 是命令本身的名字;其它的元素则是程序启动时传给它的 参数。s[m:n]形式的切片表达式,产生从第m个元素到第n-1个元素的切片,下个例子用到的 元素包含在os.Args[1:len(os.Args)]切片中。如果省略切片表达式的m或n,会默认传入0或 len(s),因此前面的切片可以简写成os.Args[1:]。 下面是Unix里echo命令的一份实现,echo把它的命令行参数打印成一行。程序导入了两个 包,用括号把它们括起来写成列表形式, 而没有分开写成独立的 import 声明。两种形式都合 法,列表形式习惯上用得多。包导入顺序并不重要;gofmt工具格式化时按照字母顺序对包名 排序。(示例有多个版本时,我们会对示例编号, 这样可以明确当前正在讨论的是哪个。) gopl.io/ch1/echo1 命令行参数 22

23.gopl // Echo1 prints its command-line arguments. package main import ( "fmt" "os" ) func main() { var s, sep string for i := 1; i < len(os.Args); i++ { s += sep + os.Args[i] sep = " " } fmt.Println(s) } 注释语句以 // 开头。对于程序员来说,//之后到行末之间所有的内容都是注释,被编译器忽 略。按照惯例,我们在每个包的包声明前添加注释;对于 main package ,注释包含一句或几 句话,从整体角度对程序做个描述。 var声明定义了两个string类型的变量s和sep。变量会在声明时直接初始化。如果变量没有显 式初始化,则被隐式地赋予其类型的零值(zero value),数值类型是0,字符串类型是空字 符串""。这个例子里,声明把s和sep隐式地初始化成空字符串。第2章再来详细地讲解变量和 声明。 对数值类型,Go语言提供了常规的数值和逻辑运算符。而对string类型, + 运算符连接字符 串(译注:和C++或者js是一样的)。所以表达式: sep + os.Args[i] 表示连接字符串sep和os.Args。程序中使用的语句: s += sep + os.Args[i] 是一条赋值语句, 将s的旧值跟sep与os.Args[i]连接后赋值回s,等价于: s = s + sep + os.Args[i] 运算符 += 是赋值运算符(assignment operator),每种数值运算符或逻辑运算符, 如 + 或 * ,都有对应的赋值运算符。 命令行参数 23

24.gopl echo程序可以每循环一次输出一个参数,这个版本却是不断地把新文本追加到末尾来构造字 符串。字符串s开始为空,即值为"",每次循环会添加一些文本;第一次迭代之后,还会再插 入一个空格,因此循环结束时每个参数中间都有一个空格。这是一种二次加工(quadratic process),当参数数量庞大时,开销很大,但是对于echo,这种情形不大可能出现。本章会 介绍echo的若干改进版,下一章解决低效问题。 循环索引变量i在for循环的第一部分中定义。符号 := 是短变量声明(short variable declaration)的一部分, 这是定义一个或多个变量并根据它们的初始值为这些变量赋予适当类 型的语句。下一章有这方面更多说明。 自增语句 i++ 给 i 加1;这和 i += 1 以及 i = i + 1 都是等价的。对应的还有 i-- 给 i 减 1。它们是语句,而不像C系的其它语言那样是表达式。所以 j = i++ 非法,而且++和--都只 能放在变量名后面,因此 --i 也非法。 Go语言只有for循环这一种循环语句。for循环有多种形式,其中一种如下所示: for initialization; condition; post { // zero or more statements } for循环三个部分不需括号包围。大括号强制要求, 左大括号必须和post语句在同一行。 initialization语句是可选的,在循环开始前执行。initalization如果存在,必须是一条简单语句 (simple statement),即,短变量声明、自增语句、赋值语句或函数调用。 condition 是一 个布尔表达式(boolean expression),其值在每次循环迭代开始时计算。如果为 true 则执 行循环体语句。 post 语句在循环体执行结束后执行,之后再次对 condition 求 值。 condition 值为 false 时,循环结束。 for循环的这三个部分每个都可以省略,如果省略 initialization 和 post ,分号也可以省 略: // a traditional "while" loop for condition { // ... } 如果连 condition 也省略了,像下面这样: // a traditional infinite loop for { // ... } 命令行参数 24

25.gopl 这就变成一个无限循环,尽管如此,还可以用其他方式终止循环, 如一条 break 或 return 语 句。 for 循环的另一种形式, 在某种数据类型的区间(range)上遍历,如字符串或切 片。 echo 的第二版本展示了这种形式: gopl.io/ch1/echo2 // Echo2 prints its command-line arguments. package main import ( "fmt" "os" ) func main() { s, sep := "", "" for _, arg := range os.Args[1:] { s += sep + arg sep = " " } fmt.Println(s) } 每次循环迭代, range 产生一对值;索引以及在该索引处的元素值。这个例子不需要索引, 但 range 的语法要求, 要处理元素, 必须处理索引。一种思路是把索引赋值给一个临时变量, 如 temp , 然后忽略它的值,但Go语言不允许使用无用的局部变量(local variables),因为这 会导致编译错误。 Go语言中这种情况的解决方法是用 空标识符 (blank identifier),即 _ (也就是下划线)。 空标识符可用于任何语法需要变量名但程序逻辑不需要的时候, 例如, 在循环里,丢弃不需要 的循环索引, 保留元素值。大多数的Go程序员都会像上面这样使用 range 和 _ 写 echo 程 序,因为隐式地而非显式地索引os.Args,容易写对。 echo 的这个版本使用一条短变量声明来声明并初始化 s 和 seps ,也可以将这两个变量分开 声明,声明一个变量有好几种方式,下面这些都等价: s := "" var s string var s = "" var s string = "" 用哪种不用哪种,为什么呢?第一种形式,是一条短变量声明,最简洁,但只能用在函数内 部,而不能用于包变量。第二种形式依赖于字符串的默认初始化零值机制,被初始化为""。第 三种形式用得很少,除非同时声明多个变量。第四种形式显式地标明变量的类型,当变量类 命令行参数 25

26.gopl 型与初值类型相同时,类型冗余,但如果两者类型不同,变量类型就必须了。实践中一般使 用前两种形式中的某个,初始值重要的话就显式地指定变量的类型,否则使用隐式初始化。 如前文所述,每次循环迭代字符串s的内容都会更新。 += 连接原字符串、空格和下个参数, 产生新字符串, 并把它赋值给 s 。 s 原来的内容已经不再使用,将在适当时机对它进行垃圾 回收。 如果连接涉及的数据量很大,这种方式代价高昂。一种简单且高效的解决方案是使 用 strings 包的 Join 函数: gopl.io/ch1/echo3 func main() { fmt.Println(strings.Join(os.Args[1:], " ")) } 最后,如果不关心输出格式,只想看看输出值,或许只是为了调试,可以用 Println 为我们 格式化输出。 fmt.Println(os.Args[1:]) 这条语句的输出结果跟 strings.Join 得到的结果很像,只是被放到了一对方括号里。切片都 会被打印成这种格式。 练习 1.1: 修改 echo 程序,使其能够打印 os.Args[0] ,即被执行命令本身的名字。 练习 1.2: 修改 echo 程序,使其打印每个参数的索引和值,每个一行。 练习 1.3: 做实验测量潜在低效的版本和使用了 strings.Join 的版本的运行时间差异。(1.6 节讲解了部分 time 包,11.4节展示了如何写标准测试程序,以得到系统性的性能评测。) 命令行参数 26

27.gopl 1.3. 查找重复的行 对文件做拷贝、打印、搜索、排序、统计或类似事情的程序都有一个差不多的程序结构:一 个处理输入的循环,在每个元素上执行计算处理,在处理的同时或最后产生输出。我们会展 示一个名为 dup 的程序的三个版本;灵感来自于Unix的 uniq 命令,其寻找相邻的重复行。 该程序使用的结构和包是个参考范例,可以方便地修改。 dup 的第一个版本打印标准输入中多次出现的行,以重复次数开头。该程序将引入 if 语 句, map 数据类型以及 bufio 包。 gopl.io/ch1/dup1 // Dup1 prints the text of each line that appears more than // once in the standard input, preceded by its count. package main import ( "bufio" "fmt" "os" ) func main() { counts := make(map[string]int) input := bufio.NewScanner(os.Stdin) for input.Scan() { counts[input.Text()]++ } // NOTE: ignoring potential errors from input.Err() for line, n := range counts { if n > 1 { fmt.Printf("%d\t%s\n", n, line) } } } 正如 for 循环一样, if 语句条件两边也不加括号,但是主体部分需要加。 if 语句 的 else 部分是可选的,在 if 的条件为 false 时执行。 map存储了键/值(key/value)的集合,对集合元素,提供常数时间的存、取或测试操作。键 可以是任意类型,只要其值能用 == 运算符比较,最常见的例子是字符串;值则可以是任意类 型。这个例子中的键是字符串,值是整数。内置函数 make 创建空 map ,此外,它还有别的 作用。4.3节讨论 map 。 (译注:从功能和实现上说, Go 的 map 类似于 Java 语言中的 HashMap ,Python语言中 的 dict , Lua 语言中的 table ,通常使用 hash 实现。遗憾的是,对于该词的翻译并不统 查找重复的行 27

28.gopl 一,数学界术语为 映射 ,而计算机界众说纷纭莫衷一是。为了防止对读者造成误解,保留不 译。) 每次 dup 读取一行输入,该行被当做 map ,其对应的值递增。 counts[input.Text()]++ 语句 等价下面两句: line := input.Text() counts[line] = counts[line] + 1 map 中不含某个键时不用担心,首次读到新行时,等号右边的表达式 counts[line] 的值将被 计算为其类型的零值,对于int`即0。 为了打印结果,我们使用了基于 range 的循环,并在 counts 这个 map 上迭代。跟之前类 似,每次迭代得到两个结果,键和其在 map 中对应的值。 map 的迭代顺序并不确定,从实践 来看,该顺序随机,每次运行都会变化。这种设计是有意为之的,因为能防止程序依赖特定 遍历顺序,而这是无法保证的。(译注:具体可以参见这里 http://stackoverflow.com/questions/11853396/google-go-lang-assignment-order) 继续来看 bufio 包,它使处理输入和输出方便又高效。 Scanner 类型是该包最有用的特性之 一,它读取输入并将其拆成行或单词;通常是处理行形式的输入最简单的方法。 程序使用短变量声明创建 bufio.Scanner 类型的变量 input 。 input := bufio.NewScanner(os.Stdin) 该变量从程序的标准输入中读取内容。每次调用 input.Scan() ,即读入下一行,并移除行末 的换行符;读取的内容可以调用 input.Text() 得到。 Scan 函数在读到一行时返回 true ,不 再有输入时返回 false 。 类似于C或其它语言里的 printf 函数, fmt.Printf 函数对一些表达式产生格式化输出。该函 数的首个参数是个格式字符串,指定后续参数被如何格式化。各个参数的格式取决于“转换字 符”(conversion character),形式为百分号后跟一个字母。举个例子, %d 表示以十进制形 式打印一个整型操作数,而 %s 则表示把字符串型操作数的值展开。 Printf 有一大堆这种转换,Go程序员称之为动词(verb)。下面的表格虽然远不是完整的规 范,但展示了可用的很多特性: 查找重复的行 28

29.gopl %d 十进制整数 %x, %o, %b 十六进制,八进制,二进制整数。 %f, %g, %e 浮点数: 3.141593 3.141592653589793 3.141593e+00 %t 布尔:true或false %c 字符(rune) (Unicode码点) %s 字符串 %q 带双引号的字符串"abc"或带单引号的字符'c' %v 变量的自然形式(natural format) %T 变量的类型 %% 字面上的百分号标志(无操作数) dup1 的格式字符串中还含有制表符 \t 和换行符 \n 。字符串字面上可能含有这些代表不可 见字符的转义字符(escap sequences)。默认情况下, Printf 不会换行。按照惯例,以字 母 f 结尾的格式化函数,如 log.Printf 和 fmt.Errorf ,都采用 fmt.Printf 的格式化准则。 而以 ln 结尾的格式化函数,则遵循 Println 的方式,以跟 %v 差不多的方式格式化参数,并 在最后添加一个换行符。(译注:后缀 f 指 fomart , ln 指 line 。) 很多程序要么从标准输入中读取数据,如上面的例子所示,要么从一系列具名文件中读取数 据。 dup 程序的下个版本读取标准输入或是使用 os.Open 打开各个具名文件,并操作它们。 gopl.io/ch1/dup2 查找重复的行 29