学习Go语言版高清.pdf
http://www.100md.com
2020年11月2日
![]() |
| 第1页 |
![]() |
| 第8页 |
![]() |
| 第18页 |
![]() |
| 第21页 |
![]() |
| 第41页 |
![]() |
| 第53页 |
参见附件(1135KB,119页)。
学习Go语言版
Google工程师亲授,从学习语言语法特性到函数式编程、并发编程等等。理论与实战结合,帮助快速掌握Go语言。通过研读标准库等经典代码设计模式,启发读者深刻理解Go语言的核心思维,进入Go语言开发的更高阶段。

学习目标
从基础语法到应用拓展,学习从单体到并发到分布式的项目演进过程。
课程重点
1、函数式编程、面向接口、并发编程
2、Go语言工程化
3、爬虫项目:单任务爬虫、多任务并发爬虫、分布式爬虫
内容介绍
详细讲述Go语言规范与语法细节及开发中常见的误区,通过研读标准库等经典代码设计模式,启发读者深刻理解Go语言的核心思维,进入Go语言开发的更高阶段。
本书详细讲解了Go语言数据类型、关键字、字面量、基本语法等基础概念及Go项目的工程构建、测试、编译与运行等;深入讲解了协程(goroutine)和通道(channel)等与并发编程有关的概念;
还介绍了系统标准库、网络编程和第三方包。读者掌握本书内容后,可以顺利进行实际项目开发。
本书适合Go语言初学者和有一定经验的程序员阅读。
学习Go语言相关内容解析
本书详细讲解了Go语言基础知识点,并联系实际指出其可能存在的陷阱,帮助读者加深学习时的理解。本书还结合流行度较高的开源第三方包,引导读者进行更高级的实际项目开发。
本书非常适合Go语言新手细细阅读。有一定经验的开发人员,也可以根据自己的情况,选择一些章节来看。
第 1~4章为基础部分,主要讲解Go语言的基础知识,包括Go语言的安装、基本语法、标识符、关键字、运算符、标点符号、字面量等,以及Go项目的工程构建、编译与运行等。
第 5~8 章为中级部分,主要讲解Go语言的复合数据类型,包括数组(array)、切片(slice)、字典(map)、结构体(struct)、指针(pointer)、函数(function)、接口(interface)和通道(channel)类型等。利用灵活的type关键字,可以自定义各种需要的数据类型。函数提供了更直接的数据处理能力,而通过panic,recover,defer处理错误的方式,也是Go语言的典型特征。
第9~13章为高级部分,主要讲解结构体、接口和方法,它们是Go语言简单与组合思维的基础。非常友好地支持并发是Go语言天然具有的典型特征,协程(gorountine)和通道(channel)配合,加上sync 包提供的系列功能,使我们可以很方便地编写支持高并发的代码。
第14~16章为拓展部分,主要介绍Go语言提供的官方标准库,包括OS操作、文件I/O、网络传输处理、指针相关操作、代码反射、日志记录等。这些包可以让我们快速进入实际开发。另外对MySQL数据库以及LevelDB、BoltDB数据库的操作有简单介绍。
第17、18章为应用部分,主要以网络爬虫和Web框架为例,进入实际开发。网络爬虫是互联网服务中比较重要的功能,通过互联网抓取、分析、保存资料是程序员的一项基本能力,读者可以看到Go语言在此方面也是游刃有余。而利用Gin这款轻量级的Web框架,可以很方便地搭建各种Web服务。
自2009年Go语言面世以来,已经有越来越多的公司转向Go语言开发。而Go语言以语法简单、学习门槛低、上手快著称,但入门后很多人发现要写出地道的、遵循 Go语言思维的代码却实属不易。
我作为Go语言的爱好者,在阅读系统标准库源代码或其他知名开源包源代码时,发现大牛对这门语言的了解之深入,代码实现之巧妙优美,除了膜拜还是膜拜。所以我建议你有时间多多阅读这些代码,网上说Go大神的标准是“能理解简洁和可组合性哲学”。的确,Go语言追求代码简洁到极致,而组合思想可谓借助于结构体和接口而成为Go的灵魂。
function、method、interface、type等名词是程序员们接触比较多的关键字,但在Go语言中,你会发现,它们有更强大、更灵活的用法。当你彻底理解了Go语言相关基本概念,以及对其特点有了深入的认知(当然这也是这本书的目的),再假以时日多练习和实践,我相信你很快就能真正掌握这门语言,成为一名出色的Gopher。
本书最早通过网络发布,有不少关注Go语言的朋友通过各种途径给了不少建议,这里要感谢网友Joyboo、林远鹏、Mr_RSI、magic-joker等。
本书最终得以出版,需要感谢李岩兄的鼓励和帮助,以及其他各位朋友和老师们,感谢你们的鼓励和帮助,感谢你们的支持!
最后,希望更多的人了解和使用Go语言,也希望阅读本书的朋友们多多交流。虽然本书中的例子都经过实际运行,但难免会有错误和不足之处,烦请您指出。书中其他疏漏之处也恳请各位读者斧正。
祝各位Gopher工作开心,编码愉快!
学习Go语言需要哪些基础?
先放结论:没你想的那么难,干就完了。
这里插一句,话说前几日看到过一句话:现在的80后真是可怕,狠起来什么都学!
打完鸡血,接下来说说怎么学。
Go语言语法特别简单简洁,有C的底子更好,差一些也没关系。前提是你要真心想学,才有足够的动力去学。
初学Go语言首先弄懂基础语法和概念:基本数据类型、Struct、Array、map、Slice、指针、接口、map、内置函数,常用工具包等,还有接口和Slice的底层数据结构。这些不需要弄特别懂,能自己理解并自己描述我觉得就可以了,关键在实践和应用练习。
然后学文件操作、网络编程、锁、协程、对象序列化和反序列化,以及各种数据格式的封装等,这是进阶的内容。
学完上面两个阶段,就可以自己接触一些框架,然后搭建一个web服务器,来做一些测试和练习。Go各种框架都非常多了,github上star数过万的就有很多,这里列举几个:beego、iris、gin、echo、revel等。对于学习一些使用方法还是很有帮助的。
在第三个阶段做的过程中,自然就接触到了数据库,io,操作系统,网络,存储优化、需要排很多雷。到这一步,已经能够模块化开发,应对正式的开发任务需求了。
以上是作为从Java转过来的一枚现役goer的感悟吧,供大家讨论。
下面再来说说学习资料的问题。
我们码农界讲究追根溯源,所以最正宗的还是去官方哪里找找线索。
Go官方网站:https://golang.org/
Go官方文档:https://golang.org/doc/ 在这里有学习者关于Go这门语言的文档说明,可以参考学习。
中文Go社区:https://studygolang.com/ 这里是社区形式,适合有一定基础后,讨论一些技术问题或者交流观点,挺好的一个网站。
再有就是书籍:基础可以看《Go语言快乐学习》讲基础语法的一本书;进阶的话,可以看Go大神郝林的《Go并发编程实战》。
再有就是视频教程:现在各大付费网站也有很多关于Go框架的学习视频课程,也应该可以帮助到你。
学习Go语言版截图




学习 Go 语言
作者:
Miek Gieben
译者:
邢星
感谢:
Go 作者,Google
下朋友提供了帮助和贡献:
(按字母顺序)
Adam J. Gray,Alex Sychev,Alexey Chernenkov,Andrea Spadaccini,Andrey Mirtchovski,Anthony Magro,Babu Sreekanth,Ben Bullock,Bob Cunningham,Brian Fallik,Cecil
New,Damian Gryski,Dan Kortschak,David Otton,Fabian Becker,Filip Zaludek,Hadi
Amiri,Haiping Fan,Jaap Akkerhuis,JC van Winkel,Jeroen Bulten,Jinpu Hu,John
Shahid,Jonathan Kans,Joshua Stein,Makoto Inoue,Mayuresh Kathe,“mem”,Michael
Stapelberg,Olexandr Shalakhin,Paulo Pinto,Peter Kleiweg,Philipp Schmidt,Robert
Johnson,Russel Winder,Sonia Keys,Stefan Schroeder,Thomas Kapplet,T.J. Yang,“Cobold”,“Simoc”,“Uriel”y,邢星。 还有来自以下朋友的小帮助:
Alexander Katasonov, Daniele Pala, Iaroslav Tymchenko, Nicolas Kaiser, Marco Ynema.
This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 License.
Miek Gieben – ?2010 - 2012 邢星 – ?2011 - 2013本作品依照署名 -非商业性使用 -相同方式共享 3.0 Unported 许可证发布。访问 http:
creativecommons.orglicensesby-nc-sa3.0 查看该许可证副本,或写信
到 Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA。
本书所有实例代码依此方式放入公共领域。
“学习 Go 语言” 已经被翻译为:
中文,邢星;
Learning as we Go (1.0)
支持 Go 1.1 版本Contents
读者 . . . . . . . . . . iv
1 简介 1
官方文档 . . . . . . . . . 1
Hello World . . . . . . . . 2
编译和运行代码 . . . . . . . . 3
本书使用的设置 . . . . . . . . 3
变量、类型和关键字 . . . . . . . . 3
运算符和内建函数 . . . . . . . . 7
Go 关键字 . . . . . . . . . 8
控制结构 . . . . . . . . . 8
内建函数 . . . . . . . . . 13
array、slices 和 map . . . . . . . . 14
练习 . . . . . . . . . . 19
答案 . . . . . . . . . . 21
2 函数 26
作用域 . . . . . . . . . . 27
多值返回 . . . . . . . . . 28
命名返回值 . . . . . . . . 28
延迟代码 . . . . . . . . . 29
变参 . . . . . . . . . . 30
函数作为值 . . . . . . . . 31
回调 . . . . . . . . . . 31
恐慌(Panic)和恢复(Recover) . . . . . . 32
练习 . . . . . . . . . . 33
答案 . . . . . . . . . . 37
3 包 44
标识符 . . . . . . . . . . 45
包的文档 . . . . . . . . . 46
测试包 . . . . . . . . . . 47
常用的包 . . . . . . . . . 48
练习 . . . . . . . . . . 50
答案 . . . . . . . . . . 51
4 进阶 54
内存分配 . . . . . . . . . 54
定义自己的类型 . . . . . . . . 57
转换 . . . . . . . . . . 59
组合 . . . . . . . . . . 60
练习 . . . . . . . . . . 61
答案 . . . . . . . . . . 63ii Chapter: Contents
5 接口 70
方法 . . . . . . . . . . 72
接口名字 . . . . . . . . . 73
简短的例子 . . . . . . . . 73
练习 . . . . . . . . . . 78
答案 . . . . . . . . . . 79
6 并发 82
更多关于 channel . . . . . . . . 84
练习 . . . . . . . . . . 85
答案 . . . . . . . . . . 87
7 通讯 90
io.Reader . . . . . . . . . 91
一些例子 . . . . . . . . . 91
命令行参数 . . . . . . . . 92
执行命令 . . . . . . . . . 92
网络 . . . . . . . . . . 93
练习 . . . . . . . . . . 94
答案 . . . . . . . . . . 97
A 版权 106
贡献者 . . . . . . . . . . 106
许可证和版权 . . . . . . . . 107
B 索引 108
C Bibliography 110
List of Exercises
1 (0) For-loop . . . . . . . . 19
2 (0) FizzBuzz . . . . . . . . 19
3 (1) 字符串 . . . . . . . . 19
4 (1) 平均值 . . . . . . . . 19
5 (0) 平均值 . . . . . . . . 33
6 (0) 整数顺序 . . . . . . . . 33
7 (1) 作用域 . . . . . . . . 33
8 (1) 栈 . . . . . . . . . . 33
9 (1) 变参 . . . . . . . . . 34
10 (1) 斐波那契 . . . . . . . . 34
11 (1) map 函数 . . . . . . . . 34
12 (0) 最小值和最大值 . . . . . . . . 34
13 (1) 冒泡排序 . . . . . . . . 34
14 (1) 函数返回一个函数 . . . . . . . 35
15 (0) stack 包 . . . . . . . . 50
16 (2) 计算器 . . . . . . . . 50List of Exercises iii
17 (1) 指针运算 . . . . . . . . 61
18 (2) 使用 interface 的 map 函数 . . . . . . 61
19 (1) 指针 . . . . . . . . . 61
20 (1) Linked List . . . . . . . . 61
21 (1) Cat . . . . . . . . . 61
22 (2) 方法调用 . . . . . . . . 62
23 (1) 接口和编译 . . . . . . . . 78
24 (1) 指针和反射 . . . . . . . . 78
25 (2) 接口和 max . . . . . . . . 78
26 (1) Channel . . . . . . . . 85
27 (2) 斐波那契 II . . . . . . . . 85
28 (2) 进程 . . . . . . . . . 94
29 (0) 单词和字母统计 . . . . . . . . 95
30 (0) Uniq . . . . . . . . . 95
31 (2) Quine . . . . . . . . . 95
32 (1) Echo 服务 . . . . . . . . 95
33 (2) 数字游戏 . . . . . . . . 95
34 (1) Finger 守护进程 . . . . . . . . 96前言
“Go 是面向对象的语言吗?是也不是。”
FAQ
GO AUTHORS
读者
这是关于来自 Google 的 Go 语言的简介。目标是为这个新的、革命性的语言提供一个
指南。
本书假设你已经在系统中安装了 Go。
这本书的目标读者是那些熟悉编程,并且了解某些编程语言,例如 C[3],C++[21],Perl[5],Java[15],Erlang[4],Scala[16],Haskell[1]。这不是教你如何编程的书,只是
教你如何使用 Go。
学习某样新东西,最佳的方式可能是通过编写程序来探索它。因此每章都包含了若干
练习(和答案)来让你熟悉这个语言。练习标有编号 Qn,而 n 是一个数字。在练习编
号后面的圆括号中的数字指定了该题的难度。难度范围从 0 到 2:
0. 简单;
1. 中等;
2. 困难。
其后为了容易索引,提供了一个简短的标题。例如:
Q1. (1) map 函数 …
展示了难度等级1、编号Q1的关于map函数的问题。相关答案在练习的下一页。答
案的顺序和练习一致,以An开头的答案,对应编号n的练习。一些练习没有答案,它
们将用星号标记出来。
内容布局
第 1 章:简介
讨论了语言中可用的基本类型、变量和控制结构。
第 2 章:函数
会了解到函数,这是 Go 程序中的基本部件。
第 3 章:包
会了解在包中整合函数和数据。同时也将了解如何对包编写文档和进行测试。
第 4 章:进阶
会看到如何创建自定义的类型。同时也将了解 Go 中的内存分配。
第 5 章:接口
Go 不支持传统意义上的面向对象。在 Go 中接口是核心概念。读者 1
第 6 章:并发
通过go关键字,函数可以在不同的例程(叫做goroutines)中执行。通过channel
来完成这些 goroutines 之间的通讯。
第 7 章:通讯
最后一章展示了如何用接口来完成 Go 程序的其他部分。如何创建、读取和写入
文件。同时也简要了解一下网络的使用。
希望你喜欢本书,同时也喜欢上 Go 语言。
翻译
本书的内容可随意取用。这里已经有相关翻译:
中文,邢星:http:www.mikespook.comlearning-go
俄文,Michael Davydenko
Miek Gieben,2011 – miek@miek.nl
邢星,2011 – mikespook@gmail.com1 简介
“对此感兴趣,并且希望做点什么。”
在为 Go 添加复数支持时
KEN THOMPSON
什么是 Go?来自其网站 [13] 的介绍:
Go编程语言是一个使得程序员更加有效率的开源项目。Go是有表达力、简
洁、清晰和有效率的。它的并行机制使其很容易编写多核和网络应用,而
新?的类型系统允许构建有?性的模块化程序。Go 编译到机器码非常快
速,同时具有便利的垃圾回收和强大的运行时反射。它是快速的、静态类
型编译语言,但是感觉上是动态类型的,解释型语言。
Go 1 是 Go 语言的第一个稳定发布版本。本文档的所有练习都工作于 Go 1 – 如果不能
工作,那就一定是 bug。
本书使用了下面的约定:
代码、关键字和注释使用 Source Code Pro 显示;
代码中的额外标识 像这样显示;
较长的标识提供数字 – . .
1 – 详细解释在其后显示;
(如果需要)行号在右边显示;
shell 的例子使用 % 作为输入符;
用户在 shell 输入内容的例子 用黑体显示,系统反馈 用普通的黑体显示;
强调的段落会缩进,并在左边有竖线。
官方文档
Go已经有大量的文档。例如Go Tutorial [12]和Effective Go [8]。网站http:golang. 在 互 联 网 上 搜
索时,应当使用
“golang” 这个词来
代替原始的 “go”。
orgdoc 也是绝佳的起点 a。虽然并不一定要阅读这些文档,但是强烈建议这么做。
Go 1 通过叫做 go doc 的标准程序提供其文档。如果你想了解内建相关(参阅下一章
“运算符和内建函数” 小节)的文档,可以像这样获取:
% go doc builtin
在第 3 章解释了如何构造你自己的包的文档。
有一些特性让 Go 与众不同。
清晰并且简洁
Go 努力保持小并且优美,你可以在短短几行代码里做许多事情;
ahttp:golang.orgdoc 本身是由 go doc 提供服务的。2 Chapter 1: 简介
并行
Go 让函数很容易成为非常轻量的线程。这些线程在 Go 中被叫做 goroutines b;
Channel
这些 goroutines 之间的通讯由 channel[18, 25] 完成;
快速
编译很快,执行也很快。目标是跟 C 一样快。编译时间用秒计算;
安全
当转换一个类型到另一个类型的时候需要显式的转换并遵循严格的规则。Go 有
垃圾收集,在 Go 中无须 free,语言会处理这一切;
标准格式化
Go 程序可以被格式化为程序员希望的(几乎)任何形式,但是官方格式是存在
的。标准也非常简单:gofmt 的输出就是官方认可的格式;
类型后置
类型在变量名的后面,像这样 var a int,来代替 C 中的 int a;
UTF-8
任何地方都是 UTF-8 的,包括字符串以及程序代码。你可以在代码中使用 =
+ 1;
开源
Go 的许可证是完全开源的,参阅 Go 发布的源码中的 LICENSE 文件;
开心
用 Go 写程序会非常开心!
Erlang[4] 与 Go 在部分功能上类似。Erlang 和 Go 之间主要的区别是 Erlang 是函数式
语言,而 Go 是命令式的。Erlang 运行在虚拟机上,而 Go 是编译的。Go 用起来感觉更
接近 Unix。
Hello World
在 Go 指南中,用一个传统的方式展现了 Go:让它打印 “Hello World”(Ken Thompson
和 Dennis Ritchie 在 20 世纪 70 年代,发布 C 语言的时候开创了这个先河)。我们不认
为其他方法可以做得更好,所以就是这个吧:Go 的 “Hello World”。
Listing 1.1. Hello world
1 package main . .
0
3 import fmt . .
1 实现格式化的 IO
6 Print something . .
2
8 func main {
. .
3
b是的,它的发音很接近 coroutines,但是 goroutines 确实有一些不同,我们将在第 6 章讨论。编译和运行代码 3
9 . .
4
10 fmt.Printf(Hello, world ; or ? ó? ; orこんにちは
世界)
11 }
逐行阅读这个程序。
. .
0 首行这个是必须的。所有的 Go 文件以 package
开头,对于独立运行的执行文件必须是 package main;
. .
1 这是说需要将fmt 包加入main。不是main的其他包都被称为库,其他许多编程语言有
着类似的概念(参阅第 3 章)。末尾以 开头的内容是注释;
. .
2 这同样是注释,不过这是被包裹于 和 之间的;
. .
3 package main 必须首先出现,紧跟着是 import。在 Go 中,package 总是首先出现,然后是 import,然后是其他所有内容。当 Go 程序在执行的时候,首先调用的函数
是 main.main,这是从 C 中继承而来。这里定义了这个函数;
. .
4 第 8 行调用了来自于 fmt 包的函数打印字符串到屏幕。字符串由 包裹,并且可以包
含非 ASCII 的字符。这里使用了希腊文和日文。
编译和运行代码
构建 Go 程序的最佳途径是使用 go 工具。
构建 helloworld 只需要:
% go build helloworld.go
结果是叫做 helloworld 的可执行文件。
% .helloworld
Hello, world; or ? ó?; or こんにちは世界
本书使用的设置
Go 被安装在 ?go,而 GOROOT 被设置为 GOROOT=?go;
希望编译的 Go 代码放在 ?gsrc 而 GOPATH 设置为 GOPATH=?g。在使用包的
时候需要用到这个变量(参阅第 3 章)。
变量、类型和关键字
在接下来的章节中,我们将会了解这个新语言的变量、基本类型、关键字和控制流。
Go 在语法上有着类 C 的感觉。如果你希望将两个(或更多)语句放在一行书写,它们
必须用分号 (’;’) 分隔。一般情况下,你不需要分号。4 Chapter 1: 简介
Go 同其他语言不同的地方在于变量的类型在变量名的后面。不是:int a,而是 a int。
当定义了一个变量,它默认赋值为其类型的 null 值。这意味着,在 var a int后,a 的
值为 0。而 var s string,意味着 s 被赋值为零长度字符串,也就是 。
在 Go 中,声明和赋值是两过程,但是可以连在一起。比较下面作用相同的代码片段。
Listing 1.2. 用 = 声明
var a int
var b bool
a = 15
b = false
Listing 1.3. 用:= 声明
a := 15
b := false
在左边使用了关键字 var 声明变量,然后赋值给它。右边的代码使用了 := 使得在一
步内完成了声明和赋值(这一形式只可用在函数内)。在这种情况下,变量的类型是由
值推演出来的。值 15 表示是 int 类型,值 false 告诉 Go 它的类型应当是 bool。多
个 var 声明可以成组;const 和 import 同样允许这么做。留意圆括号的使用:
var (
x int
b bool)
有相同类型的多个变量同样可以在一行内完成声明:var x, y int 让 x 和 y 都是 int
类型变量。同样可以使用 平行赋值:
a, b := 20, 16
让 a 和 b 都是整数变量,并且赋值 20 给 a,16 给 b。
一个特殊的变量名是 _(下划线)。任何赋给它的值都被丢弃。在这个例子中,将 35
赋值给 b,同时丢弃 34。
_, b := 34, 35
Go 的编译器对声明却未使用的变量在报错。下面的代码会产生这个错误:声明了 i
却未使用
package main
func main {
var i int
}
布尔类型
布尔类型表示由预定义的常量 true 和 false 代表的布尔判定值。布尔类型是 bool。
数字类型
Go 有众所周知的类型如 int,这个类型根据你的硬件决定适当的长度。意味着在 32 位
硬件上,是 32 位的;在 64 位硬件上是 64 位的。注意:int 是 32 或 64 位之一,不会
定义成其他值。uint 情况相同。变量、类型和关键字 5
如果你希望明确其长度,你可以使用 int32 或者 uint32。完整的整数类型列表(符
号和无符号)是 int8,int16,int32,int64 和 byte,uint8,uint16,uint32,uint64。byte 是 uint8 的别名。浮点类型的值有 ?oat32 和 ?oat64 (没有 float 类
型)。64 位的整数和浮点数总是 64 位的,即便是在 32 位的架构上。
需要留意的是这些类型全部都是独立的,并且混合用这些类型向变量赋值会引起编译
器错误,例如下面的代码:
Listing 1.4. 相似的类型都是独立的
1 package main
3 func main {
4 var a int 通用整数类型
5 var b int32 32 位整数类型
6 a = 15
7 b = a + a 混合这些类型是非法的
8 b = b + 5 5 是一个(未定义类型的)常量,所以这没?问题
9 }
在行 7 触发一个赋值错误:
types.go:7: cannot use a + a (type int) as type int32 in assignment
赋值可以用八进制、十六进制或科学计数法:077,0xFF,1e3 或者 6.022e23 这些都
是合法的。
常量
常量在 Go 中,也就是 constant。它们在编译时被创建,只能是数字、字符串或布尔
值;const x = 42 生成 x 这个常量。可以使用 iota c
生成枚举值。
const (
a = iota
b = iota)
第一个 iota 表示为 0,因此 a 等于 0,当 iota 再次在新的一行使用时,它的值增加
了 1,因此 b 的值是 1。
也可以像下面这样,省略 Go 重复的 = iota:
const (
a = iota
b Implicitly b = iota)
如果需要,可以明确指定常量的类型:
const (
a = 0 Is an int now
b s t r ing = 0)
c
单词 [iota] 在日常英语短语 “not one iota”,意思是 “不是最小”,是来自新约中的短语:“until heaven and
earth pass away, not an iota, not a dot, will pass from the Law.”[27]6 Chapter 1: 简介
字符串
另一个重要的内建类型是 string。赋值字符串的例子:
s := Hello World!
字符串在 Go 中是 UTF-8 的由双引号(”)包裹的字符序列。如果你使用单引号(’)则
表示一个字符(UTF-8 编码)——这种在 Go 中不是 string。
一旦给变量赋值,字符串就不能修改了:在 Go 中字符串是不可变的。从 C 来的用户,下面的情况在 Go 中是非法的。
var s s t r ing = hello
s[0] = 'c' 修改第一个字符为’c’,这会报错
在 Go 中实现这个,需要下面的方法:
s := hello
c := []rune(s) . .
0
c[0] = 'c' . .
1
s2 := s t r ing(c) . .
2
fmt.Printf(%s\n, s2) . .
3
. .
0 转换 s 为 rune 数组,查阅在第 4 章 “转换” 节、59 页的内容;
. .
1 修改数组的第一个元素;
. .
2 创建新的字符串 s2 保存修改;
. .
3 用 fmt.Printf 函数输出字符串。
多行字符串
基于分号的置入(查阅文档 [8] 的 “分号” 章节),你需要小心使用多行字符
串。如果这样写:
s := Starting part
+ Ending part
会被转换为:
s := Starting part ;
+ Ending part ;
这是错误的语法,应当这样写:
s := Starting part +
Ending part
Go 就不会在错误的地方插入分号。另一种方式是使用反引号 ` 作为原始字符串符
号:
s := `Starting part
Ending part`
留意最后一个例子 s 现在也包含换行。不像转义字符串标识 ,原始字符串标识的值
在引号内的字符是不转义的。运算符和内建函数 7
rune
Rune 是 int32 的别名。用 UTF-8 进行编码。这个类型在什么时候使用呢?例如需要遍
历字符串中的字符。可以循环每个字节(仅在使用 US ASCII 编码字符串时与字符等价,而它们在 Go 中不存在!)。因此为了获得实际的字符,需要使用 rune 类型。
复数
Go 原生支持复数。它的变量类型是 complex128 (64 位虚数部分)。如果需要小一些
的,还有 complex64 – 32 位的虚数部分。复数写为 re + imi,re 是实数部分,im 是
虚数部分,而 i 是标记i’ (
p8 Chapter 1: 简介
Go 关键字
Table 1.2. Go 中的关键字
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
表格 1.2 列出了 Go 中所有的关键字。在下面的段落和章节中会介绍它们。其中有一些
已经遇到过了。
var 和 const 参阅 “变量、类型和关键字” 在第 3 页;
在 “Hello World” 部分,package 和 import 已经有过短暂的接触。在第 3 章对其
有详细的描述。
其他都有对应的介绍和章节:
func 用于定义函数和方法;
return 用于从函数返回,func 和 return 参阅第 2 章了解详细信息;
go 用于并行,参阅第 6 章;
select 用于选择不同类型的通讯,参阅第 6 章;
interface 参阅第 5 章;
struct 用于抽象数据类型,参阅第 4 章;
type 同样参阅第 4 章。
控制结构
在 Go 中只有很少的几个控制结构 d。例如这里没有 do 或者 while 循环,只有 for。有
(灵活的)switch 语句和 if,而 switch 接受像 for 那样可选的初始化语句。还有叫
做类型选择和多路通讯转接器的 select(参阅第 6 章)。语法有所不同(同 C 相比):
无需圆括号,而语句体必须总是包含在大括号内。
if-eles
在 Go 中 if 看起来是这样的:
i f x > 0 { { 是强制的
return y
} else {
return x
}
d这个章节复制于 [8]。控制结构 9
强制大括号鼓励将简单的 if 语句写在多行上。无论如何,这都是一个很好的形式,尤
其是语句体中含有控制语句,例如 return 或者 break。
if 和 switch 接受初始化语句,通常用于设置一个(局部)变量。
i f err := Chmod(0664) ; err != nil { nil 与 C 的 NULL 类似
fmt.Printf(err) err 的作用域被限定在 if 内
return err
}
可以像通常那样使用逻辑运算符(参考 1.1 表格):
i f true true {
fmt.Println(true)
}
i f ! false {
fmt.Println(true)
}
在 Go 库中,你会发现当一个 if 语句不会进入下一个语句流程 – 也就是说,语句体结
束于 break,continue,goto 或者 return – 不必要的 else 会被省略。
f, err := os.Open(name, os.O_RDONLY, 0)
i f err != nil {
return err
}
doSomething(f)
这个例子通常用于检测可能的错误序列。成功的流程一直执行到底部使代码很好读,当遇到错误的时候就排除它。这样错误的情况结束于 return 语句,这样就无须 else
语句。
f, err := os.Open(name, os.O_RDONLY, 0)
i f err != nil {
return err
}
d, err := f.Stat
i f err != nil {
return err
}
doSomething(f, d)
下面的语法在 Go 中是非法的:
i f err != nil
{ 必须同 if 在同一行
return err
}
参阅文档 [8] 的 “分号” 章节了解其后更深入的原因。10 Chapter 1: 简介
goto
Go 有 goto 语句——明智的使用它。用goto 跳转到一定是当前函数内定义的标签。例
如假设这样一个循环:
func myfunc {
i := 0
Here: 这行的第一个词,以分号结束作为标签
println(i)
i++
goto Here 跳转
}
标签名是大小写敏感的。
for
Go 的 for 循环有三种形式,只有其中的一种使用分号。
for init ; condition ; post { } 和 C 的 for 一样
for condition { } 和 while 一样
for { } 死循环
短声明使得在循环中声明一个序号变量更加容易。
sum := 0
for i := 0 ; i < 10 ; i++ {
sum += i sum = sum + i 的简化写法
} i 实例在循环结束会?失
最后,由于 Go 没有逗号表达式,而 ++ 和 – 是语句而不是表达式,如果你想在 for 中
执行多个变量,应当使用 平行赋值。
Reverse a
for i, j := 0, len(a)-1 ; i < j ; i, j = i+1, j-1 {
a[i], a[j] = a[j], a[i] 平行赋值
}
break 和 continue
利用 break 可以提前退出循环,break 终止当前的循环。
for i := 0 ; i < 10 ; i++ {
i f i > 5 {
break 终止这个循环,只打印 0 到 5
}
println(i)
}
循环嵌套循环时,可以在 break 后指定标签。用标签决定哪个循环被终止:控制结构 11
J: for j := 0 ; j < 5 ; j++ {
for i := 0 ; i < 10 ; i++ {
i f i > 5 {
break J 现在终止的是 j 循环,而不是 i 的那个
}
println(i)
}
}
利用 continue 让循环进入下一个迭代,而略过剩下的所有代码。下面循环打印了 0
到 5。
for i := 0 ; i < 10 ; i++ {
i f i > 5 {
continue 跳过循环中所有的代码println(i)
range
关键字 range 可用于循环。它可以在 slice、array、string、map 和 channel(参阅第 6
章)。range 是个迭代器,当被调用的时候,从它循环的内容中返回一个键值对。基于
不同的内容,range 返回不同的东西。
当对 slice 或者 array 做循环时,range 返回序号作为键,这个序号对应的内容作为值。
考虑这个代码:
list := []s t r ing {a, b, c, d, e, f}
. .
0
for k, v := range list {
. .
1
对 k 和 v 做想做的事情 . .
2
}
. .
0 创建一个字符串的 slice(参阅 “array、slices 和 map” 的第 14 页)。
. .
1 用 range 对其进行循环。每一个迭代,range 将返回 int 类型的序号,string
类型的值,以 0 和 “a” 开始。
. .
2 k 的值为 0…5,而 v 在循环从 “a”…“f”。
也可以在字符串上直接使用 range。这样字符串被打散成独立的 Unicode 字符 e
并且
起始位按照 UTF-8 解析。循环:
for pos, char := range ax {
fmt.Printf(character '%c' starts at byte position %d\n, char
, pos)
}
打印
e在 UTF-8 世界的字符有时被称作 runes。通常,当人们讨论字符时,多数是指 8 位字符。UTF-8 字符可
能会有 32 位,称作 rune。在这个例子里,char 的类型是 rune。12 Chapter 1: 简介
character 'a' starts at byte position 0
character '' starts at byte position 1
character 'x' starts at byte position 3 took 2 bytes
switch
Go的switch非常灵活。表达式不必是常量或整数,执行的过程从上至下,直到找到匹
配项,而如果switch没有表达式,它会匹配true。这产生一种可能——使用switch
编写 if-else-if-else 判断序列。
func unhex(c byte) byte {
switch {
case '0' <= c c <= '9':
return c - '0'
case 'a' <= c c <= 'f':
return c - 'a' + 10
case 'A' <= c c <= 'F':
return c - 'A' + 10
}
return 0
}
它不会匹配失败后自动向下尝试,但是可以使用 fallthrough 使其这样做。没
有 fallthrough:
switch i {
case 0: 空的 case 体
case 1:
f 当 i == 0 时,f 不会被调用!
}
而这样:
switch i {
case 0: fal l through
case 1:
f 当 i == 0 时,f 会被调用!
}
用 default 可以指定当其他所有分支都不匹配的时候的行为。
switch i {
case 0:
case 1:
f
defaul t:
g 当 i 不等于 0 或 1 时调用
}
分支可以使用逗号分隔的列表。内建函数 13
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '', '=', '', '+': , as ”or”
return true
}
return false
}
这里有一个使用两个 switch 对字节数组进行比较的例子:
. .
0
func Compare(a, b []byte) int {
for i := 0 ; i < len(a) i < len(b) ; i++ {
switch {
case a[i] > b[i]:
return 1
case a[i] < b[i]:
return -1
}
}
switch {
. .
1
case len(a) < len(b):
return -1
case len(a) > len(b):
return 1
}
return 0 . .
2
}
. .
0 比较返回两个字节数组字典数序先后的整数。
如果 a == b 返回 0,如果 a < b 返回 -1,而如果 a > b 返回 +1;
. .
1 长度不同,则不相等;
. .
2 字符串相等。
内建函数
预定义了少数函数,这意味着无需引用任何包就可以使用它们。表格 1.3 列出了所有
的内建函数。f
Table 1.3. Go 中的预定义函数
close new panic complex
delete make recover real
len append print imag
cap copy println
f
可以使用命令 go doc builtin 获得关于内建类型和函数的在线文档。14 Chapter 1: 简介
这些内建函数的文档记录在跟随最近的 Go 版本一起发布的伪包 builtin 中。
close
用于 channel 通讯。使用它来关闭 channel,参阅第 6 章了解更多。
delete
用于在 map 中删除实例。
len 和 cap
可用于不同的类型,len用于返回字符串、slice和数组的长度。参阅“array、slices
和 map” 小节了解更多关于 slice、数组和函数 cap 的详细信息。
new
用于各种类型的内存分配。参阅 “用 new 分配内存” 的第 55 页。
make
用于内建类型(map、slice 和 channel)的内存分配。参阅 “用 make 分配内存”
的第 55 页。
copy
用于复制 slice。参阅本章的 “slice”。
append
用于追加 slice。参阅本章的 “slice”。
panic 和 recover
用于异常处理机制。参阅 “恐慌(Panic)和恢复(Recover)” 的第 32 页了解更
多信息。
print 和 println
是底层打印函数,可以在不引入 fmt 包的情况下使用。它们主要用于调试。
complex、real 和 imag
全部用于处理 复数。有了之前给的简单的例子,不用再进一步讨论复数了。
array、slices 和 map
可以利用 array 在列表中进行多个值的排序,或者使用更加灵活的:slice。字典或哈希
类型同样可以使用,在 Go 中叫做 map。
array
array 由 [n] 定义,n 标示 array 的长度,而 标示希望存储的内容的类
型。对 array 的元素赋值或索引是由方括号完成的:
var arr [10]int
arr[0] = 42
arr[1] = 13
fmt.Printf(The first element is %d\n, arr[0])
像 var arr = [10]int 这样的数组类型有固定的大小。大小是类型的一部分。由于不
同的大小是不同的类型,因此不能改变大小。数组同样是值类型的:将一个数组赋值array、slices 和 map 15
给另一个数组,会复制所有的元素。尤其是当向函数内传递一个数组的时候,它会获
得一个数组的副本,而不是数组的指针。
可以像这样声明一个数组:var a [3]int,如果不使用零来初始化它,则用复合声明:
a := [3]int{1, 2, 3} 也可以简写为 a := [...]int{1, 2, 3},Go 会自动统计元素
的个数。 复合声明允许你
直 接 将 值 赋 值
给 array、slice 或
者 map。
参阅第56页的“构
造函数与复合声
明” 了解更多信息。
注意,所有项目必须都指定。因此,如果你使用多维数组,有一些内容你必须录入:
a := [3][2]int { [2]int {1,2}, [2]int {3,4}, [2]int {5,6} }
类似于:
a := [3][2]int { [...]int {1,2}, [...]int {3,4}, [...]int {5,6} }
声明一个 array 时,你必须在方括号内输入些内容,数字或者三个点 (...)。在很久之
前,这个语法被进一步简化,这里是来自之前的发布日志:
array、slice 和 map 的复合声明变得更加简单。使用复合声明的 array、slice
和 map,元素复合声明的类型与外部一致,则可以省略。
这表示上面的例子可以修改为:
a := [3][2]int { {1,2}, {3,4}, {5,6} }
slice
slice 与 array 接近,但是在新的元素加入的时候可以增加长度。slice 总是指向底层的
一个 array。slice 是一个指向 array 的指针,这是其与 array 不同的地方;slice 是引用
类型,这意味着当赋值某个 slice 到另外一个变量,两个引用会指向同一个 array。例 引 用 类 型 使
用 make 创建。 如,如果一个函数需要一个 slice 参数,在其内对 slice 元素的修改也会体现在函数调
用者中,这和传递底层的 array 指针类似。通过:
sl := make([]int , 10)
创建了一个保存有 10 个元素的 slice。需要注意的是底层的 array 并无不同。slice 总
是与一个固定长度的 array 成对出现。其影响 slice 的容量和长度。 图 1.1 描述了下面
的 Go 代码。首先创建了 m 个元素长度的 array,元素类型 int:var array[m]int
然后对这个 array 创建 slice:slice := array[0:n]
然后现在有:16 Chapter 1: 简介
Figure 1.1. array 与 slice 对比
len(slice)== n ; cap(slice)== m ; len(array)== cap(array)== m .
. . .
len == cap == m
array
slice
0
0
n-1
n-1
m-1
len == n
. . m-1
cap == m
给定一个 array 或者其他 slice,一个新 slice 通过 a[I:J] 的方式创建。这会创建一个
新的 slice,指向变量 a,从序号 I 开始,结束在序号 J 之前。长度为 J - I。
array[n:m] 从 array 创建了一个 slice,具有元素 n 到 m-1
a := [...]int {1, 2, 3, 4, 5}
. .
0
s1 := a[2:4] . .
1array、slices 和 map 17
s2 := a[1:5] . .
2
s3 := a[:] . .
3
s4 := a[:4] . .
4
s5 := s2[:] . .
5
. .
0 定义一个 5 个元素的 array,序号从 0 到 4;
. .
1 从序号 2 至 3 创建 slice,它包含元素 3, 4;
. .
2 从序号 1 至 4 创建,它包含元素 2, 3, 4, 5;
. .
3 用 array 中的所有元素创建 slice,这是 a[0:len(a)] 的简化写法;
. .
4 从序号 0 至 3 创建,这是 a[0:4] 的简化写法,得到 1, 2, 3, 4;
. .
5 从 slice s2 创建 slice,注意 s5 仍然指向 array a。
在1.5列出的代码中,我们在第八行尝试做一些错误的事情,让一些东西超出范围(底
层 array 的最大长度),然后得到了一个运行时错误。
Listing 1.5. array 和 slice
1 package main
3 func main {
4 var array [100]int Create array, index from 0 to 99
5 slice := array[0:99] Create slice, index from 0 to 98
7 slice[98] = 'a' OK
8 slice[99] = 'a' Error: ”throw: index out of range”
9 }
如果你想要扩展slice,有一堆内建函数让你的日子更加好过一些:append和copy。来
自于 [10]:
函数 append 向 slice s 追加零值或其他 x 值,并且返回追加后的新的、与 s
有相同类型的 slice。如果 s 没有足够的容量存储追加的值,append 分配一
个足够大的、新的 slice 来存放原有 slice 的元素和追加的值。因此,返回
的 slice 可能指向不同的底层 array。
s0 := []int {0, 0}
s1 := append(s0, 2) . .
0
s2 := append(s1, 3, 5, 7) . .
1
s3 := append(s2, s0...) . .
2
. .
0 追加一个元素,s1 == []int{0, 0, 2};
. .
1 追加多个元素,s2 == []int{0, 0, 2, 3, 5, 7};
. .
2 追加一个 slice,s3 == []int{0, 0, 2, 3, 5, 7, 0, 0}。注意这三个点!18 Chapter 1: 简介
还有
函数 copy 从源 slice src 复制元素到目标 dst,并且返回复制的元素的个
数。源和目标可能重?。元素复制的数量是 len(src) 和 len(dst) 中的最
小值。
var a = [...]int {0, 1, 2, 3, 4, 5, 6, 7}
var s = make([]int , 6)
n1 := copy(s, a[0:]) n1 == 6, s == []int{0, 1, 2, 3, 4, 5}
n2 := copy(s, s[2:]) n2 == 4, s == []int{2, 3, 4, 5, 4, 5}
map
许多语言都内建了类似的类型,例如 Perl 有哈希,Python 有字典,而 C++ 同样也
有 map(作为库)。在 Go 中有 map 类型。map 可以认为是一个用字符串做索引的数
组(在其最简单的形式下)。下面定义了 map 类型,用于将 string (月的缩写)转换
为 int – 那个月的天数。一般定义 map 的方法是:map[]
monthdays := map[s t r ing]int {
Jan: 31, Feb: 28, Mar: 31,Apr: 30, May: 31, Jun: 30,Jul: 31, Aug: 31, Sep: 30,Oct: 31, Nov: 30, Dec: 31, 逗号是必须的
}
留意,当只需要声明一个map的时候,使用make的形式:monthdays := make(map
[string]int)
当在 map 中索引(搜索)时,使用方括号。例如打印出 12 月的天数:fmt.Printf(
%d\n, monthdays[Dec])
当对 array、slice、string 或者 map 循环遍历的时候,range 会帮助你,每次调用,它
都会返回一个键和对应的值。
year := 0
for _, days := range monthdays { 键没有使用,因此用 _, days
year += days
}
fmt.Printf(Numbers of days in a year: %d\n, year)
向 map 增加元素,可以这样做:
monthdays[Undecim] = 30 添加一个月
monthdays[Feb] = 29 侟年时重写这个元素
检查元素是否存在,可以使用下面的方式 [19]:
var value int
var present bool
value, present = monthdays[Jan] 如果存在,present 则有值 true
或者更接近 Go 的方式
v, ok := monthdays[Jan] “逗号 ok”形式练习 19
也可以从 map 中移除元素:
delete(monthdays, Mar) 删除”Mar” 吧,总是下傗的月?
通常来说语句 delete(m, x) 会删除 map 中由 m[x] 建立的实例。
练习
Q1. (0) For-loop
1. 创建一个基于 for 的简单的循环。使其循环 10 次,并且使用 fmt 包打印出计数
器的值。
2. 用 goto 改写 1 的循环。关键字 for 不可使用。
3. 再次改写这个循环,使其遍历一个 array,并将这个 array 打印到屏幕上。
Q2. (0) FizzBuzz
1. 解决这个叫做 Fizz-Buzz[23] 的问题:
编写一个程序,打印从 1 到 100 的数字。当是三个?数就打印 “Fizz”
代替数字,当是?的?数就打印 “Buzz”。当数字同时是三和?的?数
时,打印 “FizzBuzz”。
Q3. (1) 字符串
1. 建立一个 Go 程序打印下面的内容(到 100 个字符):
A
AA
AAA
AAAA
AAAAA
AAAAAA
AAAAAAA...
2. 建立一个程序统计字符串里的字符数量:
asSASA ddd dsjkdsjs dk
同时输出这个字符串的字节数。提示: 看看 unicodeutf8 包。
3. 扩展修改上一个问题的程序,替换位置 4 开始的三个字符为 “abc”。
4. 编写一个 Go 程序可以逆转字符串,例如 “foobar” 被打印成 “raboof”。提示:不
幸的是你需要知道一些关于转换的内容,参阅 “转换” 第 59 页的内容。
Q4. (1) 平均值
1. 编写计算一个类型是 float64 的 slice 的平均值的代码。在稍候的练习 Q5 中
将会改写为函数。答案 21
答案
A1. (0) For-loop
1. 有许多种解法,其中一种可能是:
Listing 1.6. 循环示例
package main
import fmt
func main {
for i := 0 ; i < 10 ; i++ { See page 10
fmt.Printf(%d\n, i)
}
}
编译并观察输出。
% go build for.go
% .for
0
1
.
.
.
9
2. 改写的循环最终看起来像这样(仅显示了 main 函数):
func main {
i := 0 定义循环变量
Loop: 定义标签
fmt.Printf(%d\n, i)
i f i < 10 {
i++
goto Loop 跳转回标签
}
}
3. 下面是可能的解法之一:
Listing 1.7. 用于数组的 for 循环
func main {
var arr [10]int Create an array with 10 elements
for i := 0 ; i < 10 ; i++ {
arr[i] = i Fill it one by one
}
fmt.Printf(%v, arr) With %v Go prints the value for us22 Chapter 1: 简介
}
也可以用复合声明的硬编码来实现这个:
a := [...]int {0,1,2,3,4,5,6,7,8,9} 让 Go 来计数
fmt.Printf(%v\n, a)
A2. (0) FizzBuzz
1. 下面简单的程序,是一种解决办法。
Listing 1.8. Fizz-Buzz
package main
import fmt
func main {
const (
FIZZ = 3 . .
0
BUZZ = 5)
var p bool
. .
1
for i := 1 ; i < 100 ; i++ {
. .
2;
p = false
i f i%FIZZ == 0 {
. .
3
fmt.Printf(Fizz)
p = true
}
i f i%BUZZ == 0 {
. .
4
fmt.Printf(Buzz)
p = true
}
i f !p {
. .
5
fmt.Printf(%v, i)
}
fmt.Println . .
6
}
}
. .
0 为了提高代码的可读性,定义两个常量。参阅 “常量”;
. .
1 判断是否需要打印内容;
. .
2 for 循环,参阅 “for”
. .
3 如果能被 FIZZ 整除,打印 “Fizz”;
. .
4 如果能被 BUZZ 整除,打印 “Buzz”。注意,FizzBuzz 的情况已经被处理了;
. .
5 如果 FIZZ 和 BUZZ 都没有打印,打印原始值;
. .
6 换行。答案 23
A3. (1) 字符串
1. 这是一个解法:
Listing 1.9. 字符串
package main
import fmt
func main {
str := A
for i := 0 ; i < 100 ; i++ {
fmt.Printf(%s\n, str)
str = str + A String concatenation
}
}
2. 为了解决这个问题,需要 unicodeutf8 包的帮助。首先,阅读一下文档 go doc
unicodeutf8 | less。在阅读文档的时候,会注意到 func RuneCount(p []
byte)int。然后,将 string 转换为 byte slice:
str := hello
b := []byte(str) 转换,参阅第 59 页
将这些整合到一起,得到下面的程序。
Listing 1.10. 字符串中的 rune
package main
import (
fmt
unicodeutf8)
func main {
str := dsjkdshdjsdh....js
fmt.Printf(String %s\nLength: %d, Runes: %d\n, str,len([]byte(str)), utf8.RuneCount([]byte(str)))
}24 Chapter 1: 简介
3. 如下几行代码:
package main
import (
fmt)
func main {
s := 答案 25
. .
2 为了能够进行除法,必须将值转换为 float64。2 函数
“我总是兴奋于阳光的轻抚和沉寂在早期
编程语言中。无需太多文字;许多已经完
成了。旧的程序阅读起来就像是同表达良
好的研究工作者或受到良好训练的机器同
事沟通一样,而不是与编译器争论。谁愿
意让其成熟到发出这样的声音呢?”
RICHARD P. GABRIEL
函数是构建Go 程序的基础部件;所遇有趣的事情都是在它其中发生的。函数的定义看
起来像这样:
Listing 2.1. 函数定义
. .
. .
0
.
. .
1
.
. .
2
.
. .
3
.
. .
4
.
. .
5
type mytype int 新的类型,参阅第 4 章
func (p mytype) funcname(q int) (r,s int) { return 0,0 }
. .
0 关键字 func 用于定义一个函数;
. .
1 函数可以绑定到特定的类型上。这叫做 接收者。有接收者的函数被称作 method。
第 5 章将对其进行说明;
. .
2 funcname 是你函数的名字;
. .
3 int 类型的变量 q 作为输入参数。参数用 pass-by-value 方式传递,意味着它们会
被复制;
. .
4 变量 r 和 s 是这个函数的 命名返回值。在 Go 的函数中可以返回多个值。参阅
第28页的“多值返回”。如果不想对返回的参数命名,只需要提供类型:(int,int)。
如果只有一个返回值,可以省略圆括号。如果函数是一个子过程,并且没有任何
返回值,也可以省略这些内容;
. .
5 这是函数体。注意 return 是一个语句,所以包裹参数的括号是可选的。
这里有两个例子,左边的函数没有返回值,右边的只是简单的将输入返回。
func subroutine(in int) { return }
func identity(in int) int { return in }
可以随意安排函数定义的顺序,编译器会在执行前扫描每个文件。所以函数原型在 Go
中都是过期的旧物。Go 不允许函数嵌套,然而你可以利用匿名函数实现它,参阅本章
第 31 页的 “函数作为值”。
递归函数跟其他语言是一样的:作用域 27
Listing 2.2. 递归函数
func rec(i int) {
i f i == 10 {
return
}
rec(i+1)
fmt.Printf(%d , i)
}
这会打印:9 8 7 6 5 4 3 2 1 0。
作用域
在 Go 中,定义在函数外的变量是全局的,那些定义在函数内部的变量,对于函数来说
是局部的。如果命名覆盖——一个局部变量与一个全局变量有相同的名字——在函数
执行的时候,局部变量将覆盖全局变量。
Listing 2.3. 局部作用域
.
package main
var a = 6
func main {
p
q
p
}
func p {
println(a)
}
func q {
a := 5 定义
println(a)
}
Listing 2.4. 全局作用域
.
package main
var a = 6
func main {
p
q
p
}
func p {
println(a)
}
func q {
a = 5 赋值
println(a)
}
在 2.3 中定义了函数 q 的局部变量 a。局部变量 a 仅在 q 中可见。这也就是为什
么代码会打印:656。在 2.4 中没有定义局部变量,只有全局变量 a。这将使得对 a 的
赋值全局可见。这段代码将会打印:655。
在下面的例子中,我们在 f 中调用 g:
Listing 2.5. 当函数调用函数时的作用域
package main28 Chapter 2: 函数
var a int
func main {
a = 5
println(a)
f
}
func f {
a := 6
println(a)
g
}
func g {
println(a)
}
输出内容将是:565。局部变量仅仅在执行定义它的函数时有效。
多值返回
Go 一个非常特别的特性(对于编译语言而言)是函数和方法可以返回多个值(Python
和 Perl 同样也可以)。这可以用于改进一大堆在 C 程序中糟糕的惯例用法:修改参数
的方式,返回一个错误(例如遇到 EOF 则返回 -1)。在 Go 中,Write 返回一个计数值
和一个错误:“是的,你写入了一些字节,但是由于设备异常,并不是全部都写入了。”。os 包中的 File.Write 是这样声明的:
func (file File) Write(b []byte) (n int , err er ror)
如同文档所述,它返回写入的字节数,并且当 n != len(b) 时,返回非 nil 的 error。
这是 Go 中常见的方式。
元组没有作为原生类型出现,所以多返回值可能是最佳的选择。你可以精确的返回希
望的值,而无须重载域空间到特定的错误信号上。
命名返回值
Go 函数的返回值或者结果参数可以指定一个名字,并且像原始的变量那样使用,就像
输入参数那样。如果对其命名,在函数开始时,它们会用其类型的零值初始化。如果
函数在不加参数的情况下执行了 return 语句,结果参数会返回。用这个特性,允许
(再一次的)用较少的代码做更多的事 a。
名字不是强制的,但是它们可以使得代码更加健壮和清晰:这是文档。例如命名 int
类型的 nextPos 返回值,就能说明哪个代表哪个。
func nextInt(b []byte, pos int) (value, nextPos int) { ... }
a这是 Go 的格言:“用更少的代码做更多的事”。延迟代码 29
由于命名结果会被初始化并关联于无修饰的 return,它们可以非常简单并且清晰。这
里有一段 io.ReadFull 的代码,很好的运用了它:
func ReadFull(r Reader, buf []byte) (n int , err er ror) {
for len(buf) > 0 err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:len(buf)]
}
return
}
延迟代码
假设有一个函数,打开文件并且对其进行若干读写。在这样的函数中,经常有提前返
回的地方。如果你这样做,就需要关闭正在工作的文件描述符。这经常导致产生下面
的代码:
Listing 2.6. 没有 defer
func ReadWrite bool {
file.Open(file)
做一些工作
i f failureX {
file.Close
return false
}
i f failureY {
file.Close
return false
}
file.Close
return true
}
在这里有许多重复的代码。为了解决这些,Go 有了 defer 语句。在 defer 后指定的
函数会在函数退出前调用。
上面的代码可以被改写为下面这样。将 Close 对应的放置于 Open 后,能够使函数更
加可读、健壮。
Listing 2.7. 有 defer
func ReadWrite bool {
file.Open(file)
defer file.Close file.Close 被添加到了 defer 列表
做一些工作
i f failureX {30 Chapter 2: 函数
return false Close 现在自动调用
}
i f failureY {
return false 这里也是
}
return true And here
}
可以将多个函数放入 “延迟列表”中,这个例子来自 [8]:
for i := 0 ; i < 5 ; i++ {
defer fmt.Printf(%d , i)
}
延迟的函数是按照后进先出(LIFO)的顺序执行,所以上面的代码打印:4 3 2 1 0。
利用 defer 甚至可以修改返回值,假设正在使用命名结果参数和函数符号 b,例如:
Listing 2.8. 函数符号
defer func {
...
} 在这里是必须的
或者这个例子,更加容易了解为什么,以及在哪里需要括号:
Listing 2.9. 带参数的函数符号
defer func(x int) {
...
}(5) 为输入参数 x 赋值 5
在这个(匿名)函数中,可以访问任何命名返回参数:
Listing 2.10. 在 defer 中访问返回值
func f (ret int) { ret 初始化为零
defer func {
ret++ ret 增加为 1
}
return 0 返回的是 1 而不是 0!
}
变参
接受不定数量的参数的函数叫做变参函数。定义函数使其接受变参:
func myfunc(arg ...int) { }
arg ...int告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是int。
在函数体中,变量 arg 是一个 int 类型的 slice:
b函数符号也就是被叫做闭包的东西。函数作为值 31
for _, n := range arg {
fmt.Printf(And the number is: %d\n, n)
}
如果不指定变参的类型,默认是空的接口 interface{}(参阅第 5 章)。假设有另一
个变参函数叫做 myfunc2,下面的例子演示了如何向其传递变参:
func myfunc(arg ...int) {
myfunc2(arg...) 按原样传递
myfunc2(arg[:2]...) 传递部分
}
函数作为值
就像其他在 Go 中的其他东西一样,函数也是值而已。它们可以像下面这样赋值给变
量:
Listing 2.11. 匿名函数
func main {
a := func { 定义一个匿名函数,并且赋值给 a
println(Hello)
} 这里没有
a 调用函数
}
如果使用 fmt.Printf(\%T\n, a) 打印 a 的类型,输出结果是 func。
函数作为值,也会被用在其他地方,例如 map。这里将整数转换为函数:
Listing 2.12. 使用 map 的函数作为值
var xs = map[int]func int {
1: func int { return 10 },2: func int { return 20 },3: func int { return 30 }, 必须有逗号
...
}
也可以编写一个接受函数作为参数的函数,例如用于操作 int 类型的 slice 的 Map 函
数。这是一个留给读者的练习,参考在第 34 页的练习 Q11。
回调
由于函数也是值,所以可以很容易的传递到其他函数里,然后可以作为回调。首先定
义一个函数,对整数做一些 “事情”:
func printit(x int) { 函数无返回值
fmt.Printf(%v\n, x) 仅仅打印
}32 Chapter 2: 函数
这个函数的标识是func printit(int),或者没有函数名的:func(int)。创建新的函数
使用这个作为回调,需要用到这个标识:
func callback(y int , f func(int)) { f 将会保存函数
f(y) 调用回调函数 f 输入变量 y
}
恐慌(Panic)和恢复(Recover)
Go 没有像 Java 那样的异常机制,例如你无法像在 Java 中那样抛出一个异常。作为替
代,它使用了恐慌和恢复(panic-and-recover)机制。一定要记得,这应当作为最后的
手段被使用,你的代码中应当没有,或者很少的令人恐慌的东西。这是个强大的工具,明智的使用它。那么,应该如何使用它呢。
下面的描述来自于 [7]:
Panic
是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。当函
数 F 调用 panic,函数 F 的执行被中断,并且 F 中的延迟函数会正常执行,然
后 F 返回到调用它的地方。在调用的地方,F 的行为就像调用了 panic。这一过
程继续向上,直到程序崩溃时的所有 goroutine 返回。
恐慌可以直接调用 panic 产生。也可以由运行时错误产生,例如访问越界的数
组。
Recover
是一个内建的函数,可以让进入令人恐慌的流程中的goroutine恢复过来。 recover
仅在延迟函数中有效。
在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果。如果
当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢
复正常的执行。
这个函数检查作为其参数的函数在执行时是否会产生 panic c
:
func throwsPanic(f func(b bool) {
. .
0
defer func {
. .
1
i f x := recover ; x != nil {
b = true
}
}
f . .
2
return . .
3
}
. .
0 定义一个新函数 throwsPanic 接受一个函数作为参数(参看 “函数作为值”)。函
数 f 产生 panic,就返回 true,否则返回 false;
c
复制于 Eleanor McHugh 的演讲稿练习 33
. .
1 定义了一个利用 recover 的 defer 函数。如果当前的 goroutine 产生了 panic,这个 defer 函数能够发现。当 recover 返回非 nil 值,设置 b 为 true;
. .
2 调用作为参数接收的函数。
. .
3 返回 b 的值。由于 b 是命名返回值
(第 28 页),无须指定 b。
练习
Q5. (0) 平均值
1. 编写一个函数用于计算一个 float64 类型的 slice 的平均值。
Q6. (0) 整数顺序
1. 编写函数,返回其(两个)参数正确的(自然)数字顺序:
f(7,2) ! 2,7
f(2,7) ! 2,7
Q7. (1) 作用域
1. 下面的程序有什么错误?
1 package main
3 import fmt
5 func main {
6 for i := 0 ; i < 10 ; i++ {
7 fmt.Printf(%v\n, i)
8 }
9 fmt.Printf(%v\n, i)
10 }
Q8. (1) 栈
1. 创建一个固定大小保存整数的栈。它无须超出限制的增长。定义 push 函数——
将数据放入栈,和 pop 函数——从栈中取得内容。栈应当是后进先出(LIFO)
的。
Figure 2.1. 一个简单的 LIFO 栈
push(k)
pop k
k
i
l
m
i++
i--
034 Chapter 2: 函数
2. 更进一步。编写一个 String 方法将栈转化为字符串形式的表达。可以这样的
方式打印整个栈:fmt.Printf(My stack %v\n, stack)
栈可以被输出成这样的形式:[0:m] [1:l] [2:k]
Q9. (1) 变参
1. 编写函数接受整数类型变参,并且每行打印一个数字。
Q10. (1) 斐波那契
1. 斐波那契数列以:1; 1; 2; 3; 5; 8; 13; : : :开始。或者用数学形式表达:x1 = 1; x2 =
1; xn = xn练习 35
Q14. (1) 函数返回一个函数
1. 编写一个函数返回另一个函数,返回的函数的作用是对一个整数 +2。函数的名
称叫做 plusTwo。然后可以像下面这样使用:
p := plusTwo
fmt.Printf(%v\n, p(2))
应该打印 4。参阅第 31 页的 “回调” 小节了解更多相关信息。
2. 使 1 中的函数更加通用化,创建一个 plusX(x) 函数,返回一个函数用于对整
数加上 x。答案 37
答案
A5. (0) 平均值
1. 下面的函数计算平均值。
Listing 2.13. Go 中的平均值函数
func average(xs []f loat64) (avg f loat64) {
. .
0
sum := 0.0
switch len(xs) {
case 0: . .
1
avg = 0
defaul t: . .
2
for _, v := range xs {
sum += v
}
avg = sum f loat64(len(xs)) . .
3
}
return . .
4
}
. .
0 可以使用命名返回值;
. .
1 如果长度是零,返回 0;
. .
2 否则,计算平均值;
. .
3 为了使除法能正常计算,必须将值转换为 float64;
. .
4 得到平均值,返回它
A6. (0) 整数顺序
1. 这里可以利用 Go 中的多返回值(参阅 “多值返回” 小节):
func order(a, b int) (int , int) {
i f a > b {
return b,a
}
return a,b
}
A7. (1) 作用域
1. 这个程序不能被编译,由于第 9 行的变量 i,未定义:i 仅在 for 循环中有效。
为了修正这个,main 应修改为:
func main {
var i int
for i = 0 ; i < 10 ; i++ {
fmt.Printf(%v\n, i)38 Chapter 2: 函数
}
fmt.Printf(%v\n, i)
}
现在 i 在 for 循环外定义,并且在其后仍然可访问。这会打印数字从 0 到 10。
A8. (1) 栈
1. 首先定义一个新的类型来表达栈;需要一个数组(来保存键)和一个指向最后
一个元素的索引。这个小栈只能保存 10 个元素。
type stack s t ruc t { 栈不应该被导出
i int
data [10]int
}
然后需要push和pop函数来使用这个。首先展示一下错误的解法!在Go的数
据传递中,是值传递,意味着一个副本被创建并传递给函数。push 函数的第一
个版本大约是这样:
func (s stack) push(k int) { 工作于参数的副本
i f s.i+1 > 9 {
return
}
s.data[s.i] = k
s.i++
}
函数对 stack 类型的变量 s 进行处理。调用这个,只需要 s.push(50),将整
数 50 放入栈中。但是 push 函数得到的是 s 的副本,所以它不会有真正的结果。
用这个方法,不会有内容放入栈中,例如下面的代码:
var s stack 让 s 是一个 stack 变量
s.push(25)
fmt.Printf(stack %v\n, s) ;
s.push(14)
fmt.Printf(stack %v\n, s) ;
打印:
stack [0:0]
stack [0:0]
为了解决这个,需要向函数 push 提供一个指向栈的指针。这意味着需要修
改 push
func (s stack)push(k int) ! func (s stack)push(k int)
应当使用 new(参阅第 4 章 “用 new 分配内存” 小节)创建指针指向的 stack
的空间,因此例子中的第 1 行需要是 s := new(stack)
而两个函数变为:答案 39
func (s stack) push(k int) {
s.data[s.i] = k
s.i++
}
func (s stack) pop int {
s.i--
return s.data[s.i]
}
像下面这样使用
func main {
var s stack
s.push(25)
s.push(14)
fmt.Printf(stack %v\n, s)
}
2. 这里有一个额外的问题,对于这个练习中编写打印栈的代码的时候非常有价
值。根据 Go 文档 fmt.Printf(\%v) 可以打印实现了 Stringer 接口的任何
值(%v)。为了使其工作,需要为类型定义一个 String 函数:
Listing 2.14. stack.String
func (s stack) String s t r ing {
var str s t r ing
for i := 0 ; i <= s.i ; i++ {
str = str + [ +
strconv.Itoa(i) + : + strconv.Itoa
(s.data[i]) + ]
}
return str
}
A9. (1) 变参
1. 需要使用 ... 语法来实现函数接受若干个数字作为变参。
Listing 2.15. 有变参的函数
package main
import fmt
func main {
prtthem(1, 4, 5, 7, 4)
prtthem(1, 2, 4)
}40 Chapter 2: 函数
func prtthem(numbers ... int) { numbers 现在是整数类型的 slice
for _, d := range numbers {
fmt.Printf(%d\n, d)
}
}
A10. (1) 斐波那契
1. 下面的程序会计算出斐波那契数列。
Listing 2.16. Go 编写的斐波那契函数
package main
import fmt
func fibonacci(value int) []int {
x := make([]int , value) . .
0
x[0], x[1] = 1, 1 . .
1
for n := 2 ; n < value ; n++ {
x[n] = x[n-1] + x[n-2] . .
2
}
return x . .
3
}
func main {
for _, term := range fibonacci(10) {
. .
4
fmt.Printf(%v , term)
}
}
. .
0 创建一个用于保存函数执行结果的 array;
. .
1 开始计算斐波那契数列;
. .
2 xn = xn答案 41
return j
}
func main {
m := []int {1, 3, 4}
f := func(i int) int {
return i i
}
fmt.Printf(%v, (Map(f, m)))
}
2. 字符串问题的答案
A12. (0) 最小值和最大值
1. 这个函数返回 slice l 中的最大整数:
func max(l []int) (max int) {
. .
0
max = l[0]
for _, v := range l {
. .
1
i f v > max {
. .
2
max = v
}
}
return . .
3
}
. .
0 使用了命名返回参数;
. .
1 对 l 循环。元素的序号不重要;
. .
2 如果找到了新的最大值,记住它;
. .
3 一个 “遥远的” 返回,当前的 max 值被返回。
2. 这个函数返回 slice l 中的最小整数,这几乎与 max 完全一致。
func min(l []int) (min int) {
min = l[0]
for _, v := range l {
i f v < min {
min = v
}
}
return
}
有心的读者可能已经将 max 和 min 合成一个函数,用一个选择来判断是取最小
值还是最大值,或者两个值都返回。
A13. (1) 冒泡排序42 Chapter 2: 函数
1. 冒泡排序并不是最有效率的,对于 n 个元素它的算法复杂度是 O(n2)。快速排
序 [17] 是更好的排序算法。
但是冒泡排序容易实现。
Listing 2.18. 冒泡排序
func main {
n := []int {5, -1, 0, 12, 3, 5}
fmt.Printf(unsorted %v\n, n)
bubblesort(n)
fmt.Printf(sorted %v\n, n)
}
func bubblesort(n []int) {
for i := 0 ; i < len(n) - 1 ; i++ {
for j := i + 1 ; j < len(n) ; j++ {
i f n[j] < n[i] {
n[i], n[j] = n[j], n[i]
}
}
}
}
由于 slice 是一个引用类型,bubblesort 函数可以工作,并且无须返回排序后
的 slice。
A14. (1) 函数返回一个函数
1. func main {
p2 := plusTwo
fmt.Printf(%v\n,p2(2))
}
func plusTwo func(int) int {
. .
0
return func(x int) int { return x + 2 }
. .
1
}
. .
0 定义新的函数返回一个函数。看看你写的跟要表达的意思是如何的;
. .
1 函数符号,在返回语句中定义了一个 +2 的函数。
2. 这里我们使用闭包:
func plusX(x int) func(int) int {
. .
0
return func(y int) int { return x + y }
. .
1
}
. .
0 再次定义一个函数返回一个函数;
. .
1 在函数符号中使用局部变量 x。3 包
“^”
对是否有按位非的运算符的回答。
KEN THOMPSON
包是函数和数据的集合。用 package 关键字定义一个包。文件名不需要与包名
一致。包名的约定是使用小写字符。Go 包可以由多个文件组成,但是使用相同的
package 这一行。让我们在文件 even.go 中定义一个叫做 even 的包。
Listing 3.1. 一个小包
package even 开始自定义的包
func Even(i int) bool { 可导出函数
return i % 2 == 0
}
func odd(i int) bool { 私有函数
return i % 2 == 1
}
名称以大写字母起始的是可导出的,可以在包的外部调用(稍候会对此进行讨论)。
现在只需要构建这个包。在 GOPATH 下建立一个目录,复制 even.go 到这个目录(参
阅第 1 章的 “编译和运行代码”)。
% mkdir GOPATHsrceven
% cp even.go GOPATHsrceven
% go build
% go install
现在就可以在程序 myeven.go 中使用这个包:
Listing 3.2. even 包的使用
package main
import ( . .
0
even . .
1
fmt . .
2)
func main {
i := 5
fmt.Printf(Is %d even? %v\n, i, even.Even(i)) . .
3
}标识符 45
. .
0 导入下面的包;
. .
1 本地包 even 在这里导入;
. .
2 官方 fmt 包导入;
. .
3 调用 even 包中的函数。访问一个包中的函数的语法是.Function。
% go build myeven.go
% .myeven
Is 5 even? false
在 Go 中,当函数的首字母大写的时候,函数会被从包中导出(在包外部可见,或者说
公有的),因此函数名是 Even。如果修改 myeven.go 的第 10 行,使用未导出的函数
even.odd:
fmt.Printf(Is %d even? %v\n, i, even.odd(i))
由于使用了私有的函数,会得到一个编译错误:
myeven.go:10: cannot refer to unexported name even.odd
概括来说:
公有函数的名字以大写字母开头;
私有函数的名字以小写字母开头。
这个规则同样适用于定义在包中的其他名字(新类型、全局变量)。注意,“大写” 的含
义并不仅限于 US ASCII,它被扩展到了所有大小写字母表(拉丁文、希腊文、斯拉夫
文、亚美尼亚文和埃及古文)。
标识符
像在其他语言中一样,Go 的命名是很重要的。在某些情况下,它们甚至有语义上的作
用:例如,在包外是否可见决定于首字母是不是大写。因此有必要花点时间讨论一下
Go 程序的命名规则。
使用的规则是让众所周知的缩写保持原样,而不是去尝试到底哪里应该大写。Atoi,Getwd,Chmod。
驼峰式对那些有完整单词的会很好:ReadFile,NewWriter,MakeSlice。
包名
当包导入(通过 import)时,包名成为了内容的入口。在
import bytes
之后,导入包的可以调用函数 bytes.Buffer。任何使用这个包的人,可以使用同样
的名字访问到它的内容,因此这样的包名是好的:短的、简洁的、好记的。根据规则,包名是小写的一个单词;不应当有下划线或混合大小写。保持简洁(由于每个人都可
能需要录入这个名字),不要过早考虑命名冲突。
包名是导入的默认名称。可以通过在导入语句指定其他名称来覆盖默认名称:46 Chapter 3: 包
import bar bytes
函数 Buffer 现在可以通过 bar.Buffer 来访问。这意味着,包名无需全局唯一;在少
有的冲突中,可以给导入的包选择另一个名字在局部使用。在任何时候,冲突都是很
少见的,因为导入的文件名会用来做判断,到底是哪个包使用了。
另一个规则是包名就是代码的根目录名;在srcpkgcompressgzip的包,作为compressgzip
导入,但名字是 gzip,不是 compress_gzip 也不是 compressGzip。
导入包将使用其名字引用到内容上,所以导入的包可以利用这个避免罗嗦。例如,缓冲类型 bu?o 包的读取方法,叫做 Reader,而不是 BufReader,因为用户看到的
是 bufio.Reader 这个清晰、简洁的名字。更进一步说,由于导入的实例总是它们
包名指向的地址,bufio.Reader 不会与 io.Reader 冲突。类似的,ring.Ring(包
containerring)创建新实例的函数——在Go中定义的构造函数——通常叫做NewRing,但是由于 Ring 是这个包唯一的一个导出的类型,同时,这个包也叫做 ring,所以它可
以只称作 New。包的客户看到的是 ring.New。用包的结构帮助你选择更好的名字。
另外一个简短的例子是 once.Do(参看 sync);once.Do(setup) 读起来很不错,并且
命名为 once.DoOrWaitUntilDone(setup) 不会有任何帮助。长的名字不会让其变得
容易阅读。如果名字表达了一些复杂并且微妙的内容,更好的办法是编写一些有帮助
的注释,而不是将所有信息都放入名字里。
最后,在Go 中使用混合大小写 MixedCaps或者 mixedCaps,而不是下划线区分含有多
个单词的名字。
包的文档 这段复制于 [8]。
每个包都应该有包注释,在 package 前的一个注释块。对于多文件包,包注释只需要
出现在一个文件前,任意一个文件都可以。包注释应当对包进行介绍,并提供相关于
包的整体信息。这会出现在 go doc 生成的关于包的页面上,并且相关的细节会一并
显示。来自官方 regexp 包的例子:
The regexp package implements a simple library for
regular expressions.
The syntax of the regular expressions accepted is:
regexp:
concatenation '|' concatenation
package regexp
每个定义(并且导出)的函数应当有一小段文字描述该函数的行为。来自于 fmt 包的
例子:
Printf formats according to a format specifier and writes to standard
output. It returns the number of bytes written and any write error
encountered.
func Printf(format string, a ...interface) (n int, err error)测试包 47
测试包
在 Go 中为包编写单元测试应当是一种习惯。编写测试需要包含 testing 包和程序 go
test。两者都有良好的文档。
go test 程序调用了所有的测试函数。even 包没有定义任何测试函数,执行 go test,这样:
% go test
even [no test files]
在测试文件中定义一个测试来修复这个。测试文件也在包目录中,被命名为
_test.go。这些测试文件同 Go 程序中的其他文件一样,但是 go test 只会执
行测试函数。每个测试函数都有相同的标识,它的名字以 Test 开头:
func TestXxx(t testing.T)
编写测试时,需要告诉 go test 测试是失败还是成功。测试成功则直接返回。当测
试失败可以用下面的函数标记 [11]。这是非常重要的(参阅 go doc testing 或 go
help testfunc 了解更多):
func (t T) Fail
Fail 标记测试函数失败,但仍然继续执行。
func (t T) FailNow
FailNow 标记测试函数失败,并且中断其执行。当前文件中的其余的测试将被跳过,然后执行下一个文件中的测试。
func (t T) Log(args ...inter face { })
Log 用默认格式对其参数进行格式化,与 Print 类似,并且记录文本到错误日志。
func (t T) Fatal(args ...inter face { })
Fatal 等价于 Log 后跟随 FailNow。
将这些凑到一起,就可以编写测试了。首先,选择名字 even_test.go。然后添加下面
的内容:
Listing 3.3. even 包的测试
1 package even
3 import testing
5 func TestEven(t testing.T) {
6 i f ! Even(2) {
7 t.Log(2 should be even!)
8 t.Fail
9 }
10 }48 Chapter 3: 包
注意在第一行使用了 package even,测试使用与被测试的包使用相同的名字空间。这
不仅仅是为了方便,也允许了测试未导出的函数和结构。然后导入 testing 包,并且在
第 5 行定义了这个文件中唯一的测试函数。展示的 Go 代码应当没有任何惊异的地方:
检查了 Even 函数是否工作正常。现在等待了好久的时刻到了,执行测试:
% go test
ok even 0.001s
测试执行并且报告 ok。成功了!
如果重新定义测试函数,就可以看到一个失败的测试:
Entering the twilight zone
func TestEven(t testing.T) {
i f Even(2) {
t.Log(2 should be odd!)
t.Fail
}
}
然后得到:
FAIL even 0.004s--- FAIL: TestEven (0.00 seconds)
2 should be odd!
FAIL
然后你可以以此行事(修复测试的实例)
在编写包的时候应当一边写代码,一边写(一些)文档和测试函数。这可以让你的
程序更好,并且它展示了你的努力。
The Go test suite also allows you to incorperate example functions which serve as docu-
mentation and as tests. These functions need to start with Example.
func ExampleEven {
i f Even(2) {
fmt.Printf(Is even\n)
}
Output:
Is even
}
Those last two comments lines are part of the example, go test uses those to check the
generated output with the text in the comments. If there is a mismatch the test fails.
常用的包
标准的 Go 代码库中包含了大量的包,并且在安装 Go 的时候多数会伴随一起安装。浏
览 GOROOTsrcpkg 目录并且查看那些包会非常有启发。无法对每个包就加以解说,不过下面的这些值得讨论:a
a描述来自包的 go doc。额外的解释用斜体。常用的包 49
fmt
包 fmt 实现了格式化的 IO 函数,这与 C 的 printf 和 scanf 类似。格式化短语
派生于 C 。一些短语(%-序列)这样使用:
%v
默认格式的值。当打印结构时,加号(%+v)会增加字段名;
%v
Go 样式的值表达;
%T
带有类型的 Go 样式的值表达;
io
这个包提供了原始的IO操作界面。它主要的任务是对os包这样的原始的IO进
行封装,增加一些其他相关,使其具有抽象功能用在公共的接口上。
bu?o
这个包实现了缓冲的 IO。它封装于 io.Reader 和 io.Writer 对象,创建了另
一个对象(Reader 和 Writer)在提供缓冲的同时实现了一些文本 IO 的功能。
sort
sort 包提供了对数组和用户定义集合的原始的排序功能。
strconv
strconv 包提供了将字符串转换成基本数据类型,或者从基本数据类型转换为字
符串的功能。
os
os 包提供了与平台无关的操作系统功能接口。其设计是 Unix 形式的。
sync
sync 包提供了基本的同步原语,例如互斥锁。
ag
ag 包实现了命令行解析。参阅 “命令行参数” 在第 92 页。
encodingjson
encodingjson 包实现了编码与解码 RFC 4627 [2] 定义的 JSON 对象。
htmltemplate
数据驱动的模板,用于生成文本输出,例如 HTML。
将模板关联到某个数据结构上进行解析。模板内容指向数据结构的元素(通常结
构的字段或者 map 的键)控制解析并且决定某个值会被显示。模板扫描结构以
便解析,而 “游标” @ 决定了当前位置在结构中的值。
nethttp
nethttp 实现了 HTTP 请求、响应和 URL 的解析,并且提供了可扩展的 HTTP 服
务和基本的 HTTP 客户端。
unsafe
unsafe 包包含了 Go 程序中数据类型上所有不安全的操作。通常无须使用这个。50 Chapter 3: 包
re?ect
re?ect 包实现了运行时反射,允许程序通过抽象类型操作对象。通常用于处理静
态类型 interface{} 的值,并且通过 Typeof 解析出其动态类型信息,通常会返回
一个有接口类型 Type 的对象。
参阅 5,第 “自省和反射” 节。
osexec
osexec 包执行外部命令。
练习
Q15. (0) stack 包
1. 参考 Q8 练习。在这个练习中将从那个代码中建立一个独立的包。为 stack 的实
现创建一个合适的包,Push、Pop 和 Stack 类型需要被导出。
2. 为这个包编写一个单元测试,至少测试 Push 后 Pop 的工作情况。
Q16. (2) 计算器
1. 使用 stack 包创建逆波兰计算器。答案 51
答案
A15. (0) stack 包
1. 在创建 stack 包时,仅有一些小细节需要修改。首先,导出的函数应当大写首字
母,因此应该是 Stack。包所在的文件被命名为 stack-as-package.go,内容
是:
Listing 3.4. 包里的 Stack
package stack
保存元素的 Stack
type Stack s t ruc t {
i int
data [10]int
}
Push 将元素?入栈中
func (s Stack) Push(k int) {
s.data[s.i] = k
s.i++
}
Pop 从栈中?出一个元素
func (s Stack) Pop (ret int) {
s.i--
ret = s.data[s.i]
return
}
2. 为了让单元测试正常工作,需要做一些准备。下面用一分钟的时间来做这些。
首先是单元测试本身。创建文件 pushpop_test.go,有如下内容:
Listing 3.5. PushPop 测试
package stack
import testing
func TestPushPop(t testing.T) {
c := new(Stack)
c.Push(5)
i f c.Pop != 5 {
t.Log(Pop doesn't give 5)
t.Fail
}
}52 Chapter 3: 包
为了让 go test 能够工作,需要将包所在文件放到 GOPATHsrc:
% mkdir GOPATHsrcstack
% cp pushpop_test.go GOPATHsrcstack
% cp stack-as-package.go GOPATHsrcstack
输出:
% go test stack
ok stack 0.001s
A16. (2) 计算器
1. 这是第一个答案:
Listing 3.6. 逆波兰计算器
package main
import (
bufio
fmt
os
strconv)
var reader bufio.Reader = bufio.NewReader(os.Stdin)
var st = new(Stack)
type Stack s t ruc t {
i int
data [10]int
}
func (s Stack) push(k int) {
i f s.i+1 > 9 {
return
}
s.data[s.i] = k
s.i++
}
func (s Stack) pop (ret int) {
s.i--
i f s.i < 0 {
s.i = 0
return
}
ret = s.data[s.i]
return答案 53
}
func main {
for {
s, err := reader.ReadString('\n')
var token s t r ing
i f err != nil {
return
}
for _, c := range s {
switch {
case c >= '0' c <= '9':
token = token + s t r ing(c)
case c == ' ':
r, _ := strconv.Atoi(token)
st.push(r)
token =
case c == '+':
fmt.Printf(%d\n, st.pop+
st.pop)
case c == '':
fmt.Printf(%d\n, st.pop
st.pop)
case c == '-':
p := st.pop
q := st.pop
fmt.Printf(%d\n, q-p)
case c == 'q':
return
defaul t:
error
}
}
}
}4 进阶
“Go 有指针,但是没有指针运算。你不能
用指针变量遍历字符串的各个字节。”
Go For C++ Programmers
GO AUTHORS
Go 有指针。然而却没有指针运算,因此它们更象是引用而不是你所知道的来自于 C
的指针。指针非常有用。在 Go 中调用函数的时候,得记得变量是值传递的。因此,为
了修改一个传递入函数的值的效率和可能性,有了指针。
通过类型作为前缀来定义一个指针’’:var p int。现在 p 是一个指向整数值的指针。
所有新定义的变量都被赋值为其类型的零值,而指针也一样。一个新定义的或者没有
任何指向的指针,有值 nil。在其他语言中,这经常被叫做空(NULL)指针,在 Go 中
就是 nil。让指针指向某些内容,可以使用取址操作符 ,像这样:
Listing 4.1. 指针的使用
var p int
fmt.Printf(%v, p) 打印 nil
var i int 定义一个整形变量 i
p = i 使得 p 指向 i
fmt.Printf(%v, p) 打印出来的内容类似 0x7ff96b81c000a
从指针获取值是通过在指针变量前置’’ 实现的:
Listing 4.2. 获取指针指向的值
p = i 获取 i 的地址
p = 8 修改 i 的值
fmt.Printf(%v\n, p) 打印 8
fmt.Printf(%v\n, i) 同上
前面已经说了,没有指针运算,所以如果这样写:p++,它表示 (p)++:首先获取指
针指向的值,然后对这个值加一。a
内存分配
Go 同样也垃圾收集,也就是说无须担心内存分配和回收。
Go 有两个内存分配原语,new 和 make。它们应用于不同的类型,做不同的工作,可能
有些迷惑人,但是规则很简单。下面的章节展示了在 Go 中如何处理内存分配,并且希
望能够让 new 和 make 之间的区别更加清晰。
a参看练习 17。内存分配 55
用 new 分配内存
内建函数 new 本质上说跟其他语言中的同名函数功能一样:new(T) 分配了零值填充
的 T 类型的内存空间,并且返回其地址,一个 T 类型的值。用 Go 的术语说,它返回
了一个指针,指向新分配的类型 T 的零值。记住这点非常重要。
这意味着使用者可以用 new 创建一个数据结构的实例并且可以直接工作。如
bytes.Buffer 的文档所述 “Buffer 的零值是一个准备好了的空缓冲。” 类似的,sync.Mutex 也没有明确的构造函数或 Init 方法。取而代之,sync.Mutex 的零值
被定义为非锁定的互斥量。
零值是非常有用的。例如这样的类型定义,57 页的”定义自己的类型” 内容。
type SyncedBuffer s t ruc t {
lock sync.Mutex
buffer bytes.Buffer
}
SyncedBuffer 的值在分配内存或定义之后立刻就可以使用。在这个片段中,p 和 v 都
可以在没有任何更进一步处理的情况下工作。
p := new(SyncedBuffer) Type SyncedBu?er,已经可以使用
var v SyncedBuffer Type SyncedBu?er,同上
用 make 分配内存
回到内存分配。内建函数 make(T, args) 与 new(T) 有着不同的功能。它只能创建
slice,map 和 channel,并且返回一个有初始值(非零)的 T 类型,而不是 T。本质
来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。
例如,一个 slice,是一个包含指向数据(内部 array)的指针,长度和容量的三项描述
符;在这些项目被初始化之前,slice 为 nil。对于 slice,map 和 channel,make 初始
化了内部的数据结构,填充适当的值。
例如,make([]int, 10, 100) 分配了 100 个整数的数组,然后用长度 10 和容量 100
创建了 slice 结构指向数组的前 10 个元素。区别是,new([]int) 返回指向新分配的内
存的指针,而零值填充的 slice 结构是指向 nil 的 slice 值。
这个例子展示了 new 和 make 的不同。
var p []int = new([]int) 分配 slice 结构内存;很少使用
var v []int = make([]int , 100) v 指向一个新分配的有 100 个整数的数组
var p []int = new([]int) 不必要的复杂例子
p = make([]int , 100, 100)
v := make([]int , 100) 更常见
务必记得 make 仅适用于 map,slice 和 channel,并且返回的不是指针。应当用 new 获
得特定的指针。56 Chapter 4: 进阶
new 分配;make 初始化
上面的两段可以简单总结为:
new(T) 返回 T 指向一个零值 T
make(T) 返回初始化后的 T
当然 make 仅适用于 slice,map 和 channel。
构造函数与复合声明
有时零值不能满足需求,必须要有一个用于初始化的构造函数,例如这个来自 os 包的
例子。
func NewFile(fd int , name s t r ing) File {
i f fd < 0 {
return nil
}
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}
有许多冗长的内容。可以使用复合声明使其更加简洁,每次只用一个表达式创建一个
新的实例。
func NewFile(fd int , name s t r ing) File {
i f fd < 0 {
return nil
}
f := File{fd, name, nil, 0} Create a new File
return f 返回 f 的地址
}
返回本地变量的地址没有问题;在函数返回后,相关的存储区域仍然存在。
事实上,从复合声明获取分配的实例的地址更好,因此可以最终将两行缩短到一行。b
return File{fd, name, nil, 0}
The items (called of a composite +literal are laid out in order and must all be 所有的项
目(称作 字段)都必须按顺序全部写上。然而,通过对元素用字段: 值成对的标识,初
始化内容可以按任意顺序出现,并且可以省略初始化为零值的字段。因此可以这样
return File{fd: fd, name: name}
b从复合声明中获取地址,意味着告诉编译器在堆中分配空间,而不是栈中。定义自己的类型 57
在特定的情况下,如果复合声明不包含任何字段,它创建特定类型的零值。表达式
new(File) 和 File{} 是等价的。
复合声明同样可以用于创建 array,slice 和 map,通过指定适当的索引和 map 键来标
识字段。在这个例子中,无论是 Enone,Eio 还是 Einval 初始化都能很好的工作,只
要确保它们不同就好了。
ar := [...]s t r ing {Enone: no error, Einval: invalid argument}
sl := []s t r ing {Enone: no error, Einval: invalid argument}
ma := map[int]s t r ing {Enone: no error, Einval: invalid argument
}
定义自己的类型
自然,Go 允许定义新的类型,通过关键字 type 实现:
type foo int
创建了一个新的类型 foo 作用跟 int 一样。创建更加复杂的类型需要用到struct 关键
字。这有个在一个数据结构中记录某人的姓名(string)和年龄(int),并且使其成
为一个新的类型的例子:
Listing 4.3. 结构体
package main
import fmt
type NameAge s t ruc t {
name s t r ing 不导出
age int 不导出
}
func main {
a := new(NameAge)
a.name = Pete ; a.age = 42
fmt.Printf(%v\n, a)
}
通常,fmt.Printf(%v\n, a) 的输出是
{Pete 42}
这很棒!Go 知道如何打印结构。如果仅想打印某一个,或者某几个结构中的字段,需
要使用 .。例如,仅仅打印名字:
fmt.Printf(%s, a.name) %s 格式化字符串58 Chapter 4: 进阶
结构字段
之前已经提到结构中的项目被称为?eld。没有字段的结构:struct {}
或者有四个c
字段的:
s t ruc t {
x, y int
A []int
F func
}
如果省略字段的名字,可以创建匿名字段,例如:
s t ruc t {
T1 字段名字是 T1
T2 字段名字是 T2
P.T3 字段名字是 T3
x, y int 字段名字是 x 和 y
}
注意首字母大写的字段可以被导出,也就是说,在其他包中可以进行读写。字段名以
小写字母开头是当前包的私有的。包的函数定义是类似的,参阅第 3 章了解更多细节。
方法
可以对新定义的类型创建函数以便操作,可以通过两种途径:
1. 创建一个函数接受这个类型的参数。
func doSomething(n1 NameAge, n2 int) { }
(你可能已经猜到了)这是 函数调用。
2. 创建一个工作在这个类型上的函数(参阅在 2.1 中定义的接收方):
func (n1 NameAge) doSomething(n2 int) { }
这是方法调用,可以类似这样使用:
var n NameAge
n.doSomething(2)
使用函数还是方法是由程序员决定的,但是如果想要满足接口(参阅下一章)就只能
使用方法。如果没有这方面的需求,那就由个人品味决定了。
使用函数还是方法完全是由程序员说了算,但是若需要满足接口(参看下一章)就必
须使用方法。如果没有这样的需求,那就完全由习惯来决定是使用函数还是方法了。
但是下面的内容一定要留意,引用自 [10]:
如果 x 可获取地址,并且 x 的方法中包含了 m,x.m 是 (x).m 更短
的写法。
c
是的,四(4)个。转换 59
根据上面所述,这意味着下面的情况不是错误:
var n NameAge 不是指针
n.doSomething(2)
这里 Go 会查找 NameAge 类型的变量 n 的方法列表,没有找到就会再查找 NameAge
类型的方法列表,并且将其转化为 (n).doSomething(2)。
下面的类型定义中有一些微小但是很重要的不同之处。同时可以参阅[10, section “Type
Declarations”]。假设有:
Mutex 数据类型有两个方法,Lock 和 Unlock。
type Mutex s t ruc t { Mutex 字段 }
func (m Mutex) Lock { Lock 实现 }
func (m Mutex) Unlock { Unlock 实现 }
现在用两种不同的风格创建了两个数据类型。
type NewMutex Mutex;
type PrintableMutex struct {Mutex }.
现在 NewMutux 等同于 Mutex,但是它没有任何 Mutex 的方法。换句话说,它的方法
是空的。
但是 PrintableMutex 已经从 Mutex 继承了方法集合。如同 [10] 所说:
PrintableMutex 的方法集合包含了 Lock 和 Unlock 方法,被绑定到其
匿名字段 Mutex。
转换
有时需要将一个类型转换为另一个类型。在 Go 中可以做到,不过有一些规则。首先,将一个值转换为另一个是由操作符(看起来像函数:byte)完成的,并且不是所有
的转换都是允许的。
Table 4.1. 合法的转换,?oat64 同 ?oat32 类似。注意,为了适配表格的显示,?oat32
被简写为 ?t32。
From b []byte i []int r []rune s string f flt32 i int
To
[]byte × []byte(s)
[]int × []int(s)
[]rune × []rune(s)
string string(b) string(i) string(r) ×
ftl32 × flt32(i)
int int(f) ×
从 string 到字节或者 ruin 的 slice。
mystring := hello this is string60 Chapter 4: 进阶
byteslice := []byte(mystring)
转换到 byte slice,每个 byte 保存字符串对应字节的整数值。注意 Go 的字符串
是 UTF-8 编码的,一些字符可能是 1、2、3 或者 4 个字节结尾。
runeslice := []rune(mystring)
转换到 rune slice,每个 rune 保存 Unicode 编码的指针。字符串中的每个字符
对应一个整数。
从字节或者整形的 slice 到 string。
b := []byte {'h','e','l','l','o'} 复合声明
s := s t r ing(b)
i := []rune {257,1024,65}
r := s t r ing(i)
对于数值,定义了下面的转换:
将整数转换到指定的(bit)长度:uint8(int);
从浮点数到整数:int(?oat32)。这会截断浮点数的小数部分;
其他的类似:?oat32(int)。
用户定义类型的转换
如何在自定义类型之间进行转换?这里创建了两个类型 Foo 和 Bar,而 Bar 是 Foo 的
一个别名:
type foo s t ruc t { int } 匿名字段
type bar foo bar 是 foo 的别名
然后:
var b bar = bar{1} 声明 b 为 bar 类型
var f foo = b 赋值 b 到 f
最后一行会引起错误:
cannot use b (type bar) as type foo in assignment(不能使用 b(类型 bar)
作为类型 foo 赋值)
这可以通过转换来修复:
var f foo = foo(b)
注意转换那些字段不一致的结构是相当困难的。同时注意,转换 b 到 int 同样会出错;
整数与有整数字段的结构并不一样。
组合
TODO(miek):work in progress Go 不是面向对象语言,因此并没有继承。但是有时又会
需要从已经实现的类型中“继承”并修改一些方法。在 Go 中可以用嵌入一个类型的方
式来实现。练习 61
练习
Q17. (1) 指针运算
1. 在正文的第 54 页有这样的文字:…这里没有指针运算,因此如果这样写:p++,它被解释为 (p)++:
首先解析引用然后增加值。
当像这样增加一个值的时候,什么类型可以工作?
2. 为什么它不能工作在所有类型上?
Q18. (2) 使用 interface 的 map 函数
1. 使用练习 Q11 的答案,利用 interface 使其更加通用。让它至少能同时工作于
int 和 string。
Q19. (1) 指针
1. 假设定义了下面的结构:
type Person s t ruc t {
name s t r ing
age int
}
下面两行之间的区别是什么?
var p1 Person
p2 := new(Person)
2. 下面两个内存分配的区别是什么?
func Set(t T) {
x = t
}
和
func Set(t T) {
x= t
}
Q20. (1) Linked List
1. Make use of the package containerlist to create a (doubly) linked list. Push the
values 1, 2 and 4 to the list and then print it.
2. Create your own linked list implementation. And perform the same actions as in ......
作者:
Miek Gieben
译者:
邢星
感谢:
Go 作者,Google
下朋友提供了帮助和贡献:
(按字母顺序)
Adam J. Gray,Alex Sychev,Alexey Chernenkov,Andrea Spadaccini,Andrey Mirtchovski,Anthony Magro,Babu Sreekanth,Ben Bullock,Bob Cunningham,Brian Fallik,Cecil
New,Damian Gryski,Dan Kortschak,David Otton,Fabian Becker,Filip Zaludek,Hadi
Amiri,Haiping Fan,Jaap Akkerhuis,JC van Winkel,Jeroen Bulten,Jinpu Hu,John
Shahid,Jonathan Kans,Joshua Stein,Makoto Inoue,Mayuresh Kathe,“mem”,Michael
Stapelberg,Olexandr Shalakhin,Paulo Pinto,Peter Kleiweg,Philipp Schmidt,Robert
Johnson,Russel Winder,Sonia Keys,Stefan Schroeder,Thomas Kapplet,T.J. Yang,“Cobold”,“Simoc”,“Uriel”y,邢星。 还有来自以下朋友的小帮助:
Alexander Katasonov, Daniele Pala, Iaroslav Tymchenko, Nicolas Kaiser, Marco Ynema.
This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 License.
Miek Gieben – ?2010 - 2012 邢星 – ?2011 - 2013本作品依照署名 -非商业性使用 -相同方式共享 3.0 Unported 许可证发布。访问 http:
creativecommons.orglicensesby-nc-sa3.0 查看该许可证副本,或写信
到 Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA。
本书所有实例代码依此方式放入公共领域。
“学习 Go 语言” 已经被翻译为:
中文,邢星;
Learning as we Go (1.0)
支持 Go 1.1 版本Contents
读者 . . . . . . . . . . iv
1 简介 1
官方文档 . . . . . . . . . 1
Hello World . . . . . . . . 2
编译和运行代码 . . . . . . . . 3
本书使用的设置 . . . . . . . . 3
变量、类型和关键字 . . . . . . . . 3
运算符和内建函数 . . . . . . . . 7
Go 关键字 . . . . . . . . . 8
控制结构 . . . . . . . . . 8
内建函数 . . . . . . . . . 13
array、slices 和 map . . . . . . . . 14
练习 . . . . . . . . . . 19
答案 . . . . . . . . . . 21
2 函数 26
作用域 . . . . . . . . . . 27
多值返回 . . . . . . . . . 28
命名返回值 . . . . . . . . 28
延迟代码 . . . . . . . . . 29
变参 . . . . . . . . . . 30
函数作为值 . . . . . . . . 31
回调 . . . . . . . . . . 31
恐慌(Panic)和恢复(Recover) . . . . . . 32
练习 . . . . . . . . . . 33
答案 . . . . . . . . . . 37
3 包 44
标识符 . . . . . . . . . . 45
包的文档 . . . . . . . . . 46
测试包 . . . . . . . . . . 47
常用的包 . . . . . . . . . 48
练习 . . . . . . . . . . 50
答案 . . . . . . . . . . 51
4 进阶 54
内存分配 . . . . . . . . . 54
定义自己的类型 . . . . . . . . 57
转换 . . . . . . . . . . 59
组合 . . . . . . . . . . 60
练习 . . . . . . . . . . 61
答案 . . . . . . . . . . 63ii Chapter: Contents
5 接口 70
方法 . . . . . . . . . . 72
接口名字 . . . . . . . . . 73
简短的例子 . . . . . . . . 73
练习 . . . . . . . . . . 78
答案 . . . . . . . . . . 79
6 并发 82
更多关于 channel . . . . . . . . 84
练习 . . . . . . . . . . 85
答案 . . . . . . . . . . 87
7 通讯 90
io.Reader . . . . . . . . . 91
一些例子 . . . . . . . . . 91
命令行参数 . . . . . . . . 92
执行命令 . . . . . . . . . 92
网络 . . . . . . . . . . 93
练习 . . . . . . . . . . 94
答案 . . . . . . . . . . 97
A 版权 106
贡献者 . . . . . . . . . . 106
许可证和版权 . . . . . . . . 107
B 索引 108
C Bibliography 110
List of Exercises
1 (0) For-loop . . . . . . . . 19
2 (0) FizzBuzz . . . . . . . . 19
3 (1) 字符串 . . . . . . . . 19
4 (1) 平均值 . . . . . . . . 19
5 (0) 平均值 . . . . . . . . 33
6 (0) 整数顺序 . . . . . . . . 33
7 (1) 作用域 . . . . . . . . 33
8 (1) 栈 . . . . . . . . . . 33
9 (1) 变参 . . . . . . . . . 34
10 (1) 斐波那契 . . . . . . . . 34
11 (1) map 函数 . . . . . . . . 34
12 (0) 最小值和最大值 . . . . . . . . 34
13 (1) 冒泡排序 . . . . . . . . 34
14 (1) 函数返回一个函数 . . . . . . . 35
15 (0) stack 包 . . . . . . . . 50
16 (2) 计算器 . . . . . . . . 50List of Exercises iii
17 (1) 指针运算 . . . . . . . . 61
18 (2) 使用 interface 的 map 函数 . . . . . . 61
19 (1) 指针 . . . . . . . . . 61
20 (1) Linked List . . . . . . . . 61
21 (1) Cat . . . . . . . . . 61
22 (2) 方法调用 . . . . . . . . 62
23 (1) 接口和编译 . . . . . . . . 78
24 (1) 指针和反射 . . . . . . . . 78
25 (2) 接口和 max . . . . . . . . 78
26 (1) Channel . . . . . . . . 85
27 (2) 斐波那契 II . . . . . . . . 85
28 (2) 进程 . . . . . . . . . 94
29 (0) 单词和字母统计 . . . . . . . . 95
30 (0) Uniq . . . . . . . . . 95
31 (2) Quine . . . . . . . . . 95
32 (1) Echo 服务 . . . . . . . . 95
33 (2) 数字游戏 . . . . . . . . 95
34 (1) Finger 守护进程 . . . . . . . . 96前言
“Go 是面向对象的语言吗?是也不是。”
FAQ
GO AUTHORS
读者
这是关于来自 Google 的 Go 语言的简介。目标是为这个新的、革命性的语言提供一个
指南。
本书假设你已经在系统中安装了 Go。
这本书的目标读者是那些熟悉编程,并且了解某些编程语言,例如 C[3],C++[21],Perl[5],Java[15],Erlang[4],Scala[16],Haskell[1]。这不是教你如何编程的书,只是
教你如何使用 Go。
学习某样新东西,最佳的方式可能是通过编写程序来探索它。因此每章都包含了若干
练习(和答案)来让你熟悉这个语言。练习标有编号 Qn,而 n 是一个数字。在练习编
号后面的圆括号中的数字指定了该题的难度。难度范围从 0 到 2:
0. 简单;
1. 中等;
2. 困难。
其后为了容易索引,提供了一个简短的标题。例如:
Q1. (1) map 函数 …
展示了难度等级1、编号Q1的关于map函数的问题。相关答案在练习的下一页。答
案的顺序和练习一致,以An开头的答案,对应编号n的练习。一些练习没有答案,它
们将用星号标记出来。
内容布局
第 1 章:简介
讨论了语言中可用的基本类型、变量和控制结构。
第 2 章:函数
会了解到函数,这是 Go 程序中的基本部件。
第 3 章:包
会了解在包中整合函数和数据。同时也将了解如何对包编写文档和进行测试。
第 4 章:进阶
会看到如何创建自定义的类型。同时也将了解 Go 中的内存分配。
第 5 章:接口
Go 不支持传统意义上的面向对象。在 Go 中接口是核心概念。读者 1
第 6 章:并发
通过go关键字,函数可以在不同的例程(叫做goroutines)中执行。通过channel
来完成这些 goroutines 之间的通讯。
第 7 章:通讯
最后一章展示了如何用接口来完成 Go 程序的其他部分。如何创建、读取和写入
文件。同时也简要了解一下网络的使用。
希望你喜欢本书,同时也喜欢上 Go 语言。
翻译
本书的内容可随意取用。这里已经有相关翻译:
中文,邢星:http:www.mikespook.comlearning-go
俄文,Michael Davydenko
Miek Gieben,2011 – miek@miek.nl
邢星,2011 – mikespook@gmail.com1 简介
“对此感兴趣,并且希望做点什么。”
在为 Go 添加复数支持时
KEN THOMPSON
什么是 Go?来自其网站 [13] 的介绍:
Go编程语言是一个使得程序员更加有效率的开源项目。Go是有表达力、简
洁、清晰和有效率的。它的并行机制使其很容易编写多核和网络应用,而
新?的类型系统允许构建有?性的模块化程序。Go 编译到机器码非常快
速,同时具有便利的垃圾回收和强大的运行时反射。它是快速的、静态类
型编译语言,但是感觉上是动态类型的,解释型语言。
Go 1 是 Go 语言的第一个稳定发布版本。本文档的所有练习都工作于 Go 1 – 如果不能
工作,那就一定是 bug。
本书使用了下面的约定:
代码、关键字和注释使用 Source Code Pro 显示;
代码中的额外标识 像这样显示;
较长的标识提供数字 – . .
1 – 详细解释在其后显示;
(如果需要)行号在右边显示;
shell 的例子使用 % 作为输入符;
用户在 shell 输入内容的例子 用黑体显示,系统反馈 用普通的黑体显示;
强调的段落会缩进,并在左边有竖线。
官方文档
Go已经有大量的文档。例如Go Tutorial [12]和Effective Go [8]。网站http:golang. 在 互 联 网 上 搜
索时,应当使用
“golang” 这个词来
代替原始的 “go”。
orgdoc 也是绝佳的起点 a。虽然并不一定要阅读这些文档,但是强烈建议这么做。
Go 1 通过叫做 go doc 的标准程序提供其文档。如果你想了解内建相关(参阅下一章
“运算符和内建函数” 小节)的文档,可以像这样获取:
% go doc builtin
在第 3 章解释了如何构造你自己的包的文档。
有一些特性让 Go 与众不同。
清晰并且简洁
Go 努力保持小并且优美,你可以在短短几行代码里做许多事情;
ahttp:golang.orgdoc 本身是由 go doc 提供服务的。2 Chapter 1: 简介
并行
Go 让函数很容易成为非常轻量的线程。这些线程在 Go 中被叫做 goroutines b;
Channel
这些 goroutines 之间的通讯由 channel[18, 25] 完成;
快速
编译很快,执行也很快。目标是跟 C 一样快。编译时间用秒计算;
安全
当转换一个类型到另一个类型的时候需要显式的转换并遵循严格的规则。Go 有
垃圾收集,在 Go 中无须 free,语言会处理这一切;
标准格式化
Go 程序可以被格式化为程序员希望的(几乎)任何形式,但是官方格式是存在
的。标准也非常简单:gofmt 的输出就是官方认可的格式;
类型后置
类型在变量名的后面,像这样 var a int,来代替 C 中的 int a;
UTF-8
任何地方都是 UTF-8 的,包括字符串以及程序代码。你可以在代码中使用 =
+ 1;
开源
Go 的许可证是完全开源的,参阅 Go 发布的源码中的 LICENSE 文件;
开心
用 Go 写程序会非常开心!
Erlang[4] 与 Go 在部分功能上类似。Erlang 和 Go 之间主要的区别是 Erlang 是函数式
语言,而 Go 是命令式的。Erlang 运行在虚拟机上,而 Go 是编译的。Go 用起来感觉更
接近 Unix。
Hello World
在 Go 指南中,用一个传统的方式展现了 Go:让它打印 “Hello World”(Ken Thompson
和 Dennis Ritchie 在 20 世纪 70 年代,发布 C 语言的时候开创了这个先河)。我们不认
为其他方法可以做得更好,所以就是这个吧:Go 的 “Hello World”。
Listing 1.1. Hello world
1 package main . .
0
3 import fmt . .
1 实现格式化的 IO
6 Print something . .
2
8 func main {
. .
3
b是的,它的发音很接近 coroutines,但是 goroutines 确实有一些不同,我们将在第 6 章讨论。编译和运行代码 3
9 . .
4
10 fmt.Printf(Hello, world ; or ? ó? ; orこんにちは
世界)
11 }
逐行阅读这个程序。
. .
0 首行这个是必须的。所有的 Go 文件以 package
开头,对于独立运行的执行文件必须是 package main;
. .
1 这是说需要将fmt 包加入main。不是main的其他包都被称为库,其他许多编程语言有
着类似的概念(参阅第 3 章)。末尾以 开头的内容是注释;
. .
2 这同样是注释,不过这是被包裹于 和 之间的;
. .
3 package main 必须首先出现,紧跟着是 import。在 Go 中,package 总是首先出现,然后是 import,然后是其他所有内容。当 Go 程序在执行的时候,首先调用的函数
是 main.main,这是从 C 中继承而来。这里定义了这个函数;
. .
4 第 8 行调用了来自于 fmt 包的函数打印字符串到屏幕。字符串由 包裹,并且可以包
含非 ASCII 的字符。这里使用了希腊文和日文。
编译和运行代码
构建 Go 程序的最佳途径是使用 go 工具。
构建 helloworld 只需要:
% go build helloworld.go
结果是叫做 helloworld 的可执行文件。
% .helloworld
Hello, world; or ? ó?; or こんにちは世界
本书使用的设置
Go 被安装在 ?go,而 GOROOT 被设置为 GOROOT=?go;
希望编译的 Go 代码放在 ?gsrc 而 GOPATH 设置为 GOPATH=?g。在使用包的
时候需要用到这个变量(参阅第 3 章)。
变量、类型和关键字
在接下来的章节中,我们将会了解这个新语言的变量、基本类型、关键字和控制流。
Go 在语法上有着类 C 的感觉。如果你希望将两个(或更多)语句放在一行书写,它们
必须用分号 (’;’) 分隔。一般情况下,你不需要分号。4 Chapter 1: 简介
Go 同其他语言不同的地方在于变量的类型在变量名的后面。不是:int a,而是 a int。
当定义了一个变量,它默认赋值为其类型的 null 值。这意味着,在 var a int后,a 的
值为 0。而 var s string,意味着 s 被赋值为零长度字符串,也就是 。
在 Go 中,声明和赋值是两过程,但是可以连在一起。比较下面作用相同的代码片段。
Listing 1.2. 用 = 声明
var a int
var b bool
a = 15
b = false
Listing 1.3. 用:= 声明
a := 15
b := false
在左边使用了关键字 var 声明变量,然后赋值给它。右边的代码使用了 := 使得在一
步内完成了声明和赋值(这一形式只可用在函数内)。在这种情况下,变量的类型是由
值推演出来的。值 15 表示是 int 类型,值 false 告诉 Go 它的类型应当是 bool。多
个 var 声明可以成组;const 和 import 同样允许这么做。留意圆括号的使用:
var (
x int
b bool)
有相同类型的多个变量同样可以在一行内完成声明:var x, y int 让 x 和 y 都是 int
类型变量。同样可以使用 平行赋值:
a, b := 20, 16
让 a 和 b 都是整数变量,并且赋值 20 给 a,16 给 b。
一个特殊的变量名是 _(下划线)。任何赋给它的值都被丢弃。在这个例子中,将 35
赋值给 b,同时丢弃 34。
_, b := 34, 35
Go 的编译器对声明却未使用的变量在报错。下面的代码会产生这个错误:声明了 i
却未使用
package main
func main {
var i int
}
布尔类型
布尔类型表示由预定义的常量 true 和 false 代表的布尔判定值。布尔类型是 bool。
数字类型
Go 有众所周知的类型如 int,这个类型根据你的硬件决定适当的长度。意味着在 32 位
硬件上,是 32 位的;在 64 位硬件上是 64 位的。注意:int 是 32 或 64 位之一,不会
定义成其他值。uint 情况相同。变量、类型和关键字 5
如果你希望明确其长度,你可以使用 int32 或者 uint32。完整的整数类型列表(符
号和无符号)是 int8,int16,int32,int64 和 byte,uint8,uint16,uint32,uint64。byte 是 uint8 的别名。浮点类型的值有 ?oat32 和 ?oat64 (没有 float 类
型)。64 位的整数和浮点数总是 64 位的,即便是在 32 位的架构上。
需要留意的是这些类型全部都是独立的,并且混合用这些类型向变量赋值会引起编译
器错误,例如下面的代码:
Listing 1.4. 相似的类型都是独立的
1 package main
3 func main {
4 var a int 通用整数类型
5 var b int32 32 位整数类型
6 a = 15
7 b = a + a 混合这些类型是非法的
8 b = b + 5 5 是一个(未定义类型的)常量,所以这没?问题
9 }
在行 7 触发一个赋值错误:
types.go:7: cannot use a + a (type int) as type int32 in assignment
赋值可以用八进制、十六进制或科学计数法:077,0xFF,1e3 或者 6.022e23 这些都
是合法的。
常量
常量在 Go 中,也就是 constant。它们在编译时被创建,只能是数字、字符串或布尔
值;const x = 42 生成 x 这个常量。可以使用 iota c
生成枚举值。
const (
a = iota
b = iota)
第一个 iota 表示为 0,因此 a 等于 0,当 iota 再次在新的一行使用时,它的值增加
了 1,因此 b 的值是 1。
也可以像下面这样,省略 Go 重复的 = iota:
const (
a = iota
b Implicitly b = iota)
如果需要,可以明确指定常量的类型:
const (
a = 0 Is an int now
b s t r ing = 0)
c
单词 [iota] 在日常英语短语 “not one iota”,意思是 “不是最小”,是来自新约中的短语:“until heaven and
earth pass away, not an iota, not a dot, will pass from the Law.”[27]6 Chapter 1: 简介
字符串
另一个重要的内建类型是 string。赋值字符串的例子:
s := Hello World!
字符串在 Go 中是 UTF-8 的由双引号(”)包裹的字符序列。如果你使用单引号(’)则
表示一个字符(UTF-8 编码)——这种在 Go 中不是 string。
一旦给变量赋值,字符串就不能修改了:在 Go 中字符串是不可变的。从 C 来的用户,下面的情况在 Go 中是非法的。
var s s t r ing = hello
s[0] = 'c' 修改第一个字符为’c’,这会报错
在 Go 中实现这个,需要下面的方法:
s := hello
c := []rune(s) . .
0
c[0] = 'c' . .
1
s2 := s t r ing(c) . .
2
fmt.Printf(%s\n, s2) . .
3
. .
0 转换 s 为 rune 数组,查阅在第 4 章 “转换” 节、59 页的内容;
. .
1 修改数组的第一个元素;
. .
2 创建新的字符串 s2 保存修改;
. .
3 用 fmt.Printf 函数输出字符串。
多行字符串
基于分号的置入(查阅文档 [8] 的 “分号” 章节),你需要小心使用多行字符
串。如果这样写:
s := Starting part
+ Ending part
会被转换为:
s := Starting part ;
+ Ending part ;
这是错误的语法,应当这样写:
s := Starting part +
Ending part
Go 就不会在错误的地方插入分号。另一种方式是使用反引号 ` 作为原始字符串符
号:
s := `Starting part
Ending part`
留意最后一个例子 s 现在也包含换行。不像转义字符串标识 ,原始字符串标识的值
在引号内的字符是不转义的。运算符和内建函数 7
rune
Rune 是 int32 的别名。用 UTF-8 进行编码。这个类型在什么时候使用呢?例如需要遍
历字符串中的字符。可以循环每个字节(仅在使用 US ASCII 编码字符串时与字符等价,而它们在 Go 中不存在!)。因此为了获得实际的字符,需要使用 rune 类型。
复数
Go 原生支持复数。它的变量类型是 complex128 (64 位虚数部分)。如果需要小一些
的,还有 complex64 – 32 位的虚数部分。复数写为 re + imi,re 是实数部分,im 是
虚数部分,而 i 是标记i’ (
p8 Chapter 1: 简介
Go 关键字
Table 1.2. Go 中的关键字
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
表格 1.2 列出了 Go 中所有的关键字。在下面的段落和章节中会介绍它们。其中有一些
已经遇到过了。
var 和 const 参阅 “变量、类型和关键字” 在第 3 页;
在 “Hello World” 部分,package 和 import 已经有过短暂的接触。在第 3 章对其
有详细的描述。
其他都有对应的介绍和章节:
func 用于定义函数和方法;
return 用于从函数返回,func 和 return 参阅第 2 章了解详细信息;
go 用于并行,参阅第 6 章;
select 用于选择不同类型的通讯,参阅第 6 章;
interface 参阅第 5 章;
struct 用于抽象数据类型,参阅第 4 章;
type 同样参阅第 4 章。
控制结构
在 Go 中只有很少的几个控制结构 d。例如这里没有 do 或者 while 循环,只有 for。有
(灵活的)switch 语句和 if,而 switch 接受像 for 那样可选的初始化语句。还有叫
做类型选择和多路通讯转接器的 select(参阅第 6 章)。语法有所不同(同 C 相比):
无需圆括号,而语句体必须总是包含在大括号内。
if-eles
在 Go 中 if 看起来是这样的:
i f x > 0 { { 是强制的
return y
} else {
return x
}
d这个章节复制于 [8]。控制结构 9
强制大括号鼓励将简单的 if 语句写在多行上。无论如何,这都是一个很好的形式,尤
其是语句体中含有控制语句,例如 return 或者 break。
if 和 switch 接受初始化语句,通常用于设置一个(局部)变量。
i f err := Chmod(0664) ; err != nil { nil 与 C 的 NULL 类似
fmt.Printf(err) err 的作用域被限定在 if 内
return err
}
可以像通常那样使用逻辑运算符(参考 1.1 表格):
i f true true {
fmt.Println(true)
}
i f ! false {
fmt.Println(true)
}
在 Go 库中,你会发现当一个 if 语句不会进入下一个语句流程 – 也就是说,语句体结
束于 break,continue,goto 或者 return – 不必要的 else 会被省略。
f, err := os.Open(name, os.O_RDONLY, 0)
i f err != nil {
return err
}
doSomething(f)
这个例子通常用于检测可能的错误序列。成功的流程一直执行到底部使代码很好读,当遇到错误的时候就排除它。这样错误的情况结束于 return 语句,这样就无须 else
语句。
f, err := os.Open(name, os.O_RDONLY, 0)
i f err != nil {
return err
}
d, err := f.Stat
i f err != nil {
return err
}
doSomething(f, d)
下面的语法在 Go 中是非法的:
i f err != nil
{ 必须同 if 在同一行
return err
}
参阅文档 [8] 的 “分号” 章节了解其后更深入的原因。10 Chapter 1: 简介
goto
Go 有 goto 语句——明智的使用它。用goto 跳转到一定是当前函数内定义的标签。例
如假设这样一个循环:
func myfunc {
i := 0
Here: 这行的第一个词,以分号结束作为标签
println(i)
i++
goto Here 跳转
}
标签名是大小写敏感的。
for
Go 的 for 循环有三种形式,只有其中的一种使用分号。
for init ; condition ; post { } 和 C 的 for 一样
for condition { } 和 while 一样
for { } 死循环
短声明使得在循环中声明一个序号变量更加容易。
sum := 0
for i := 0 ; i < 10 ; i++ {
sum += i sum = sum + i 的简化写法
} i 实例在循环结束会?失
最后,由于 Go 没有逗号表达式,而 ++ 和 – 是语句而不是表达式,如果你想在 for 中
执行多个变量,应当使用 平行赋值。
Reverse a
for i, j := 0, len(a)-1 ; i < j ; i, j = i+1, j-1 {
a[i], a[j] = a[j], a[i] 平行赋值
}
break 和 continue
利用 break 可以提前退出循环,break 终止当前的循环。
for i := 0 ; i < 10 ; i++ {
i f i > 5 {
break 终止这个循环,只打印 0 到 5
}
println(i)
}
循环嵌套循环时,可以在 break 后指定标签。用标签决定哪个循环被终止:控制结构 11
J: for j := 0 ; j < 5 ; j++ {
for i := 0 ; i < 10 ; i++ {
i f i > 5 {
break J 现在终止的是 j 循环,而不是 i 的那个
}
println(i)
}
}
利用 continue 让循环进入下一个迭代,而略过剩下的所有代码。下面循环打印了 0
到 5。
for i := 0 ; i < 10 ; i++ {
i f i > 5 {
continue 跳过循环中所有的代码println(i)
range
关键字 range 可用于循环。它可以在 slice、array、string、map 和 channel(参阅第 6
章)。range 是个迭代器,当被调用的时候,从它循环的内容中返回一个键值对。基于
不同的内容,range 返回不同的东西。
当对 slice 或者 array 做循环时,range 返回序号作为键,这个序号对应的内容作为值。
考虑这个代码:
list := []s t r ing {a, b, c, d, e, f}
. .
0
for k, v := range list {
. .
1
对 k 和 v 做想做的事情 . .
2
}
. .
0 创建一个字符串的 slice(参阅 “array、slices 和 map” 的第 14 页)。
. .
1 用 range 对其进行循环。每一个迭代,range 将返回 int 类型的序号,string
类型的值,以 0 和 “a” 开始。
. .
2 k 的值为 0…5,而 v 在循环从 “a”…“f”。
也可以在字符串上直接使用 range。这样字符串被打散成独立的 Unicode 字符 e
并且
起始位按照 UTF-8 解析。循环:
for pos, char := range ax {
fmt.Printf(character '%c' starts at byte position %d\n, char
, pos)
}
打印
e在 UTF-8 世界的字符有时被称作 runes。通常,当人们讨论字符时,多数是指 8 位字符。UTF-8 字符可
能会有 32 位,称作 rune。在这个例子里,char 的类型是 rune。12 Chapter 1: 简介
character 'a' starts at byte position 0
character '' starts at byte position 1
character 'x' starts at byte position 3 took 2 bytes
switch
Go的switch非常灵活。表达式不必是常量或整数,执行的过程从上至下,直到找到匹
配项,而如果switch没有表达式,它会匹配true。这产生一种可能——使用switch
编写 if-else-if-else 判断序列。
func unhex(c byte) byte {
switch {
case '0' <= c c <= '9':
return c - '0'
case 'a' <= c c <= 'f':
return c - 'a' + 10
case 'A' <= c c <= 'F':
return c - 'A' + 10
}
return 0
}
它不会匹配失败后自动向下尝试,但是可以使用 fallthrough 使其这样做。没
有 fallthrough:
switch i {
case 0: 空的 case 体
case 1:
f 当 i == 0 时,f 不会被调用!
}
而这样:
switch i {
case 0: fal l through
case 1:
f 当 i == 0 时,f 会被调用!
}
用 default 可以指定当其他所有分支都不匹配的时候的行为。
switch i {
case 0:
case 1:
f
defaul t:
g 当 i 不等于 0 或 1 时调用
}
分支可以使用逗号分隔的列表。内建函数 13
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '', '=', '', '+': , as ”or”
return true
}
return false
}
这里有一个使用两个 switch 对字节数组进行比较的例子:
. .
0
func Compare(a, b []byte) int {
for i := 0 ; i < len(a) i < len(b) ; i++ {
switch {
case a[i] > b[i]:
return 1
case a[i] < b[i]:
return -1
}
}
switch {
. .
1
case len(a) < len(b):
return -1
case len(a) > len(b):
return 1
}
return 0 . .
2
}
. .
0 比较返回两个字节数组字典数序先后的整数。
如果 a == b 返回 0,如果 a < b 返回 -1,而如果 a > b 返回 +1;
. .
1 长度不同,则不相等;
. .
2 字符串相等。
内建函数
预定义了少数函数,这意味着无需引用任何包就可以使用它们。表格 1.3 列出了所有
的内建函数。f
Table 1.3. Go 中的预定义函数
close new panic complex
delete make recover real
len append print imag
cap copy println
f
可以使用命令 go doc builtin 获得关于内建类型和函数的在线文档。14 Chapter 1: 简介
这些内建函数的文档记录在跟随最近的 Go 版本一起发布的伪包 builtin 中。
close
用于 channel 通讯。使用它来关闭 channel,参阅第 6 章了解更多。
delete
用于在 map 中删除实例。
len 和 cap
可用于不同的类型,len用于返回字符串、slice和数组的长度。参阅“array、slices
和 map” 小节了解更多关于 slice、数组和函数 cap 的详细信息。
new
用于各种类型的内存分配。参阅 “用 new 分配内存” 的第 55 页。
make
用于内建类型(map、slice 和 channel)的内存分配。参阅 “用 make 分配内存”
的第 55 页。
copy
用于复制 slice。参阅本章的 “slice”。
append
用于追加 slice。参阅本章的 “slice”。
panic 和 recover
用于异常处理机制。参阅 “恐慌(Panic)和恢复(Recover)” 的第 32 页了解更
多信息。
print 和 println
是底层打印函数,可以在不引入 fmt 包的情况下使用。它们主要用于调试。
complex、real 和 imag
全部用于处理 复数。有了之前给的简单的例子,不用再进一步讨论复数了。
array、slices 和 map
可以利用 array 在列表中进行多个值的排序,或者使用更加灵活的:slice。字典或哈希
类型同样可以使用,在 Go 中叫做 map。
array
array 由 [n]
型。对 array 的元素赋值或索引是由方括号完成的:
var arr [10]int
arr[0] = 42
arr[1] = 13
fmt.Printf(The first element is %d\n, arr[0])
像 var arr = [10]int 这样的数组类型有固定的大小。大小是类型的一部分。由于不
同的大小是不同的类型,因此不能改变大小。数组同样是值类型的:将一个数组赋值array、slices 和 map 15
给另一个数组,会复制所有的元素。尤其是当向函数内传递一个数组的时候,它会获
得一个数组的副本,而不是数组的指针。
可以像这样声明一个数组:var a [3]int,如果不使用零来初始化它,则用复合声明:
a := [3]int{1, 2, 3} 也可以简写为 a := [...]int{1, 2, 3},Go 会自动统计元素
的个数。 复合声明允许你
直 接 将 值 赋 值
给 array、slice 或
者 map。
参阅第56页的“构
造函数与复合声
明” 了解更多信息。
注意,所有项目必须都指定。因此,如果你使用多维数组,有一些内容你必须录入:
a := [3][2]int { [2]int {1,2}, [2]int {3,4}, [2]int {5,6} }
类似于:
a := [3][2]int { [...]int {1,2}, [...]int {3,4}, [...]int {5,6} }
声明一个 array 时,你必须在方括号内输入些内容,数字或者三个点 (...)。在很久之
前,这个语法被进一步简化,这里是来自之前的发布日志:
array、slice 和 map 的复合声明变得更加简单。使用复合声明的 array、slice
和 map,元素复合声明的类型与外部一致,则可以省略。
这表示上面的例子可以修改为:
a := [3][2]int { {1,2}, {3,4}, {5,6} }
slice
slice 与 array 接近,但是在新的元素加入的时候可以增加长度。slice 总是指向底层的
一个 array。slice 是一个指向 array 的指针,这是其与 array 不同的地方;slice 是引用
类型,这意味着当赋值某个 slice 到另外一个变量,两个引用会指向同一个 array。例 引 用 类 型 使
用 make 创建。 如,如果一个函数需要一个 slice 参数,在其内对 slice 元素的修改也会体现在函数调
用者中,这和传递底层的 array 指针类似。通过:
sl := make([]int , 10)
创建了一个保存有 10 个元素的 slice。需要注意的是底层的 array 并无不同。slice 总
是与一个固定长度的 array 成对出现。其影响 slice 的容量和长度。 图 1.1 描述了下面
的 Go 代码。首先创建了 m 个元素长度的 array,元素类型 int:var array[m]int
然后对这个 array 创建 slice:slice := array[0:n]
然后现在有:16 Chapter 1: 简介
Figure 1.1. array 与 slice 对比
len(slice)== n ; cap(slice)== m ; len(array)== cap(array)== m .
. . .
len == cap == m
array
slice
0
0
n-1
n-1
m-1
len == n
. . m-1
cap == m
给定一个 array 或者其他 slice,一个新 slice 通过 a[I:J] 的方式创建。这会创建一个
新的 slice,指向变量 a,从序号 I 开始,结束在序号 J 之前。长度为 J - I。
array[n:m] 从 array 创建了一个 slice,具有元素 n 到 m-1
a := [...]int {1, 2, 3, 4, 5}
. .
0
s1 := a[2:4] . .
1array、slices 和 map 17
s2 := a[1:5] . .
2
s3 := a[:] . .
3
s4 := a[:4] . .
4
s5 := s2[:] . .
5
. .
0 定义一个 5 个元素的 array,序号从 0 到 4;
. .
1 从序号 2 至 3 创建 slice,它包含元素 3, 4;
. .
2 从序号 1 至 4 创建,它包含元素 2, 3, 4, 5;
. .
3 用 array 中的所有元素创建 slice,这是 a[0:len(a)] 的简化写法;
. .
4 从序号 0 至 3 创建,这是 a[0:4] 的简化写法,得到 1, 2, 3, 4;
. .
5 从 slice s2 创建 slice,注意 s5 仍然指向 array a。
在1.5列出的代码中,我们在第八行尝试做一些错误的事情,让一些东西超出范围(底
层 array 的最大长度),然后得到了一个运行时错误。
Listing 1.5. array 和 slice
1 package main
3 func main {
4 var array [100]int Create array, index from 0 to 99
5 slice := array[0:99] Create slice, index from 0 to 98
7 slice[98] = 'a' OK
8 slice[99] = 'a' Error: ”throw: index out of range”
9 }
如果你想要扩展slice,有一堆内建函数让你的日子更加好过一些:append和copy。来
自于 [10]:
函数 append 向 slice s 追加零值或其他 x 值,并且返回追加后的新的、与 s
有相同类型的 slice。如果 s 没有足够的容量存储追加的值,append 分配一
个足够大的、新的 slice 来存放原有 slice 的元素和追加的值。因此,返回
的 slice 可能指向不同的底层 array。
s0 := []int {0, 0}
s1 := append(s0, 2) . .
0
s2 := append(s1, 3, 5, 7) . .
1
s3 := append(s2, s0...) . .
2
. .
0 追加一个元素,s1 == []int{0, 0, 2};
. .
1 追加多个元素,s2 == []int{0, 0, 2, 3, 5, 7};
. .
2 追加一个 slice,s3 == []int{0, 0, 2, 3, 5, 7, 0, 0}。注意这三个点!18 Chapter 1: 简介
还有
函数 copy 从源 slice src 复制元素到目标 dst,并且返回复制的元素的个
数。源和目标可能重?。元素复制的数量是 len(src) 和 len(dst) 中的最
小值。
var a = [...]int {0, 1, 2, 3, 4, 5, 6, 7}
var s = make([]int , 6)
n1 := copy(s, a[0:]) n1 == 6, s == []int{0, 1, 2, 3, 4, 5}
n2 := copy(s, s[2:]) n2 == 4, s == []int{2, 3, 4, 5, 4, 5}
map
许多语言都内建了类似的类型,例如 Perl 有哈希,Python 有字典,而 C++ 同样也
有 map(作为库)。在 Go 中有 map 类型。map 可以认为是一个用字符串做索引的数
组(在其最简单的形式下)。下面定义了 map 类型,用于将 string (月的缩写)转换
为 int – 那个月的天数。一般定义 map 的方法是:map[
monthdays := map[s t r ing]int {
Jan: 31, Feb: 28, Mar: 31,Apr: 30, May: 31, Jun: 30,Jul: 31, Aug: 31, Sep: 30,Oct: 31, Nov: 30, Dec: 31, 逗号是必须的
}
留意,当只需要声明一个map的时候,使用make的形式:monthdays := make(map
[string]int)
当在 map 中索引(搜索)时,使用方括号。例如打印出 12 月的天数:fmt.Printf(
%d\n, monthdays[Dec])
当对 array、slice、string 或者 map 循环遍历的时候,range 会帮助你,每次调用,它
都会返回一个键和对应的值。
year := 0
for _, days := range monthdays { 键没有使用,因此用 _, days
year += days
}
fmt.Printf(Numbers of days in a year: %d\n, year)
向 map 增加元素,可以这样做:
monthdays[Undecim] = 30 添加一个月
monthdays[Feb] = 29 侟年时重写这个元素
检查元素是否存在,可以使用下面的方式 [19]:
var value int
var present bool
value, present = monthdays[Jan] 如果存在,present 则有值 true
或者更接近 Go 的方式
v, ok := monthdays[Jan] “逗号 ok”形式练习 19
也可以从 map 中移除元素:
delete(monthdays, Mar) 删除”Mar” 吧,总是下傗的月?
通常来说语句 delete(m, x) 会删除 map 中由 m[x] 建立的实例。
练习
Q1. (0) For-loop
1. 创建一个基于 for 的简单的循环。使其循环 10 次,并且使用 fmt 包打印出计数
器的值。
2. 用 goto 改写 1 的循环。关键字 for 不可使用。
3. 再次改写这个循环,使其遍历一个 array,并将这个 array 打印到屏幕上。
Q2. (0) FizzBuzz
1. 解决这个叫做 Fizz-Buzz[23] 的问题:
编写一个程序,打印从 1 到 100 的数字。当是三个?数就打印 “Fizz”
代替数字,当是?的?数就打印 “Buzz”。当数字同时是三和?的?数
时,打印 “FizzBuzz”。
Q3. (1) 字符串
1. 建立一个 Go 程序打印下面的内容(到 100 个字符):
A
AA
AAA
AAAA
AAAAA
AAAAAA
AAAAAAA...
2. 建立一个程序统计字符串里的字符数量:
asSASA ddd dsjkdsjs dk
同时输出这个字符串的字节数。提示: 看看 unicodeutf8 包。
3. 扩展修改上一个问题的程序,替换位置 4 开始的三个字符为 “abc”。
4. 编写一个 Go 程序可以逆转字符串,例如 “foobar” 被打印成 “raboof”。提示:不
幸的是你需要知道一些关于转换的内容,参阅 “转换” 第 59 页的内容。
Q4. (1) 平均值
1. 编写计算一个类型是 float64 的 slice 的平均值的代码。在稍候的练习 Q5 中
将会改写为函数。答案 21
答案
A1. (0) For-loop
1. 有许多种解法,其中一种可能是:
Listing 1.6. 循环示例
package main
import fmt
func main {
for i := 0 ; i < 10 ; i++ { See page 10
fmt.Printf(%d\n, i)
}
}
编译并观察输出。
% go build for.go
% .for
0
1
.
.
.
9
2. 改写的循环最终看起来像这样(仅显示了 main 函数):
func main {
i := 0 定义循环变量
Loop: 定义标签
fmt.Printf(%d\n, i)
i f i < 10 {
i++
goto Loop 跳转回标签
}
}
3. 下面是可能的解法之一:
Listing 1.7. 用于数组的 for 循环
func main {
var arr [10]int Create an array with 10 elements
for i := 0 ; i < 10 ; i++ {
arr[i] = i Fill it one by one
}
fmt.Printf(%v, arr) With %v Go prints the value for us22 Chapter 1: 简介
}
也可以用复合声明的硬编码来实现这个:
a := [...]int {0,1,2,3,4,5,6,7,8,9} 让 Go 来计数
fmt.Printf(%v\n, a)
A2. (0) FizzBuzz
1. 下面简单的程序,是一种解决办法。
Listing 1.8. Fizz-Buzz
package main
import fmt
func main {
const (
FIZZ = 3 . .
0
BUZZ = 5)
var p bool
. .
1
for i := 1 ; i < 100 ; i++ {
. .
2;
p = false
i f i%FIZZ == 0 {
. .
3
fmt.Printf(Fizz)
p = true
}
i f i%BUZZ == 0 {
. .
4
fmt.Printf(Buzz)
p = true
}
i f !p {
. .
5
fmt.Printf(%v, i)
}
fmt.Println . .
6
}
}
. .
0 为了提高代码的可读性,定义两个常量。参阅 “常量”;
. .
1 判断是否需要打印内容;
. .
2 for 循环,参阅 “for”
. .
3 如果能被 FIZZ 整除,打印 “Fizz”;
. .
4 如果能被 BUZZ 整除,打印 “Buzz”。注意,FizzBuzz 的情况已经被处理了;
. .
5 如果 FIZZ 和 BUZZ 都没有打印,打印原始值;
. .
6 换行。答案 23
A3. (1) 字符串
1. 这是一个解法:
Listing 1.9. 字符串
package main
import fmt
func main {
str := A
for i := 0 ; i < 100 ; i++ {
fmt.Printf(%s\n, str)
str = str + A String concatenation
}
}
2. 为了解决这个问题,需要 unicodeutf8 包的帮助。首先,阅读一下文档 go doc
unicodeutf8 | less。在阅读文档的时候,会注意到 func RuneCount(p []
byte)int。然后,将 string 转换为 byte slice:
str := hello
b := []byte(str) 转换,参阅第 59 页
将这些整合到一起,得到下面的程序。
Listing 1.10. 字符串中的 rune
package main
import (
fmt
unicodeutf8)
func main {
str := dsjkdshdjsdh....js
fmt.Printf(String %s\nLength: %d, Runes: %d\n, str,len([]byte(str)), utf8.RuneCount([]byte(str)))
}24 Chapter 1: 简介
3. 如下几行代码:
package main
import (
fmt)
func main {
s := 答案 25
. .
2 为了能够进行除法,必须将值转换为 float64。2 函数
“我总是兴奋于阳光的轻抚和沉寂在早期
编程语言中。无需太多文字;许多已经完
成了。旧的程序阅读起来就像是同表达良
好的研究工作者或受到良好训练的机器同
事沟通一样,而不是与编译器争论。谁愿
意让其成熟到发出这样的声音呢?”
RICHARD P. GABRIEL
函数是构建Go 程序的基础部件;所遇有趣的事情都是在它其中发生的。函数的定义看
起来像这样:
Listing 2.1. 函数定义
. .
. .
0
.
. .
1
.
. .
2
.
. .
3
.
. .
4
.
. .
5
type mytype int 新的类型,参阅第 4 章
func (p mytype) funcname(q int) (r,s int) { return 0,0 }
. .
0 关键字 func 用于定义一个函数;
. .
1 函数可以绑定到特定的类型上。这叫做 接收者。有接收者的函数被称作 method。
第 5 章将对其进行说明;
. .
2 funcname 是你函数的名字;
. .
3 int 类型的变量 q 作为输入参数。参数用 pass-by-value 方式传递,意味着它们会
被复制;
. .
4 变量 r 和 s 是这个函数的 命名返回值。在 Go 的函数中可以返回多个值。参阅
第28页的“多值返回”。如果不想对返回的参数命名,只需要提供类型:(int,int)。
如果只有一个返回值,可以省略圆括号。如果函数是一个子过程,并且没有任何
返回值,也可以省略这些内容;
. .
5 这是函数体。注意 return 是一个语句,所以包裹参数的括号是可选的。
这里有两个例子,左边的函数没有返回值,右边的只是简单的将输入返回。
func subroutine(in int) { return }
func identity(in int) int { return in }
可以随意安排函数定义的顺序,编译器会在执行前扫描每个文件。所以函数原型在 Go
中都是过期的旧物。Go 不允许函数嵌套,然而你可以利用匿名函数实现它,参阅本章
第 31 页的 “函数作为值”。
递归函数跟其他语言是一样的:作用域 27
Listing 2.2. 递归函数
func rec(i int) {
i f i == 10 {
return
}
rec(i+1)
fmt.Printf(%d , i)
}
这会打印:9 8 7 6 5 4 3 2 1 0。
作用域
在 Go 中,定义在函数外的变量是全局的,那些定义在函数内部的变量,对于函数来说
是局部的。如果命名覆盖——一个局部变量与一个全局变量有相同的名字——在函数
执行的时候,局部变量将覆盖全局变量。
Listing 2.3. 局部作用域
.
package main
var a = 6
func main {
p
q
p
}
func p {
println(a)
}
func q {
a := 5 定义
println(a)
}
Listing 2.4. 全局作用域
.
package main
var a = 6
func main {
p
q
p
}
func p {
println(a)
}
func q {
a = 5 赋值
println(a)
}
在 2.3 中定义了函数 q 的局部变量 a。局部变量 a 仅在 q 中可见。这也就是为什
么代码会打印:656。在 2.4 中没有定义局部变量,只有全局变量 a。这将使得对 a 的
赋值全局可见。这段代码将会打印:655。
在下面的例子中,我们在 f 中调用 g:
Listing 2.5. 当函数调用函数时的作用域
package main28 Chapter 2: 函数
var a int
func main {
a = 5
println(a)
f
}
func f {
a := 6
println(a)
g
}
func g {
println(a)
}
输出内容将是:565。局部变量仅仅在执行定义它的函数时有效。
多值返回
Go 一个非常特别的特性(对于编译语言而言)是函数和方法可以返回多个值(Python
和 Perl 同样也可以)。这可以用于改进一大堆在 C 程序中糟糕的惯例用法:修改参数
的方式,返回一个错误(例如遇到 EOF 则返回 -1)。在 Go 中,Write 返回一个计数值
和一个错误:“是的,你写入了一些字节,但是由于设备异常,并不是全部都写入了。”。os 包中的 File.Write 是这样声明的:
func (file File) Write(b []byte) (n int , err er ror)
如同文档所述,它返回写入的字节数,并且当 n != len(b) 时,返回非 nil 的 error。
这是 Go 中常见的方式。
元组没有作为原生类型出现,所以多返回值可能是最佳的选择。你可以精确的返回希
望的值,而无须重载域空间到特定的错误信号上。
命名返回值
Go 函数的返回值或者结果参数可以指定一个名字,并且像原始的变量那样使用,就像
输入参数那样。如果对其命名,在函数开始时,它们会用其类型的零值初始化。如果
函数在不加参数的情况下执行了 return 语句,结果参数会返回。用这个特性,允许
(再一次的)用较少的代码做更多的事 a。
名字不是强制的,但是它们可以使得代码更加健壮和清晰:这是文档。例如命名 int
类型的 nextPos 返回值,就能说明哪个代表哪个。
func nextInt(b []byte, pos int) (value, nextPos int) { ... }
a这是 Go 的格言:“用更少的代码做更多的事”。延迟代码 29
由于命名结果会被初始化并关联于无修饰的 return,它们可以非常简单并且清晰。这
里有一段 io.ReadFull 的代码,很好的运用了它:
func ReadFull(r Reader, buf []byte) (n int , err er ror) {
for len(buf) > 0 err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:len(buf)]
}
return
}
延迟代码
假设有一个函数,打开文件并且对其进行若干读写。在这样的函数中,经常有提前返
回的地方。如果你这样做,就需要关闭正在工作的文件描述符。这经常导致产生下面
的代码:
Listing 2.6. 没有 defer
func ReadWrite bool {
file.Open(file)
做一些工作
i f failureX {
file.Close
return false
}
i f failureY {
file.Close
return false
}
file.Close
return true
}
在这里有许多重复的代码。为了解决这些,Go 有了 defer 语句。在 defer 后指定的
函数会在函数退出前调用。
上面的代码可以被改写为下面这样。将 Close 对应的放置于 Open 后,能够使函数更
加可读、健壮。
Listing 2.7. 有 defer
func ReadWrite bool {
file.Open(file)
defer file.Close file.Close 被添加到了 defer 列表
做一些工作
i f failureX {30 Chapter 2: 函数
return false Close 现在自动调用
}
i f failureY {
return false 这里也是
}
return true And here
}
可以将多个函数放入 “延迟列表”中,这个例子来自 [8]:
for i := 0 ; i < 5 ; i++ {
defer fmt.Printf(%d , i)
}
延迟的函数是按照后进先出(LIFO)的顺序执行,所以上面的代码打印:4 3 2 1 0。
利用 defer 甚至可以修改返回值,假设正在使用命名结果参数和函数符号 b,例如:
Listing 2.8. 函数符号
defer func {
...
} 在这里是必须的
或者这个例子,更加容易了解为什么,以及在哪里需要括号:
Listing 2.9. 带参数的函数符号
defer func(x int) {
...
}(5) 为输入参数 x 赋值 5
在这个(匿名)函数中,可以访问任何命名返回参数:
Listing 2.10. 在 defer 中访问返回值
func f (ret int) { ret 初始化为零
defer func {
ret++ ret 增加为 1
}
return 0 返回的是 1 而不是 0!
}
变参
接受不定数量的参数的函数叫做变参函数。定义函数使其接受变参:
func myfunc(arg ...int) { }
arg ...int告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是int。
在函数体中,变量 arg 是一个 int 类型的 slice:
b函数符号也就是被叫做闭包的东西。函数作为值 31
for _, n := range arg {
fmt.Printf(And the number is: %d\n, n)
}
如果不指定变参的类型,默认是空的接口 interface{}(参阅第 5 章)。假设有另一
个变参函数叫做 myfunc2,下面的例子演示了如何向其传递变参:
func myfunc(arg ...int) {
myfunc2(arg...) 按原样传递
myfunc2(arg[:2]...) 传递部分
}
函数作为值
就像其他在 Go 中的其他东西一样,函数也是值而已。它们可以像下面这样赋值给变
量:
Listing 2.11. 匿名函数
func main {
a := func { 定义一个匿名函数,并且赋值给 a
println(Hello)
} 这里没有
a 调用函数
}
如果使用 fmt.Printf(\%T\n, a) 打印 a 的类型,输出结果是 func。
函数作为值,也会被用在其他地方,例如 map。这里将整数转换为函数:
Listing 2.12. 使用 map 的函数作为值
var xs = map[int]func int {
1: func int { return 10 },2: func int { return 20 },3: func int { return 30 }, 必须有逗号
...
}
也可以编写一个接受函数作为参数的函数,例如用于操作 int 类型的 slice 的 Map 函
数。这是一个留给读者的练习,参考在第 34 页的练习 Q11。
回调
由于函数也是值,所以可以很容易的传递到其他函数里,然后可以作为回调。首先定
义一个函数,对整数做一些 “事情”:
func printit(x int) { 函数无返回值
fmt.Printf(%v\n, x) 仅仅打印
}32 Chapter 2: 函数
这个函数的标识是func printit(int),或者没有函数名的:func(int)。创建新的函数
使用这个作为回调,需要用到这个标识:
func callback(y int , f func(int)) { f 将会保存函数
f(y) 调用回调函数 f 输入变量 y
}
恐慌(Panic)和恢复(Recover)
Go 没有像 Java 那样的异常机制,例如你无法像在 Java 中那样抛出一个异常。作为替
代,它使用了恐慌和恢复(panic-and-recover)机制。一定要记得,这应当作为最后的
手段被使用,你的代码中应当没有,或者很少的令人恐慌的东西。这是个强大的工具,明智的使用它。那么,应该如何使用它呢。
下面的描述来自于 [7]:
Panic
是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。当函
数 F 调用 panic,函数 F 的执行被中断,并且 F 中的延迟函数会正常执行,然
后 F 返回到调用它的地方。在调用的地方,F 的行为就像调用了 panic。这一过
程继续向上,直到程序崩溃时的所有 goroutine 返回。
恐慌可以直接调用 panic 产生。也可以由运行时错误产生,例如访问越界的数
组。
Recover
是一个内建的函数,可以让进入令人恐慌的流程中的goroutine恢复过来。 recover
仅在延迟函数中有效。
在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果。如果
当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢
复正常的执行。
这个函数检查作为其参数的函数在执行时是否会产生 panic c
:
func throwsPanic(f func(b bool) {
. .
0
defer func {
. .
1
i f x := recover ; x != nil {
b = true
}
}
f . .
2
return . .
3
}
. .
0 定义一个新函数 throwsPanic 接受一个函数作为参数(参看 “函数作为值”)。函
数 f 产生 panic,就返回 true,否则返回 false;
c
复制于 Eleanor McHugh 的演讲稿练习 33
. .
1 定义了一个利用 recover 的 defer 函数。如果当前的 goroutine 产生了 panic,这个 defer 函数能够发现。当 recover 返回非 nil 值,设置 b 为 true;
. .
2 调用作为参数接收的函数。
. .
3 返回 b 的值。由于 b 是命名返回值
(第 28 页),无须指定 b。
练习
Q5. (0) 平均值
1. 编写一个函数用于计算一个 float64 类型的 slice 的平均值。
Q6. (0) 整数顺序
1. 编写函数,返回其(两个)参数正确的(自然)数字顺序:
f(7,2) ! 2,7
f(2,7) ! 2,7
Q7. (1) 作用域
1. 下面的程序有什么错误?
1 package main
3 import fmt
5 func main {
6 for i := 0 ; i < 10 ; i++ {
7 fmt.Printf(%v\n, i)
8 }
9 fmt.Printf(%v\n, i)
10 }
Q8. (1) 栈
1. 创建一个固定大小保存整数的栈。它无须超出限制的增长。定义 push 函数——
将数据放入栈,和 pop 函数——从栈中取得内容。栈应当是后进先出(LIFO)
的。
Figure 2.1. 一个简单的 LIFO 栈
push(k)
pop k
k
i
l
m
i++
i--
034 Chapter 2: 函数
2. 更进一步。编写一个 String 方法将栈转化为字符串形式的表达。可以这样的
方式打印整个栈:fmt.Printf(My stack %v\n, stack)
栈可以被输出成这样的形式:[0:m] [1:l] [2:k]
Q9. (1) 变参
1. 编写函数接受整数类型变参,并且每行打印一个数字。
Q10. (1) 斐波那契
1. 斐波那契数列以:1; 1; 2; 3; 5; 8; 13; : : :开始。或者用数学形式表达:x1 = 1; x2 =
1; xn = xn练习 35
Q14. (1) 函数返回一个函数
1. 编写一个函数返回另一个函数,返回的函数的作用是对一个整数 +2。函数的名
称叫做 plusTwo。然后可以像下面这样使用:
p := plusTwo
fmt.Printf(%v\n, p(2))
应该打印 4。参阅第 31 页的 “回调” 小节了解更多相关信息。
2. 使 1 中的函数更加通用化,创建一个 plusX(x) 函数,返回一个函数用于对整
数加上 x。答案 37
答案
A5. (0) 平均值
1. 下面的函数计算平均值。
Listing 2.13. Go 中的平均值函数
func average(xs []f loat64) (avg f loat64) {
. .
0
sum := 0.0
switch len(xs) {
case 0: . .
1
avg = 0
defaul t: . .
2
for _, v := range xs {
sum += v
}
avg = sum f loat64(len(xs)) . .
3
}
return . .
4
}
. .
0 可以使用命名返回值;
. .
1 如果长度是零,返回 0;
. .
2 否则,计算平均值;
. .
3 为了使除法能正常计算,必须将值转换为 float64;
. .
4 得到平均值,返回它
A6. (0) 整数顺序
1. 这里可以利用 Go 中的多返回值(参阅 “多值返回” 小节):
func order(a, b int) (int , int) {
i f a > b {
return b,a
}
return a,b
}
A7. (1) 作用域
1. 这个程序不能被编译,由于第 9 行的变量 i,未定义:i 仅在 for 循环中有效。
为了修正这个,main 应修改为:
func main {
var i int
for i = 0 ; i < 10 ; i++ {
fmt.Printf(%v\n, i)38 Chapter 2: 函数
}
fmt.Printf(%v\n, i)
}
现在 i 在 for 循环外定义,并且在其后仍然可访问。这会打印数字从 0 到 10。
A8. (1) 栈
1. 首先定义一个新的类型来表达栈;需要一个数组(来保存键)和一个指向最后
一个元素的索引。这个小栈只能保存 10 个元素。
type stack s t ruc t { 栈不应该被导出
i int
data [10]int
}
然后需要push和pop函数来使用这个。首先展示一下错误的解法!在Go的数
据传递中,是值传递,意味着一个副本被创建并传递给函数。push 函数的第一
个版本大约是这样:
func (s stack) push(k int) { 工作于参数的副本
i f s.i+1 > 9 {
return
}
s.data[s.i] = k
s.i++
}
函数对 stack 类型的变量 s 进行处理。调用这个,只需要 s.push(50),将整
数 50 放入栈中。但是 push 函数得到的是 s 的副本,所以它不会有真正的结果。
用这个方法,不会有内容放入栈中,例如下面的代码:
var s stack 让 s 是一个 stack 变量
s.push(25)
fmt.Printf(stack %v\n, s) ;
s.push(14)
fmt.Printf(stack %v\n, s) ;
打印:
stack [0:0]
stack [0:0]
为了解决这个,需要向函数 push 提供一个指向栈的指针。这意味着需要修
改 push
func (s stack)push(k int) ! func (s stack)push(k int)
应当使用 new(参阅第 4 章 “用 new 分配内存” 小节)创建指针指向的 stack
的空间,因此例子中的第 1 行需要是 s := new(stack)
而两个函数变为:答案 39
func (s stack) push(k int) {
s.data[s.i] = k
s.i++
}
func (s stack) pop int {
s.i--
return s.data[s.i]
}
像下面这样使用
func main {
var s stack
s.push(25)
s.push(14)
fmt.Printf(stack %v\n, s)
}
2. 这里有一个额外的问题,对于这个练习中编写打印栈的代码的时候非常有价
值。根据 Go 文档 fmt.Printf(\%v) 可以打印实现了 Stringer 接口的任何
值(%v)。为了使其工作,需要为类型定义一个 String 函数:
Listing 2.14. stack.String
func (s stack) String s t r ing {
var str s t r ing
for i := 0 ; i <= s.i ; i++ {
str = str + [ +
strconv.Itoa(i) + : + strconv.Itoa
(s.data[i]) + ]
}
return str
}
A9. (1) 变参
1. 需要使用 ... 语法来实现函数接受若干个数字作为变参。
Listing 2.15. 有变参的函数
package main
import fmt
func main {
prtthem(1, 4, 5, 7, 4)
prtthem(1, 2, 4)
}40 Chapter 2: 函数
func prtthem(numbers ... int) { numbers 现在是整数类型的 slice
for _, d := range numbers {
fmt.Printf(%d\n, d)
}
}
A10. (1) 斐波那契
1. 下面的程序会计算出斐波那契数列。
Listing 2.16. Go 编写的斐波那契函数
package main
import fmt
func fibonacci(value int) []int {
x := make([]int , value) . .
0
x[0], x[1] = 1, 1 . .
1
for n := 2 ; n < value ; n++ {
x[n] = x[n-1] + x[n-2] . .
2
}
return x . .
3
}
func main {
for _, term := range fibonacci(10) {
. .
4
fmt.Printf(%v , term)
}
}
. .
0 创建一个用于保存函数执行结果的 array;
. .
1 开始计算斐波那契数列;
. .
2 xn = xn答案 41
return j
}
func main {
m := []int {1, 3, 4}
f := func(i int) int {
return i i
}
fmt.Printf(%v, (Map(f, m)))
}
2. 字符串问题的答案
A12. (0) 最小值和最大值
1. 这个函数返回 slice l 中的最大整数:
func max(l []int) (max int) {
. .
0
max = l[0]
for _, v := range l {
. .
1
i f v > max {
. .
2
max = v
}
}
return . .
3
}
. .
0 使用了命名返回参数;
. .
1 对 l 循环。元素的序号不重要;
. .
2 如果找到了新的最大值,记住它;
. .
3 一个 “遥远的” 返回,当前的 max 值被返回。
2. 这个函数返回 slice l 中的最小整数,这几乎与 max 完全一致。
func min(l []int) (min int) {
min = l[0]
for _, v := range l {
i f v < min {
min = v
}
}
return
}
有心的读者可能已经将 max 和 min 合成一个函数,用一个选择来判断是取最小
值还是最大值,或者两个值都返回。
A13. (1) 冒泡排序42 Chapter 2: 函数
1. 冒泡排序并不是最有效率的,对于 n 个元素它的算法复杂度是 O(n2)。快速排
序 [17] 是更好的排序算法。
但是冒泡排序容易实现。
Listing 2.18. 冒泡排序
func main {
n := []int {5, -1, 0, 12, 3, 5}
fmt.Printf(unsorted %v\n, n)
bubblesort(n)
fmt.Printf(sorted %v\n, n)
}
func bubblesort(n []int) {
for i := 0 ; i < len(n) - 1 ; i++ {
for j := i + 1 ; j < len(n) ; j++ {
i f n[j] < n[i] {
n[i], n[j] = n[j], n[i]
}
}
}
}
由于 slice 是一个引用类型,bubblesort 函数可以工作,并且无须返回排序后
的 slice。
A14. (1) 函数返回一个函数
1. func main {
p2 := plusTwo
fmt.Printf(%v\n,p2(2))
}
func plusTwo func(int) int {
. .
0
return func(x int) int { return x + 2 }
. .
1
}
. .
0 定义新的函数返回一个函数。看看你写的跟要表达的意思是如何的;
. .
1 函数符号,在返回语句中定义了一个 +2 的函数。
2. 这里我们使用闭包:
func plusX(x int) func(int) int {
. .
0
return func(y int) int { return x + y }
. .
1
}
. .
0 再次定义一个函数返回一个函数;
. .
1 在函数符号中使用局部变量 x。3 包
“^”
对是否有按位非的运算符的回答。
KEN THOMPSON
包是函数和数据的集合。用 package 关键字定义一个包。文件名不需要与包名
一致。包名的约定是使用小写字符。Go 包可以由多个文件组成,但是使用相同的
package
Listing 3.1. 一个小包
package even 开始自定义的包
func Even(i int) bool { 可导出函数
return i % 2 == 0
}
func odd(i int) bool { 私有函数
return i % 2 == 1
}
名称以大写字母起始的是可导出的,可以在包的外部调用(稍候会对此进行讨论)。
现在只需要构建这个包。在 GOPATH 下建立一个目录,复制 even.go 到这个目录(参
阅第 1 章的 “编译和运行代码”)。
% mkdir GOPATHsrceven
% cp even.go GOPATHsrceven
% go build
% go install
现在就可以在程序 myeven.go 中使用这个包:
Listing 3.2. even 包的使用
package main
import ( . .
0
even . .
1
fmt . .
2)
func main {
i := 5
fmt.Printf(Is %d even? %v\n, i, even.Even(i)) . .
3
}标识符 45
. .
0 导入下面的包;
. .
1 本地包 even 在这里导入;
. .
2 官方 fmt 包导入;
. .
3 调用 even 包中的函数。访问一个包中的函数的语法是
% go build myeven.go
% .myeven
Is 5 even? false
在 Go 中,当函数的首字母大写的时候,函数会被从包中导出(在包外部可见,或者说
公有的),因此函数名是 Even。如果修改 myeven.go 的第 10 行,使用未导出的函数
even.odd:
fmt.Printf(Is %d even? %v\n, i, even.odd(i))
由于使用了私有的函数,会得到一个编译错误:
myeven.go:10: cannot refer to unexported name even.odd
概括来说:
公有函数的名字以大写字母开头;
私有函数的名字以小写字母开头。
这个规则同样适用于定义在包中的其他名字(新类型、全局变量)。注意,“大写” 的含
义并不仅限于 US ASCII,它被扩展到了所有大小写字母表(拉丁文、希腊文、斯拉夫
文、亚美尼亚文和埃及古文)。
标识符
像在其他语言中一样,Go 的命名是很重要的。在某些情况下,它们甚至有语义上的作
用:例如,在包外是否可见决定于首字母是不是大写。因此有必要花点时间讨论一下
Go 程序的命名规则。
使用的规则是让众所周知的缩写保持原样,而不是去尝试到底哪里应该大写。Atoi,Getwd,Chmod。
驼峰式对那些有完整单词的会很好:ReadFile,NewWriter,MakeSlice。
包名
当包导入(通过 import)时,包名成为了内容的入口。在
import bytes
之后,导入包的可以调用函数 bytes.Buffer。任何使用这个包的人,可以使用同样
的名字访问到它的内容,因此这样的包名是好的:短的、简洁的、好记的。根据规则,包名是小写的一个单词;不应当有下划线或混合大小写。保持简洁(由于每个人都可
能需要录入这个名字),不要过早考虑命名冲突。
包名是导入的默认名称。可以通过在导入语句指定其他名称来覆盖默认名称:46 Chapter 3: 包
import bar bytes
函数 Buffer 现在可以通过 bar.Buffer 来访问。这意味着,包名无需全局唯一;在少
有的冲突中,可以给导入的包选择另一个名字在局部使用。在任何时候,冲突都是很
少见的,因为导入的文件名会用来做判断,到底是哪个包使用了。
另一个规则是包名就是代码的根目录名;在srcpkgcompressgzip的包,作为compressgzip
导入,但名字是 gzip,不是 compress_gzip 也不是 compressGzip。
导入包将使用其名字引用到内容上,所以导入的包可以利用这个避免罗嗦。例如,缓冲类型 bu?o 包的读取方法,叫做 Reader,而不是 BufReader,因为用户看到的
是 bufio.Reader 这个清晰、简洁的名字。更进一步说,由于导入的实例总是它们
包名指向的地址,bufio.Reader 不会与 io.Reader 冲突。类似的,ring.Ring(包
containerring)创建新实例的函数——在Go中定义的构造函数——通常叫做NewRing,但是由于 Ring 是这个包唯一的一个导出的类型,同时,这个包也叫做 ring,所以它可
以只称作 New。包的客户看到的是 ring.New。用包的结构帮助你选择更好的名字。
另外一个简短的例子是 once.Do(参看 sync);once.Do(setup) 读起来很不错,并且
命名为 once.DoOrWaitUntilDone(setup) 不会有任何帮助。长的名字不会让其变得
容易阅读。如果名字表达了一些复杂并且微妙的内容,更好的办法是编写一些有帮助
的注释,而不是将所有信息都放入名字里。
最后,在Go 中使用混合大小写 MixedCaps或者 mixedCaps,而不是下划线区分含有多
个单词的名字。
包的文档 这段复制于 [8]。
每个包都应该有包注释,在 package 前的一个注释块。对于多文件包,包注释只需要
出现在一个文件前,任意一个文件都可以。包注释应当对包进行介绍,并提供相关于
包的整体信息。这会出现在 go doc 生成的关于包的页面上,并且相关的细节会一并
显示。来自官方 regexp 包的例子:
The regexp package implements a simple library for
regular expressions.
The syntax of the regular expressions accepted is:
regexp:
concatenation '|' concatenation
package regexp
每个定义(并且导出)的函数应当有一小段文字描述该函数的行为。来自于 fmt 包的
例子:
Printf formats according to a format specifier and writes to standard
output. It returns the number of bytes written and any write error
encountered.
func Printf(format string, a ...interface) (n int, err error)测试包 47
测试包
在 Go 中为包编写单元测试应当是一种习惯。编写测试需要包含 testing 包和程序 go
test。两者都有良好的文档。
go test 程序调用了所有的测试函数。even 包没有定义任何测试函数,执行 go test,这样:
% go test
even [no test files]
在测试文件中定义一个测试来修复这个。测试文件也在包目录中,被命名为
_test.go。这些测试文件同 Go 程序中的其他文件一样,但是 go test 只会执
行测试函数。每个测试函数都有相同的标识,它的名字以 Test 开头:
func TestXxx(t testing.T)
编写测试时,需要告诉 go test 测试是失败还是成功。测试成功则直接返回。当测
试失败可以用下面的函数标记 [11]。这是非常重要的(参阅 go doc testing 或 go
help testfunc 了解更多):
func (t T) Fail
Fail 标记测试函数失败,但仍然继续执行。
func (t T) FailNow
FailNow 标记测试函数失败,并且中断其执行。当前文件中的其余的测试将被跳过,然后执行下一个文件中的测试。
func (t T) Log(args ...inter face { })
Log 用默认格式对其参数进行格式化,与 Print 类似,并且记录文本到错误日志。
func (t T) Fatal(args ...inter face { })
Fatal 等价于 Log 后跟随 FailNow。
将这些凑到一起,就可以编写测试了。首先,选择名字 even_test.go。然后添加下面
的内容:
Listing 3.3. even 包的测试
1 package even
3 import testing
5 func TestEven(t testing.T) {
6 i f ! Even(2) {
7 t.Log(2 should be even!)
8 t.Fail
9 }
10 }48 Chapter 3: 包
注意在第一行使用了 package even,测试使用与被测试的包使用相同的名字空间。这
不仅仅是为了方便,也允许了测试未导出的函数和结构。然后导入 testing 包,并且在
第 5 行定义了这个文件中唯一的测试函数。展示的 Go 代码应当没有任何惊异的地方:
检查了 Even 函数是否工作正常。现在等待了好久的时刻到了,执行测试:
% go test
ok even 0.001s
测试执行并且报告 ok。成功了!
如果重新定义测试函数,就可以看到一个失败的测试:
Entering the twilight zone
func TestEven(t testing.T) {
i f Even(2) {
t.Log(2 should be odd!)
t.Fail
}
}
然后得到:
FAIL even 0.004s--- FAIL: TestEven (0.00 seconds)
2 should be odd!
FAIL
然后你可以以此行事(修复测试的实例)
在编写包的时候应当一边写代码,一边写(一些)文档和测试函数。这可以让你的
程序更好,并且它展示了你的努力。
The Go test suite also allows you to incorperate example functions which serve as docu-
mentation and as tests. These functions need to start with Example.
func ExampleEven {
i f Even(2) {
fmt.Printf(Is even\n)
}
Output:
Is even
}
Those last two comments lines are part of the example, go test uses those to check the
generated output with the text in the comments. If there is a mismatch the test fails.
常用的包
标准的 Go 代码库中包含了大量的包,并且在安装 Go 的时候多数会伴随一起安装。浏
览 GOROOTsrcpkg 目录并且查看那些包会非常有启发。无法对每个包就加以解说,不过下面的这些值得讨论:a
a描述来自包的 go doc。额外的解释用斜体。常用的包 49
fmt
包 fmt 实现了格式化的 IO 函数,这与 C 的 printf 和 scanf 类似。格式化短语
派生于 C 。一些短语(%-序列)这样使用:
%v
默认格式的值。当打印结构时,加号(%+v)会增加字段名;
%v
Go 样式的值表达;
%T
带有类型的 Go 样式的值表达;
io
这个包提供了原始的IO操作界面。它主要的任务是对os包这样的原始的IO进
行封装,增加一些其他相关,使其具有抽象功能用在公共的接口上。
bu?o
这个包实现了缓冲的 IO。它封装于 io.Reader 和 io.Writer 对象,创建了另
一个对象(Reader 和 Writer)在提供缓冲的同时实现了一些文本 IO 的功能。
sort
sort 包提供了对数组和用户定义集合的原始的排序功能。
strconv
strconv 包提供了将字符串转换成基本数据类型,或者从基本数据类型转换为字
符串的功能。
os
os 包提供了与平台无关的操作系统功能接口。其设计是 Unix 形式的。
sync
sync 包提供了基本的同步原语,例如互斥锁。
ag
ag 包实现了命令行解析。参阅 “命令行参数” 在第 92 页。
encodingjson
encodingjson 包实现了编码与解码 RFC 4627 [2] 定义的 JSON 对象。
htmltemplate
数据驱动的模板,用于生成文本输出,例如 HTML。
将模板关联到某个数据结构上进行解析。模板内容指向数据结构的元素(通常结
构的字段或者 map 的键)控制解析并且决定某个值会被显示。模板扫描结构以
便解析,而 “游标” @ 决定了当前位置在结构中的值。
nethttp
nethttp 实现了 HTTP 请求、响应和 URL 的解析,并且提供了可扩展的 HTTP 服
务和基本的 HTTP 客户端。
unsafe
unsafe 包包含了 Go 程序中数据类型上所有不安全的操作。通常无须使用这个。50 Chapter 3: 包
re?ect
re?ect 包实现了运行时反射,允许程序通过抽象类型操作对象。通常用于处理静
态类型 interface{} 的值,并且通过 Typeof 解析出其动态类型信息,通常会返回
一个有接口类型 Type 的对象。
参阅 5,第 “自省和反射” 节。
osexec
osexec 包执行外部命令。
练习
Q15. (0) stack 包
1. 参考 Q8 练习。在这个练习中将从那个代码中建立一个独立的包。为 stack 的实
现创建一个合适的包,Push、Pop 和 Stack 类型需要被导出。
2. 为这个包编写一个单元测试,至少测试 Push 后 Pop 的工作情况。
Q16. (2) 计算器
1. 使用 stack 包创建逆波兰计算器。答案 51
答案
A15. (0) stack 包
1. 在创建 stack 包时,仅有一些小细节需要修改。首先,导出的函数应当大写首字
母,因此应该是 Stack。包所在的文件被命名为 stack-as-package.go,内容
是:
Listing 3.4. 包里的 Stack
package stack
保存元素的 Stack
type Stack s t ruc t {
i int
data [10]int
}
Push 将元素?入栈中
func (s Stack) Push(k int) {
s.data[s.i] = k
s.i++
}
Pop 从栈中?出一个元素
func (s Stack) Pop (ret int) {
s.i--
ret = s.data[s.i]
return
}
2. 为了让单元测试正常工作,需要做一些准备。下面用一分钟的时间来做这些。
首先是单元测试本身。创建文件 pushpop_test.go,有如下内容:
Listing 3.5. PushPop 测试
package stack
import testing
func TestPushPop(t testing.T) {
c := new(Stack)
c.Push(5)
i f c.Pop != 5 {
t.Log(Pop doesn't give 5)
t.Fail
}
}52 Chapter 3: 包
为了让 go test 能够工作,需要将包所在文件放到 GOPATHsrc:
% mkdir GOPATHsrcstack
% cp pushpop_test.go GOPATHsrcstack
% cp stack-as-package.go GOPATHsrcstack
输出:
% go test stack
ok stack 0.001s
A16. (2) 计算器
1. 这是第一个答案:
Listing 3.6. 逆波兰计算器
package main
import (
bufio
fmt
os
strconv)
var reader bufio.Reader = bufio.NewReader(os.Stdin)
var st = new(Stack)
type Stack s t ruc t {
i int
data [10]int
}
func (s Stack) push(k int) {
i f s.i+1 > 9 {
return
}
s.data[s.i] = k
s.i++
}
func (s Stack) pop (ret int) {
s.i--
i f s.i < 0 {
s.i = 0
return
}
ret = s.data[s.i]
return答案 53
}
func main {
for {
s, err := reader.ReadString('\n')
var token s t r ing
i f err != nil {
return
}
for _, c := range s {
switch {
case c >= '0' c <= '9':
token = token + s t r ing(c)
case c == ' ':
r, _ := strconv.Atoi(token)
st.push(r)
token =
case c == '+':
fmt.Printf(%d\n, st.pop+
st.pop)
case c == '':
fmt.Printf(%d\n, st.pop
st.pop)
case c == '-':
p := st.pop
q := st.pop
fmt.Printf(%d\n, q-p)
case c == 'q':
return
defaul t:
error
}
}
}
}4 进阶
“Go 有指针,但是没有指针运算。你不能
用指针变量遍历字符串的各个字节。”
Go For C++ Programmers
GO AUTHORS
Go 有指针。然而却没有指针运算,因此它们更象是引用而不是你所知道的来自于 C
的指针。指针非常有用。在 Go 中调用函数的时候,得记得变量是值传递的。因此,为
了修改一个传递入函数的值的效率和可能性,有了指针。
通过类型作为前缀来定义一个指针’’:var p int。现在 p 是一个指向整数值的指针。
所有新定义的变量都被赋值为其类型的零值,而指针也一样。一个新定义的或者没有
任何指向的指针,有值 nil。在其他语言中,这经常被叫做空(NULL)指针,在 Go 中
就是 nil。让指针指向某些内容,可以使用取址操作符 ,像这样:
Listing 4.1. 指针的使用
var p int
fmt.Printf(%v, p) 打印 nil
var i int 定义一个整形变量 i
p = i 使得 p 指向 i
fmt.Printf(%v, p) 打印出来的内容类似 0x7ff96b81c000a
从指针获取值是通过在指针变量前置’’ 实现的:
Listing 4.2. 获取指针指向的值
p = i 获取 i 的地址
p = 8 修改 i 的值
fmt.Printf(%v\n, p) 打印 8
fmt.Printf(%v\n, i) 同上
前面已经说了,没有指针运算,所以如果这样写:p++,它表示 (p)++:首先获取指
针指向的值,然后对这个值加一。a
内存分配
Go 同样也垃圾收集,也就是说无须担心内存分配和回收。
Go 有两个内存分配原语,new 和 make。它们应用于不同的类型,做不同的工作,可能
有些迷惑人,但是规则很简单。下面的章节展示了在 Go 中如何处理内存分配,并且希
望能够让 new 和 make 之间的区别更加清晰。
a参看练习 17。内存分配 55
用 new 分配内存
内建函数 new 本质上说跟其他语言中的同名函数功能一样:new(T) 分配了零值填充
的 T 类型的内存空间,并且返回其地址,一个 T 类型的值。用 Go 的术语说,它返回
了一个指针,指向新分配的类型 T 的零值。记住这点非常重要。
这意味着使用者可以用 new 创建一个数据结构的实例并且可以直接工作。如
bytes.Buffer 的文档所述 “Buffer 的零值是一个准备好了的空缓冲。” 类似的,sync.Mutex 也没有明确的构造函数或 Init 方法。取而代之,sync.Mutex 的零值
被定义为非锁定的互斥量。
零值是非常有用的。例如这样的类型定义,57 页的”定义自己的类型” 内容。
type SyncedBuffer s t ruc t {
lock sync.Mutex
buffer bytes.Buffer
}
SyncedBuffer 的值在分配内存或定义之后立刻就可以使用。在这个片段中,p 和 v 都
可以在没有任何更进一步处理的情况下工作。
p := new(SyncedBuffer) Type SyncedBu?er,已经可以使用
var v SyncedBuffer Type SyncedBu?er,同上
用 make 分配内存
回到内存分配。内建函数 make(T, args) 与 new(T) 有着不同的功能。它只能创建
slice,map 和 channel,并且返回一个有初始值(非零)的 T 类型,而不是 T。本质
来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。
例如,一个 slice,是一个包含指向数据(内部 array)的指针,长度和容量的三项描述
符;在这些项目被初始化之前,slice 为 nil。对于 slice,map 和 channel,make 初始
化了内部的数据结构,填充适当的值。
例如,make([]int, 10, 100) 分配了 100 个整数的数组,然后用长度 10 和容量 100
创建了 slice 结构指向数组的前 10 个元素。区别是,new([]int) 返回指向新分配的内
存的指针,而零值填充的 slice 结构是指向 nil 的 slice 值。
这个例子展示了 new 和 make 的不同。
var p []int = new([]int) 分配 slice 结构内存;很少使用
var v []int = make([]int , 100) v 指向一个新分配的有 100 个整数的数组
var p []int = new([]int) 不必要的复杂例子
p = make([]int , 100, 100)
v := make([]int , 100) 更常见
务必记得 make 仅适用于 map,slice 和 channel,并且返回的不是指针。应当用 new 获
得特定的指针。56 Chapter 4: 进阶
new 分配;make 初始化
上面的两段可以简单总结为:
new(T) 返回 T 指向一个零值 T
make(T) 返回初始化后的 T
当然 make 仅适用于 slice,map 和 channel。
构造函数与复合声明
有时零值不能满足需求,必须要有一个用于初始化的构造函数,例如这个来自 os 包的
例子。
func NewFile(fd int , name s t r ing) File {
i f fd < 0 {
return nil
}
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}
有许多冗长的内容。可以使用复合声明使其更加简洁,每次只用一个表达式创建一个
新的实例。
func NewFile(fd int , name s t r ing) File {
i f fd < 0 {
return nil
}
f := File{fd, name, nil, 0} Create a new File
return f 返回 f 的地址
}
返回本地变量的地址没有问题;在函数返回后,相关的存储区域仍然存在。
事实上,从复合声明获取分配的实例的地址更好,因此可以最终将两行缩短到一行。b
return File{fd, name, nil, 0}
The items (called of a composite +literal are laid out in order and must all be 所有的项
目(称作 字段)都必须按顺序全部写上。然而,通过对元素用字段: 值成对的标识,初
始化内容可以按任意顺序出现,并且可以省略初始化为零值的字段。因此可以这样
return File{fd: fd, name: name}
b从复合声明中获取地址,意味着告诉编译器在堆中分配空间,而不是栈中。定义自己的类型 57
在特定的情况下,如果复合声明不包含任何字段,它创建特定类型的零值。表达式
new(File) 和 File{} 是等价的。
复合声明同样可以用于创建 array,slice 和 map,通过指定适当的索引和 map 键来标
识字段。在这个例子中,无论是 Enone,Eio 还是 Einval 初始化都能很好的工作,只
要确保它们不同就好了。
ar := [...]s t r ing {Enone: no error, Einval: invalid argument}
sl := []s t r ing {Enone: no error, Einval: invalid argument}
ma := map[int]s t r ing {Enone: no error, Einval: invalid argument
}
定义自己的类型
自然,Go 允许定义新的类型,通过关键字 type 实现:
type foo int
创建了一个新的类型 foo 作用跟 int 一样。创建更加复杂的类型需要用到struct 关键
字。这有个在一个数据结构中记录某人的姓名(string)和年龄(int),并且使其成
为一个新的类型的例子:
Listing 4.3. 结构体
package main
import fmt
type NameAge s t ruc t {
name s t r ing 不导出
age int 不导出
}
func main {
a := new(NameAge)
a.name = Pete ; a.age = 42
fmt.Printf(%v\n, a)
}
通常,fmt.Printf(%v\n, a) 的输出是
{Pete 42}
这很棒!Go 知道如何打印结构。如果仅想打印某一个,或者某几个结构中的字段,需
要使用 .
fmt.Printf(%s, a.name) %s 格式化字符串58 Chapter 4: 进阶
结构字段
之前已经提到结构中的项目被称为?eld。没有字段的结构:struct {}
或者有四个c
字段的:
s t ruc t {
x, y int
A []int
F func
}
如果省略字段的名字,可以创建匿名字段,例如:
s t ruc t {
T1 字段名字是 T1
T2 字段名字是 T2
P.T3 字段名字是 T3
x, y int 字段名字是 x 和 y
}
注意首字母大写的字段可以被导出,也就是说,在其他包中可以进行读写。字段名以
小写字母开头是当前包的私有的。包的函数定义是类似的,参阅第 3 章了解更多细节。
方法
可以对新定义的类型创建函数以便操作,可以通过两种途径:
1. 创建一个函数接受这个类型的参数。
func doSomething(n1 NameAge, n2 int) { }
(你可能已经猜到了)这是 函数调用。
2. 创建一个工作在这个类型上的函数(参阅在 2.1 中定义的接收方):
func (n1 NameAge) doSomething(n2 int) { }
这是方法调用,可以类似这样使用:
var n NameAge
n.doSomething(2)
使用函数还是方法是由程序员决定的,但是如果想要满足接口(参阅下一章)就只能
使用方法。如果没有这方面的需求,那就由个人品味决定了。
使用函数还是方法完全是由程序员说了算,但是若需要满足接口(参看下一章)就必
须使用方法。如果没有这样的需求,那就完全由习惯来决定是使用函数还是方法了。
但是下面的内容一定要留意,引用自 [10]:
如果 x 可获取地址,并且 x 的方法中包含了 m,x.m 是 (x).m 更短
的写法。
c
是的,四(4)个。转换 59
根据上面所述,这意味着下面的情况不是错误:
var n NameAge 不是指针
n.doSomething(2)
这里 Go 会查找 NameAge 类型的变量 n 的方法列表,没有找到就会再查找 NameAge
类型的方法列表,并且将其转化为 (n).doSomething(2)。
下面的类型定义中有一些微小但是很重要的不同之处。同时可以参阅[10, section “Type
Declarations”]。假设有:
Mutex 数据类型有两个方法,Lock 和 Unlock。
type Mutex s t ruc t { Mutex 字段 }
func (m Mutex) Lock { Lock 实现 }
func (m Mutex) Unlock { Unlock 实现 }
现在用两种不同的风格创建了两个数据类型。
type NewMutex Mutex;
type PrintableMutex struct {Mutex }.
现在 NewMutux 等同于 Mutex,但是它没有任何 Mutex 的方法。换句话说,它的方法
是空的。
但是 PrintableMutex 已经从 Mutex 继承了方法集合。如同 [10] 所说:
PrintableMutex 的方法集合包含了 Lock 和 Unlock 方法,被绑定到其
匿名字段 Mutex。
转换
有时需要将一个类型转换为另一个类型。在 Go 中可以做到,不过有一些规则。首先,将一个值转换为另一个是由操作符(看起来像函数:byte)完成的,并且不是所有
的转换都是允许的。
Table 4.1. 合法的转换,?oat64 同 ?oat32 类似。注意,为了适配表格的显示,?oat32
被简写为 ?t32。
From b []byte i []int r []rune s string f flt32 i int
To
[]byte × []byte(s)
[]int × []int(s)
[]rune × []rune(s)
string string(b) string(i) string(r) ×
ftl32 × flt32(i)
int int(f) ×
从 string 到字节或者 ruin 的 slice。
mystring := hello this is string60 Chapter 4: 进阶
byteslice := []byte(mystring)
转换到 byte slice,每个 byte 保存字符串对应字节的整数值。注意 Go 的字符串
是 UTF-8 编码的,一些字符可能是 1、2、3 或者 4 个字节结尾。
runeslice := []rune(mystring)
转换到 rune slice,每个 rune 保存 Unicode 编码的指针。字符串中的每个字符
对应一个整数。
从字节或者整形的 slice 到 string。
b := []byte {'h','e','l','l','o'} 复合声明
s := s t r ing(b)
i := []rune {257,1024,65}
r := s t r ing(i)
对于数值,定义了下面的转换:
将整数转换到指定的(bit)长度:uint8(int);
从浮点数到整数:int(?oat32)。这会截断浮点数的小数部分;
其他的类似:?oat32(int)。
用户定义类型的转换
如何在自定义类型之间进行转换?这里创建了两个类型 Foo 和 Bar,而 Bar 是 Foo 的
一个别名:
type foo s t ruc t { int } 匿名字段
type bar foo bar 是 foo 的别名
然后:
var b bar = bar{1} 声明 b 为 bar 类型
var f foo = b 赋值 b 到 f
最后一行会引起错误:
cannot use b (type bar) as type foo in assignment(不能使用 b(类型 bar)
作为类型 foo 赋值)
这可以通过转换来修复:
var f foo = foo(b)
注意转换那些字段不一致的结构是相当困难的。同时注意,转换 b 到 int 同样会出错;
整数与有整数字段的结构并不一样。
组合
TODO(miek):work in progress Go 不是面向对象语言,因此并没有继承。但是有时又会
需要从已经实现的类型中“继承”并修改一些方法。在 Go 中可以用嵌入一个类型的方
式来实现。练习 61
练习
Q17. (1) 指针运算
1. 在正文的第 54 页有这样的文字:…这里没有指针运算,因此如果这样写:p++,它被解释为 (p)++:
首先解析引用然后增加值。
当像这样增加一个值的时候,什么类型可以工作?
2. 为什么它不能工作在所有类型上?
Q18. (2) 使用 interface 的 map 函数
1. 使用练习 Q11 的答案,利用 interface 使其更加通用。让它至少能同时工作于
int 和 string。
Q19. (1) 指针
1. 假设定义了下面的结构:
type Person s t ruc t {
name s t r ing
age int
}
下面两行之间的区别是什么?
var p1 Person
p2 := new(Person)
2. 下面两个内存分配的区别是什么?
func Set(t T) {
x = t
}
和
func Set(t T) {
x= t
}
Q20. (1) Linked List
1. Make use of the package containerlist to create a (doubly) linked list. Push the
values 1, 2 and 4 to the list and then print it.
2. Create your own linked list implementation. And perform the same actions as in ......
您现在查看是摘要介绍页, 详见PDF附件(1135KB,119页)。





