Go-编程入门指南-全-
Go 编程入门指南(全)
原文:Get Programming with Go
译者:飞龙
单元 0. 入门
传统上,学习一门新编程语言的第一步是设置工具和环境来运行一个简单的“Hello, world”应用程序。有了 Go 操场,这项古老的努力简化为单次点击。
在此之后,你可以开始学习编写和修改简单程序所需的语法和概念。
第 1 课. 准备,设置,Go
在阅读 第 1 课 之后,你将能够
-
了解 Go 的独特之处
-
访问 Go 操场
-
在屏幕上打印文本
-
在任何自然语言中实验文本
Go 是当代的云计算编程语言。亚马逊、苹果、 canonical、雪佛龙、迪士尼、Facebook、通用电气、谷歌、Heroku、微软、Twitch、威瑞森和沃尔玛等公司正在采用 Go 进行重要项目(参见 thenewstack.io/who-is-the-go-developer/ 和 golang.org/wiki/GoUsers)。网络基础设施的大部分正在转向 Go,这得益于 CloudFlare、Cockroach Labs、DigitalOcean、Docker、InfluxData、Iron.io、Let’s Encrypt、Light Code Labs、Red Hat CoreOS、SendGrid 以及像云原生计算基金会这样的组织。
Go 在数据中心表现出色,但其应用范围远超工作场所。Ron Evans 和 Adrian Zankich 创建了 Gobot(gobot.io),这是一个用于控制机器人和硬件的库。Alan Shreve 创建了开发工具 ngrok(ngrok.com),作为学习 Go 的项目,并且后来将其转变为全职业务。
采用 Go 的人称自己为 gophers,以纪念 Go 的轻松 mascot (figure 1.1)。编程具有挑战性,但有了 Go 和这本书,我们希望你能发现编码的乐趣。
图 1.1. 由 Renée French 设计的 Go gopher 面饰

在本课中,你将在网页浏览器中实验一个 Go 程序。
考虑这一点
如果你告诉一个数字助手,“Call me a cab”,它会拨打出租车公司吗?还是它假设你改名为 a cab?自然语言,如英语,充满了歧义。
清晰性在编程语言中至关重要。如果语言的语法或语法允许歧义,计算机可能不会按照你的说法行事。这反而违背了编写程序的目的。
Go 并不是一种完美的语言,但它比我们使用的任何其他语言都更追求清晰。随着你通过本课的学习,你将需要学习一些缩写和术语。不是所有内容一开始都会一目了然,但请花时间欣赏 Go 的工作方式,以减少歧义。
1.1. 什么是 Go?
Go 是一种 编译型 编程语言。在运行程序之前,Go 使用 编译器 将你的代码翻译成机器语言中的 1 和 0。它将你的所有代码编译成一个单一的 可执行文件,供你运行或分发。在这个过程中,Go 编译器可以捕捉到拼写错误和错误。
并非所有编程语言都采用这种方法。Python、Ruby 和其他几种流行的语言使用解释器在程序运行时逐条翻译语句。这意味着可能存在你尚未测试的路径中的错误。
另一方面,解释器使编写代码的过程变得快速且交互式,使用动态、轻松且有趣的编程语言。编译语言以静态、僵化、程序员被迫取悦的机器人而闻名,而编译器则因其速度慢而受到嘲笑。但真的需要这样吗?
*我们想要一种语言,它具有像 C++ 和 Java 这样的静态编译语言的安全性和性能,同时又有像 Python 这样的动态类型解释语言的轻便和乐趣。
罗布·派克,本周极客(见 mng.bz/jr8y)
Go 在编写软件的体验上投入了大量的考虑。大型程序只需一条命令即可在几秒钟内编译完成。该语言省略了导致歧义的特征,鼓励编写可预测且易于理解的代码。而且 Go 提供了轻量级的替代方案,以应对像 Java 这样的经典语言的僵化结构。
Java 省略了 C++ 中许多很少使用、理解不佳、令人困惑的特征,根据我们的经验,这些特征带来的麻烦多于好处。
詹姆斯·高斯林,Java:概述**
每种新的编程语言都会对过去的思想进行细化。在 Go 中,使用内存更高效且比早期语言更少出错,Go 还利用了多核机器上的每个核心。成功案例通常将提高效率作为转向 Go 的原因。Iron.io 能够用 2 个使用 Go 的服务器替换运行 Ruby 的 30 个服务器(见 mng.bz/Wevx 和 mng.bz/8yo2)。Bitly 在用 Go 重写 Python 应用程序后“看到了一致、可衡量的性能提升”,随后用 Go 后继者替换了其 C 应用程序(见 mng.bz/EnYl)。
Go 提供了解释语言的乐趣和易用性,同时在效率和可靠性方面有所提升。作为一个小型语言,只有几个简单概念,Go 相对容易学习。这三个原则构成了 Go 的座右铭:
Go 是一种开源编程语言,它能够以简单、高效和可靠的方式大规模生产软件。
Go 品牌手册
提示
在互联网上搜索与 Go 相关的主题时,使用关键词 golang,代表 Go 语言。-lang 后缀也可以应用于其他编程语言,如 Ruby、Rust 等。
| |
快速检查 1.1
Q1:
Go 编译器的两个好处是什么?
| |
QC 1.1 答案
1:
大型程序可以在几秒钟内编译完成,Go 编译器可以在程序运行前捕获拼写错误和错误。
1.2. Go 演示场
开始学习 Go 的最快方式是导航到 play.golang.org。在 Go Playground (图 1.2) 中,你可以编辑、运行和实验 Go,无需安装任何东西。当你点击运行按钮时,Playground 将在 Google 服务器上编译并执行你的代码,并显示结果。
图 1.2. Go Playground

如果你点击分享按钮,你会收到一个链接,可以回到你编写的代码。你可以与朋友分享这个链接,或者将其添加到书签以保存你的工作。
注意
你可以使用 Go Playground 来完成本书中的每个代码列表和练习。或者,如果你已经熟悉文本编辑器和命令行,你可以从 golang.org/dl/ 下载并安装 Go。
快速检查 1.2
Q1:
在 Go Playground 中,运行按钮做什么?
QC 1.2 答案
1:
运行按钮将在 Google 服务器上编译然后执行你的代码。
1.3. 包和函数
当你访问 Go Playground 时,你会看到以下代码,这是一个很好的起点。
列表 1.1. Hello, playground: playground.go
package main *1*
import (
"fmt" *2*
)
func main() { *3*
fmt.Println("Hello, playground") *4*
}
-
1 声明此代码所属的包
-
2 使 fmt(格式)包可用于使用
-
3 声明一个名为 main 的函数
-
4 将“Hello, playground”打印到屏幕上
尽管简短,但前面的列表介绍了三个关键字:package、import 和 func。每个关键字都保留用于特殊目的。
package 关键字声明了此代码所属的包,在这种情况下是一个名为 main 的包。Go 中的所有代码都组织成 包。Go 提供了一个标准库,由数学、压缩、加密、图像处理等包组成。每个包对应一个单一的概念。
下一个行使用 import 关键字来指定此代码将使用的包。包包含任意数量的 函数。例如,math 包提供了 Sin、Cos、Tan 和 Sqrt(平方根)等函数。这里使用的 fmt 包提供了 格式化 输入和输出的函数。在屏幕上显示文本是一种常见的操作,因此这个包名被缩写为 fmt。Gophers 将 fmt 发音为“FŌŌMT!”,就像它被写成漫画书中大爆炸式的字母一样。
func 关键字声明一个函数,在这种情况下是一个名为 main 的函数。每个函数的 主体 被括号 {} 包围,这是 Go 知道每个函数开始和结束的方式。
main 标识符 是特殊的。当你运行用 Go 编写的程序时,执行从 main 包中的 main 函数开始。如果没有 main,Go 编译器将报告错误,因为它不知道程序应该从哪里开始。

要打印一行文本,你可以使用Println函数(ln是行的缩写)。Println前面带有fmt后跟一个点,因为它是由fmt包提供的。每次你使用一个导入包中的函数时,该函数前面都会带有包名和一个点。当你阅读用 Go 编写的代码时,每个函数来自哪个包会立即清楚。
在 Go Playground 中运行程序,可以看到文本Hello, playground。引号内的文本会被输出到屏幕上。在英语中,一个缺失的逗号可以改变句子的意思。标点符号在编程语言中也很重要。Go 依赖于引号、括号和大括号来理解你编写的代码。
快速检查 1.3
1
Go 程序从哪里开始?
2
fmt包提供什么?
QC 1.3 答案
1
程序从
main包中的main函数开始。2
fmt包提供格式化输入和输出的函数。
1.4. 唯一的正确的大括号风格
Go 对花括号{}的位置很挑剔。在列表 1.1 中,开括号{与func关键字在同一行,而闭括号}则单独一行。这是唯一的正确的大括号风格——没有其他方式。见mng.bz/NdE2。
要理解为什么 Go 变得如此严格,你需要回到 Go 的诞生时刻。在那些早期日子里,代码到处都是分号。无处不在。无法摆脱它们;分号像迷失的小狗一样跟在每一个语句后面。例如:
fmt.Println("Hello, fire hydrant");
在 2009 年 12 月,一群忍者仓鼠从语言中移除了分号。好吧,不是真的。实际上,Go 编译器会代表你插入那些可爱的分号,并且它工作得非常完美。是的,完美,但作为交换,你必须遵循唯一的正确的大括号风格。

如果你将开括号放在与func关键字不同的行上,Go 编译器将报告语法错误:
func main() *1*
{ *2*
}
-
1 缺少函数体
-
2 语法错误:在
{之前意外出现分号或换行符
编译器并没有生你的气。分号插入的位置不正确,它有点困惑。
小贴士
在你通过这本书的过程中,自己输入代码列表是个好主意。如果你输入错误,可能会看到语法错误,这是正常的。能够阅读、理解和纠正错误是一项重要的技能,而坚持不懈是一个宝贵的品质。
快速检查 1.4
Q1:
开括号
{必须放在哪里才能避免语法错误?
QC 1.4 答案
1:
开括号必须与
func关键字在同一行上,而不是单独一行。这是唯一的正确的大括号风格。
摘要
-
使用 Go Playground,你可以开始使用 Go 而无需安装任何东西。
-
每个 Go 程序都是由包含在包中的函数组成的。
-
要在屏幕上打印文本,请使用标准库提供的
fmt包。 -
标点符号在编程语言中与在自然语言中一样重要。
-
你使用了 25 个 Go 关键字中的 3 个:
package、import和func。
让我们看看你是否掌握了这个...
对于以下练习,修改 Go Playgound 中的代码并点击运行按钮以查看结果。如果你卡住了,刷新你的网络浏览器以恢复原始代码。
实验:playground.go
-
通过修改引号之间的内容来更改打印到屏幕上的文本。让计算机通过你的名字问候你。
-
通过在
main函数的{}体中编写第二行代码来显示两行文本。例如:fmt.Println("Hello, world") fmt.Println("Hello, ") -
Go 支持所有语言的字符。打印中文、日语、俄语或西班牙语的文本。如果你不说法语,你可以使用 Google Translate (translate.google.com)并将文本复制/粘贴到 Go Playgound 中。
使用分享按钮获取你程序的链接,并通过在Get Programming with Go论坛(forums.manning.com/forums/get-programming-with-go)上发布来与其他读者分享。
将你的解决方案与附录中的代码列表进行比较。
单元 1. 命令式编程
大多数计算机程序都是一系列步骤,就像你妈妈做肉汁蘑菇的指示一样。精确地告诉计算机如何完成任务,它就可以做各种事情。写下这些指示被称为命令式编程。如果计算机能做饭就好了!
在单元 1 中,你将复习 Go 的基础知识,并开始学习 Go 用来指导计算机的语法。每一课都会积累你需要应对第一个挑战的知识:一个列出火星度假票价的应用程序。
第 2 课。一个高级计算器
在阅读第 2 课之后,你将能够
-
教会计算机做数学
-
声明变量和常量
-
看看声明和赋值如何不同
-
使用标准库生成伪随机数
计算机程序能够做很多事情。在本课中,你将编写程序来解决数学问题。
考虑这一点
为什么写程序,而不是直接使用计算器呢?
嗯,你记住了光速或者火星绕太阳运行需要多长时间吗?代码可以保存和稍后读取,既可以作为计算器,也可以作为参考。程序是一个可执行的文档,可以共享和修改。
2.1. 执行计算
有时候我们会想,如果年轻一点,体重轻一点就好了。在这方面,火星有很多可以提供的。火星绕太阳运行需要 687 个地球日,其较弱的引力意味着一切物体的重量大约是地球上的 38%。
为了计算 Nathan 在火星上的年龄和体重,我们编写了一个小程序,如代码清单 2.1 所示。Go 提供了与其他编程语言相同的算术运算符:+、-、*、/和%分别用于加法、减法、乘法、除法和取模。

提示
取模运算符(%)获取两个整数相除的余数(例如,42 % 10是 2)。
代码清单 2.1. 欢迎火星:mars.go
// My weight loss program. *1*
package main
import "fmt"
// main is the function where it all begins. *1*
func main() {
fmt.Print("My weight on the surface of Mars is ")
fmt.Print(149.0 * 0.3783) *2*
fmt.Print(" lbs, and I would be ")
fmt.Print(41 * 365 / 687) *3*
fmt.Print(" years old.")
}
-
1 人类读者注释
-
2 打印 56.3667
-
3 打印 21
注意
虽然代码清单 2.1 显示的是磅重,但选择的计量单位不会影响重量计算。无论你选择什么单位,火星上的重量是地球上的 37.83%。
代码清单中前面的代码以注释开始。当 Go 看到双斜杠//时,它会忽略该行直到行尾。计算机编程全部是关于沟通的。代码将你的指令传达给计算机,当编写得好的时候,它也能将你的意图传达给其他人。注释只是给我们自己看的。它们不会影响程序运行。
前面的代码清单多次调用Print函数,在单行上显示一个句子。或者,你可以传递一个由逗号分隔的参数列表。Println的参数可以是文本、数字或数学表达式:
fmt.Println("My weight on the surface of Mars is", 149.0*0.3783, "lbs, and
I would be", 41*365.2425/687, "years old.") *1*
- 1 打印我的体重在火星表面是 56.3667 磅,并且我会是 21.79758733624454 岁。
快速检查 2.1
Q1:
在 play.golang.org 的 Go Playground 中输入并运行 列表 2.1。你在火星上会重多少?你会是多少岁?用 Nathan 的年龄(41)和体重(149.0)替换你自己的。
QC 2.1 答案
1:
这取决于你的体重和年龄。
提示
修改完代码后,点击 Go Playground 中的 Format 按钮。它将自动重新格式化代码的缩进和间距,而不会改变其功能。
2.2. 格式化打印
Print 和 Println 函数有一个兄弟函数,它提供了更多对输出的控制。通过使用以下列表中的 Printf,您可以在文本中的任何位置插入值。
列表 2.2. Printf: fmt.go
fmt.Printf("My weight on the surface of Mars is %v lbs,", 149.0*0.3783) *1*
fmt.Printf(" and I would be %v years old.\n", 41*365/687) *2*
-
1 打印我的体重在火星表面是 56.3667 磅,
-
2 打印并且我会是 21 岁。
与 Print 和 Println 不同,Printf 的第一个参数始终是文本。文本包含 格式动词 %v,它将被第二个参数提供的表达式的 值 替换。
注意
我们将在接下来的课程中根据需要介绍更多的格式动词(除了 %v)。有关完整参考,请参阅在线文档 golang.org/pkg/fmt/。
Println 函数会自动移动到下一行,但 Printf 和 Print 不会。每当您想要移动到新的一行时,请将 \n 放入文本中。
如果指定了多个格式动词,Printf 函数将按顺序替换多个值:
fmt.Printf("My weight on the surface of %v is %v lbs.\n", "Earth", 149.0) *1*
- 1 打印我的体重在地球上是 149 磅。
除了在句子中的任何位置替换值之外,Printf 还可以帮助您对齐文本。将宽度指定为格式动词的一部分,例如 %4v 可以将一个值填充到 4 个字符的宽度。正数用空格填充到左边,负数用空格填充到右边:
fmt.Printf("%-15v $%4v\n", "SpaceX", 94)
fmt.Printf("%-15v $%4v\n", "Virgin Galactic", 100)
上述代码显示以下输出:
SpaceX $ 94
Virgin Galactic $ 100
快速检查 2.2
1
如何打印新的一行?
2
当
Printf遇到%v格式动词时,它会做什么?
QC 2.2 答案
1
在您要打印的文本中的任何位置使用
\n插入新行或使用fmt.Println()。2
%v将替换为以下参数中的一个值。
2.3. 常量和变量
列表 2.1 中的计算是在 字面量 数字上进行的。这些数字的含义并不明确,尤其是像 0.3783 这样的值。程序员有时将不明确的字面量数字称为 魔法数字。常量和变量可以通过提供描述性名称来帮助。
在看到火星上生活的益处后,我们接下来的问题是旅行需要多长时间。以光速旅行将是理想的。光在太空的真空中以恒定速度传播,这使得数学变得简单。另一方面,地球和火星之间的距离会根据行星在太阳轨道上的位置而显著变化。
下面的列表介绍了两个新的关键字,const和var,分别用于声明常量和变量。
列表 2.3. 以光速旅行:lightspeed.go
// How long does it take to get to Mars?
package main
import "fmt"
func main() {
const lightSpeed = 299792 // km/s
var distance = 56000000 // km
fmt.Println(distance/lightSpeed, "seconds") *1*
distance = 401000000
fmt.Println(distance/lightSpeed, "seconds") *2*
}
-
1 打印 186 秒
-
2 打印 1337 秒
将列表 2.3 输入到 Go 沙盒中并点击运行。光速非常方便;你大概不会听到有人问,“我们到了吗?”
第一次计算基于火星和地球靠近的情况,声明并赋予distance变量一个初始值 5600 万公里。然后,将distance变量赋予一个新的值 4010 万公里,此时行星位于太阳的两侧,尽管直接穿越太阳的航线可能会有问题。
注意
lightSpeed常量不能更改。如果你尝试给它赋予一个新值,Go 编译器将报告错误“不能向 lightSpeed 赋值。”
| |
注意
在使用变量之前必须声明它们。如果你尝试将值赋给未用var声明的变量,Go 将报告错误——例如,speed = 16。这种限制可以帮助捕捉错误,比如在打算输入distance时意外地将值赋给了distence。
| |
快速检查 2.3
1
SpaceX 星际运输系统没有超光速驱动器,但它将以可尊敬的 100,800 公里/小时的速度滑行到火星。一个雄心勃勃的发射日期是 2025 年 1 月,届时火星和地球相距 9600 万公里。到达火星需要多少天?修改列表 2.3 以找出答案。
2
一个地球日有 24 小时。为了在你的程序中给 24 一个描述性的名称,你会使用哪个关键字?
| |
QC 2.3 答案
1
飞船不会直线行驶,但作为一个近似值,旅行将需要 39 天。
const hoursPerDay = 24 var speed = 100800 // km/h var distance = 96300000 // km fmt.Println(distance/speed/hoursPerDay, "days")2
const关键字因为程序运行期间值不会改变。
2.4. 简化方法
访问火星可能没有捷径,但 Go 提供了一些节省按键的快捷方式。
2.4.1. 一次性声明多个变量
当你声明变量或常量时,你可以像这样单独声明每个:
var distance = 56000000
var speed = 100800
或者你可以将它们作为一个组声明:
var (
distance = 56000000
speed = 100800
)
另一个选项是在单行中声明多个变量:
var distance, speed = 56000000, 100800
在你将多个变量作为一个组或单行声明之前,考虑一下这些变量是否相关。始终牢记代码的可读性。
快速检查 2.4
Q1:
以下哪一行代码可以声明一天中的小时数和每小时分钟数?
QC 2.4 答案
1:
const hoursPerDay, minutesPerHour = 24, 60
2.4.2. 增量和赋值运算符
有一些快捷方式可以与其他操作一起执行赋值。以下列表的最后两行是等效的。
列表 2.4. 赋值运算符:shortcut.go
var weight = 149.0
weight = weight * 0.3783
weight *= 0.3783
增量可以通过以下列表中的方式使用额外的快捷方式。
列表 2.5. 增量运算符
var age = 41
age = age + 1 *1*
age += 1
age++
- 1 祝你生日快乐!
你可以使用 count-- 来递减,或者以相同的方式缩短其他操作,比如 price /= 2。
注意
如果你想知道,Go 不支持像 C 和 Java 那样的前缀增量 ++count。
快速检查 2.5
Q1:
编写一行最短的代码,从名为
weight的变量中减去两磅。
QC 2.5 答案
1:
weight -= 2
2.5. 思考一个数字
思考一个介于 1 和 10 之间的数字。
明白了?好的。
现在让你的电脑“思考”一个介于 1 和 10 之间的数字。你的电脑可以使用 rand 包生成伪随机数。它们被称为 伪随机,因为它们或多或少是随机的,但不是真正的随机。
列表 2.6 中的代码将显示 1 到 10 之间的两个数字。将 10 传递给 Intn 会返回一个 0 到 9 之间的数字,然后你将 1 加到这个数字上,并将结果赋值给 num。num 变量不能是一个 Go 常量,因为它是一个函数调用的结果。
注意
如果你忘记加 1,你将得到一个介于 0–9 之间的数字。因为我们想要一个介于 1–10 之间的数字,这是一个离一错误(off-by-one error)的例子,这是一个经典的编程错误。
列表 2.6. 随机数:rand.go
package main
import (
"fmt"
"math/rand"
)
func main() {
var num = rand.Intn(10) + 1
fmt.Println(num)
num = rand.Intn(10) + 1
fmt.Println(num)
}
rand 包的 导入路径 是 math/rand。Intn 函数以包名 rand 为前缀,但导入路径更长。
提示
要使用一个新的包,它必须被列为一个 import。Go Playground 可以为你添加导入路径。首先确保勾选了 Imports 复选框,然后点击 Format 按钮。Go Playground 将确定正在使用哪些包,并更新你的导入路径。
注意
每次运行 列表 2.6 时,都会显示相同的两个伪随机数。这是被操纵的!在 Go Playground 中,时间静止并且结果被缓存,但这些数字对于我们的目的来说已经足够好了。
快速检查 2.6
Q1:
地球和火星之间的距离从太阳的近端到远端变化。编写一个程序,生成一个介于 5,600,000 到 40,100,000 公里之间的随机距离。
QC 2.6 答案
1:
// a random distance to Mars (km) var distance = rand.Intn(345000001) + 56000000 fmt.Println(distance)
摘要
-
Print、Println和Printf函数在屏幕上显示文本和数字。 -
使用
Printf和%v格式动词,值可以放在显示文本的任何位置。 -
常量使用
const关键字声明,并且不能更改。 -
变量使用
var声明,并且在程序运行期间可以分配新的值。 -
math/rand导入路径指的是rand包。 -
rand包中的Intn函数生成伪随机数。 -
你使用了 25 个 Go 关键字中的 5 个:
package、import、func、const和var。
让我们看看你是否掌握了这个...
实验:malacandra.go
马尔卡丹比那更近:我们将在大约二十八天内到达那里。
C.S.路易斯,《寂静星球之外》
马尔卡丹是 C.S.路易斯《太空三部曲》中火星的另一个名字。编写一个程序来确定一艘船需要以多快的速度(以公里/小时计)才能在 28 天内到达马尔卡丹。假设距离为 5600 万公里。
将你的解决方案与附录中的代码列表进行比较。
第 3 课。循环和分支
在阅读第 3 课之后,你将能够
-
使用
if和switch让计算机做出选择 -
使用
for循环重复代码 -
使用条件进行循环和分支
计算机程序很少像小说一样从头到尾阅读。程序更像《选择你的冒险》书籍或互动小说。它们在特定条件下采取不同的路径,或者重复相同的步骤,直到满足某个条件。
如果你熟悉许多编程语言中找到的if、else和for关键字,这门课程将作为 Go 语法的快速入门。
考虑这一点
当内森还小的时候,他的家人会在长途旅行中玩“二十个问题”来消磨时间。一个人会想出一个东西,其他人试图猜是什么。问题只能用是或不是来回答。像“它有多大?”这样的问题会引来一个茫然的表情。相反,一个常见的问题是“它比烤面包机大吗?”
计算机程序通过是/否问题进行操作。给定某些条件(如比烤面包机大),CPU 可以继续沿着一条路径向下走,或者跳转(JMP)到程序中的另一个地方。复杂的决策需要分解成更小、更简单的条件。
考虑你今天穿的衣服。你是如何挑选每一件衣服的?涉及哪些变量,比如天气预报、计划的活动、可用性、时尚、随机性等等?你将如何教计算机在早上穿衣服?写下几个有是或否答案的问题。
3.1. 是或不是
当你阅读《选择你的冒险》书籍时,你会遇到这样的选择:
如果你走出洞穴,翻到第 21 页。
爱德华·帕卡德,《时间洞穴》
你是否走出洞穴?在 Go 语言中,你的答案可以是true或false,这两个常量已经声明。你可以这样使用它们:
var walkOutside = true
var takeTheBluePill = false
注意
一些编程语言对真值有宽松的定义。在 Python 和 JavaScript 中,文本的缺失 ("") 被认为是 false,数字零也是如此。在 Ruby 和 Elixir 中,相同的值被认为是 true。在 Go 中,唯一的真值是 true,唯一的假值是 false。
True 和 false 是 Boolean 值,因此以 19 世纪数学家乔治·布尔命名。标准库中的几个函数返回布尔值。例如,以下代码示例使用 strings 包中的 Contains 函数来检查 command 变量是否包含文本“outside”。它确实包含该文本,因此结果是 true。
列表 3.1. 返回布尔值的函数:contains.go
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println("You find yourself in a dimly lit cavern.")
var command = "walk outside"
var exit = strings.Contains(command, "outside")
fmt.Println("You leave the cave:", exit) *1*
}
- 1 打印 You leave the cave: true
快速检查 3.1
1
从洞穴中出来,你的眼睛遇到了刺眼的正午阳光。你如何声明一个名为
wearShades的布尔变量?2
洞穴入口附近有一个标志。你如何确定
command是否包含单词"read"?
| |
QC 3.1 答案
1
var wearShades = true2
var read = strings.Contains(command, "read")
3.2. 比较运算
另一种得到 true 或 false 值的方法是通过比较两个值。Go 提供了 表 3.1 中显示的比较运算符。
表 3.1. 比较运算符
| == | 等于 | != | 不等于 |
|---|---|---|---|
| < | 小于 | > | 大于 |
| <= | 小于或等于 | >= | 大于或等于 |
你可以使用 表 3.1 中的运算符来比较文本或数字,如下面的代码示例所示。
列表 3.2. 比较数字:compare.go
fmt.Println("There is a sign near the entrance that reads 'No Minors'.")
var age = 41
var minor = age < 18
fmt.Printf("At age %v, am I a minor? %v\n", age, minor)
之前的代码示例将产生以下输出:
There is a sign near the entrance that reads 'No Minors'.
At age 41, am I a minor? false
注意
JavaScript 和 PHP 有一个特殊的 threequals 操作符用于严格相等。在这些语言中 "1" == 1 是 true(宽松),但 "1" === 1 是 false(严格)。Go 只有一个相等操作符,它不允许直接比较文本和数字。第 10 课演示了如何将数字转换为文本以及相反。
| |
快速检查 3.2
Q1:
哪个更大,是“apple”还是“banana”?
| |
QC 3.2 答案
1:
香蕉显然更大。
fmt.Println("apple" > "banana") *1*
- 1 打印 false
3.3. 使用 if 分支
计算机可以使用布尔值或比较来通过 if 语句选择不同的路径,如下面的代码示例所示。
列表 3.3. 分支:if.go
package main
import "fmt"
func main() {
var command = "go east"
if command == "go east" { *1*
fmt.Println("You head further up the mountain.")
} else if command == "go inside" { *2*
fmt.Println("You enter the cave where you live out the rest of your
life.")
} else { *3*
fmt.Println("Didn't quite get that.")
}
}
-
1 如果命令等于“go east”
-
2 否则,如果命令等于“go inside”
-
3 或者,如果其他任何情况
之前的代码示例将产生以下输出:
You head further up the mountain.
else if 和 else 都是可选的。当有多个路径要考虑时,你可以根据需要重复使用 else if。
注意
如果你意外地使用了赋值运算符 (=) 而不是相等运算符 (==),Go 会报告错误。
| |
快速检查 3.3
Q1:
冒险游戏被划分为房间。编写一个程序,使用
if和else if来显示三个房间(洞穴、入口和山脉)的描述。在编写程序时,确保花括号{}的放置符合正确的花括号风格,如列表 3.3 所示。
QC 3.3 答案
1:
package main import "fmt" func main() { var room = "cave" if room == "cave" { fmt.Println("You find yourself in a dimly lit cavern.") } else if room == "entrance" { fmt.Println("There is a cavern entrance here and a path to the east.") } else if room == "mountain" { fmt.Println("There is a cliff here. A path leads west down the mountain.") } else { fmt.Println("Everything is white.") } }
3.4. 逻辑运算符
在 Go 语言中,逻辑运算符 || 表示 或,而逻辑运算符 && 表示 与。使用逻辑运算符可以同时检查多个条件。参见图 3.1 和 3.2 了解这些运算符的评估方式。
图 3.1. 当 a || b 中的任意一个为真时(或)

图 3.2. 当 a && b 都为真时(与)

列表 3.4 中的代码判断 2100 年是否是闰年。判断闰年的规则如下:
-
任何能被 4 整除但不能被 100 整除的年份
-
或者任何能被 400 整除的年份
注意
回想一下,取模运算符 (%) 获取两个整数相除的余数。余数为零表示一个数可以被另一个数整除。

列表 3.4. 闰年判断:leap.go
fmt.Println("The year is 2100, should you leap?")
var year = 2100
var leap = year%400 == 0 || (year%4 == 0 && year%100 != 0)
if leap {
fmt.Println("Look before you leap!")
} else {
fmt.Println("Keep your feet on the ground.")
}
以下列表将产生以下输出:
The year is 2100, should you leap?
Keep your feet on the ground.
与大多数编程语言一样,Go 使用 短路逻辑。如果第一个条件为真(年份能被 400 整除),则不需要评估 || 运算符后面的内容,因此它被忽略。
&& 运算符正好相反。结果为假,除非两个条件都为真。如果年份不能被 4 整除,则不需要评估下一个条件。
逻辑运算符的非运算符 (!) 会将布尔值从 false 翻转到 true 或相反。以下列表显示如果玩家没有火炬或火炬未点亮时将显示的消息。
列表 3.5. 非运算符:torch.go
var haveTorch = true
var litTorch = false
if !haveTorch || !litTorch {
fmt.Println("Nothing to see here.") *1*
}
- 1 打印无内容可看。
快速检查 3.4
1
使用笔和纸,将
2000代入 列表 3.4 中的闰年表达式。计算所有模运算符的余数(如有必要,请使用计算器)。然后评估==和!=条件为true或false。最后,评估逻辑运算符&&和||。2000 年是闰年吗?2
如果你首先使用短路逻辑评估
2000%400 == 0为true,你会节省时间吗?
QC 3.4 答案
1
是的,2000 年是闰年:
2000%400 == 0 || (2000%4 == 0 && 2000%100 != 0) 0 == 0 || (0 == 0 && 0 != 0) true || (true && false) true || (false) true2
是的,评估并写下方程的后半部分确实花费了一些时间。计算机要快得多,但短路逻辑仍然可以节省时间。
3.5. 使用 switch 进行分支
当比较一个值与多个值时,Go 提供了 switch 语句,您可以在下面的列表中看到。
列表 3.6. switch 语句:concise-switch.go
fmt.Println("There is a cavern entrance here and a path to the east.")
var command = "go inside"
switch command { *1*
case "go east":
fmt.Println("You head further up the mountain.")
case "enter cave", "go inside": *2*
fmt.Println("You find yourself in a dimly lit cavern.")
case "read sign":
fmt.Println("The sign reads 'No Minors'.")
default:
fmt.Println("Didn't quite get that.")
}
-
1 比较案例到命令
-
2 以逗号分隔的可能值列表
之前的列表将生成以下输出:
There is a cavern entrance here and a path to the east.
You find yourself in a dimly lit cavern.
注意
你也可以使用带有数字的switch语句。
或者,你可以使用每个 case 的条件switch语句,就像使用if...else一样。switch的一个独特功能是fallthrough关键字,它用于执行下一个case的体,如下一个列表所示。
列表 3.7. 开关语句:switch.go
var room = "lake"
switch { *1*
case room == "cave":
fmt.Println("You find yourself in a dimly lit cavern.")
case room == "lake":
fmt.Println("The ice seems solid enough.")
fallthrough *2*
case room == "underwater":
fmt.Println("The water is freezing cold.")
}
-
1 每个 case 中都有表达式。
-
2 跳转到下一个 case
之前的列表将生成以下输出:
The ice seems solid enough.
The water is freezing cold.
注意
在 C、Java 和 JavaScript 中,默认情况下会发生跳过,而 Go 采取更安全的方法,需要显式使用fallthrough关键字。
| |
快速检查 3.5
Q1:
将列表 3.7 修改为使用更简洁的
switch形式,因为每个比较都是与room进行的。
| |
QC 3.5 答案
1:
switch room { case "cave": fmt.Println("You find yourself in a dimly lit cavern.") case "lake": fmt.Println("The ice seems solid enough.") fallthrough case "underwater": fmt.Println("The water is freezing cold.") }
3.6. 循环中的重复
与多次输入相同的代码相比,for关键字为你重复代码。列表 3.8 循环直到count等于 0。
在每次迭代之前,表达式count > 0被评估以产生一个布尔值。如果值为false(count = 0),则循环终止——否则,它将运行循环体({和}之间的代码)。

列表 3.8. 倒计时循环:countdown.go
package main
import (
"fmt"
"time"
)
func main() {
var count = 10 *1*
for count > 0 { *2*
fmt.Println(count)
time.Sleep(time.Second)
count-- *3*
}
fmt.Println("Liftoff!")
}
-
1 声明并初始化
-
2 条件
-
3 减少 count;否则它将无限循环
一个无限循环没有指定for条件,但你仍然可以在任何时候break跳出循环。以下列表围绕 360°圆圈旋转并随机停止。
列表 3.9. 到无限远:infinity.go
var degrees = 0
for {
fmt.Println(degrees)
degrees++
if degrees >= 360 {
degrees = 0
if rand.Intn(2) == 0 {
break
}
}
}
注意
for循环还有其他形式,将在第 4 课(lessons 4)和第 9 课(9)中介绍。
| |
快速检查 3.6
Q1:
并非每次发射都能顺利进行。实现一个倒计时,每秒钟有 1%的概率发射失败,倒计时停止。
| |
QC 3.6 答案
1:
var count = 10 for count > 0 { fmt.Println(count) time.Sleep(time.Second) if rand.Intn(100) == 0 { break } count-- } if count == 0 { fmt.Println("Liftoff!") } else { fmt.Println("Launch failed.") }
摘要
-
布尔值是唯一可以用于条件的值。
-
Go 语言通过
if、switch和for提供分支和循环。 -
你使用了 25 个 Go 关键字中的 12 个:
package、import、func、var、if、else、switch、case、default、fallthrough、for和break。
让我们看看你是否掌握了这个...
实验:guess.go
编写一个猜数字程序。让计算机随机选择 1 到 100 之间的数字,直到它猜出你声明的程序顶部的数字。显示每个猜测以及它是否过大或过小。
第 4 课. 变量作用域
在阅读第 4 课之后,你将能够
-
了解变量作用域的好处
-
使用更简洁的方式来声明变量
-
看看变量作用域如何与
for、if和switch交互 -
了解何时使用宽作用域或窄作用域
在程序运行的过程中,许多变量被短暂使用然后丢弃。这是通过语言的作用域规则来实现的。
考虑这一点
你一次能记住多少件事情?
有建议称我们的短期记忆限制在大约七个项目左右,七个数字的电话号码是一个很好的例子。
计算机可以在它们的短期或随机存取存储器(RAM)中存储许多值,但记住代码不仅被计算机阅读,也被人类阅读。因此,代码应尽可能简单。
如果程序中的任何变量都可能随时更改,并且可以从任何地方访问,那么在大型程序中跟踪所有内容可能会变得相当混乱。变量作用域通过允许你专注于给定函数或代码部分中的相关变量,而无需关心其他变量,从而提供帮助。
4.1. 探索作用域
当一个变量被声明时,它进入作用域,或者说,变量变得可见。只要变量在作用域内,程序就可以访问它,但一旦变量不再在作用域内,尝试访问它将会报告错误。
变量作用域的一个好处是你可以为不同的变量重用相同的名称。你能想象如果你的程序中的每个变量都必须有一个唯一的名称吗?如果是这样,试着想象一个稍微大一点的程序。
作用域还有助于阅读代码,因为你不需要将所有变量都记在脑海中。一旦变量超出作用域,你就可以停止考虑那个变量。

在 Go 中,作用域通常从大括号{}开始和结束。在下面的列表中,main函数开始一个作用域,for循环开始一个嵌套作用域。
列表 4.1. 作用域规则:scope.go
package main
import (
"fmt"
"math/rand"
)
func main() {
var count = 0
for count < 10 { *1*
var num = rand.Intn(10) + 1
fmt.Println(num)
count++
} *2*
}
-
1 开始一个新的作用域。
-
2 这个作用域结束了。
count变量在函数作用域内声明,直到main函数结束才可见,而num变量在for循环的作用域内声明。循环结束后,num变量将超出作用域。
Go 编译器会在循环之后尝试访问num时报告错误。循环结束后,你可以访问count变量,因为它是在循环外部声明的,尽管实际上没有理由这样做。为了将count限制在循环的作用域内,你需要用 Go 中声明变量的不同方式。
快速检查 4.1
1
变量作用域如何为你带来好处?
2
变量超出作用域后会发生什么?修改列表 4.1 以在循环之后访问
num并查看会发生什么。
QC 4.1 答案
1
同一个变量名可以在多个地方使用而不会发生冲突。你只需要考虑当前在作用域内的变量。
2
变量不再可见或可访问。Go 编译器报告
undefined: num错误。
4.2. 简短声明
简短声明为 var 关键字提供了一种替代语法。以下两行是等效的:
var count = 10
count := 10
虽然节省了三个字符,但简短声明比 var 更受欢迎。更重要的是,简短声明可以到达 var 无法到达的地方。
下面的列表演示了 for 循环的一种变体,它结合了初始化、条件和递减 count 的后语句。当使用这种形式的 for 循环时,提供的顺序很重要:初始化、条件、后。
列表 4.2. 紧凑的倒计时:loop.go
var count = 0
for count = 10; count > 0; count-- {
fmt.Println(count)
}
fmt.Println(count) *1*
- 1 count 仍然在作用域内。
如果没有简短声明,count 变量必须在循环外部声明,这意味着它在循环结束后仍然保持作用域。
通过使用简短声明,下一个列表中的 count 变量作为 for 循环的一部分声明和初始化,一旦循环结束就超出作用域。如果尝试在循环外部访问 count,Go 编译器将报告 undefined: count 错误。
列表 4.3. 在 for 循环中的简短声明:short-loop.go
for count := 10; count > 0; count-- {
fmt.Println(count)
} *1*
- 1 计数器已不再在作用域内。
小贴士
为了最佳的可读性,请将变量声明在它们被使用的地方附近。
简短声明使得在 if 语句中声明新变量成为可能。在下面的列表中,num 变量可以在 if 语句的任何分支中使用。
列表 4.4. 在 if 语句中的简短声明:short-if.go
if num := rand.Intn(3); num == 0 {
fmt.Println("Space Adventures")
} else if num == 1 {
fmt.Println("SpaceX")
} else {
fmt.Println("Virgin Galactic")
} *1*
- 1 num 已不再在作用域内。
简短声明也可以作为 switch 语句的一部分使用,如下面的列表所示。
列表 4.5. 在 switch 语句中的简短声明:short-switch.go
switch num := rand.Intn(10); num {
case 0:
fmt.Println("Space Adventures")
case 1:
fmt.Println("SpaceX")
case 2:
fmt.Println("Virgin Galactic")
default:
fmt.Println("Random spaceline #", num)
}
快速检查 4.2
Q1:
如果在 列表 4.4 或 4.5 中没有使用简短声明,
num的作用域将如何受到影响?
| |
QC 4.2 答案
1:
在
if、switch或for关键字之后立即使用var声明变量是不可能的。如果没有简短声明,num必须在if/switch语句之前声明,因此num将在if/switch语句结束后保持作用域。
4.3. 窄作用域,宽作用域
下一个列表中的代码生成并显示一个随机日期——可能是一个前往火星的出发日期。它还展示了 Go 中的几个不同作用域,并说明了为什么在声明变量时考虑作用域很重要。
列表 4.6. 变量作用域规则:scope-rules.go
package main
import (
"fmt"
"math/rand"
)
var era = "AD" *1*
func main() {
year := 2018 *2*
switch month := rand.Intn(12) + 1; month { *3*
case 2:
day := rand.Intn(28) + 1 *4*
fmt.Println(era, year, month, day)
case 4, 6, 9, 11:
day := rand.Intn(30) + 1 *5*
fmt.Println(era, year, month, day)
default:
day := rand.Intn(31) + 1 *5*
fmt.Println(era, year, month, day)
} *6*
} *7*
-
1 时代在整个包中可用。
-
2 时代和年份在作用域内。
-
3 时代、年份和月份在作用域内。
-
4 时代、年份、月份和日期在作用域内。
-
5 新的一天开始了。
-
6 月份和日期已不在作用域内。
-
7 年份已不再在作用域内。
era变量在main函数外部,在包作用域中声明。如果main包中有多个函数,era将可以从所有这些函数中访问。
注意
在包作用域中声明的变量不支持简短声明,因此era不能在其当前位置使用era := "AD"进行声明。
year变量仅在main函数内部可见。如果有其他函数,它们可以看到era但不能看到year。函数作用域比包作用域窄。它从func关键字开始,到结束括号结束。
month变量在switch语句的任何地方都是可见的,但一旦switch语句结束,month就不再具有作用域。作用域从switch关键字开始,到switch的结束括号结束。
每个case都有自己的作用域,因此有三个独立的day变量。随着每个case的结束,在该case中声明的day变量将超出作用域。这是唯一没有大括号来指示作用域的情况。
列表 4.6 中的代码远非完美。month和day的作用域太窄,导致代码重复(Println、Println、Println)。当代码重复时,有人可能会在一个地方修改代码,但忘记在另一个地方修改(例如,决定不打印时代,但忘记更改一个情况)。有时代码重复是合理的,但被认为是一种代码异味,应该被检查。
为了消除重复并简化代码,列表 4.6 中的变量应在更宽泛的函数作用域中声明,这样它们在switch语句之后就可以用于后续操作。是时候重构了!重构意味着在不改变代码行为的情况下修改代码。以下列表中的代码仍然显示随机日期。
列表 4.7. 随机日期重构:random-date.go
package main
import (
"fmt"
"math/rand"
)
var era = "AD"
func main() {
year := 2018
month := rand.Intn(12) + 1
daysInMonth := 31
switch month {
case 2:
daysInMonth = 28
case 4, 6, 9, 11:
daysInMonth = 30
}
day := rand.Intn(daysInMonth) + 1
fmt.Println(era, year, month, day)
}
尽管较窄的作用域通常可以减少心理负担,但列表 4.6 证明了过于严格地约束变量可能会导致更少可读的代码。根据具体情况,重构直到无法进一步提高可读性为止。
快速检查 4.3
Q1:
如何识别变量作用域过窄的一种方法?
| |
QC 4.3 答案
1:
如果代码重复是由于变量声明的位置引起的。
摘要
-
开放的大括号
{引入了一个以关闭大括号}结束的新作用域。 -
即使没有大括号,
case和default关键字也会引入一个新的作用域。 -
变量声明的位置决定了它所在的作用域。
-
不仅简短声明更短,你还可以将其用于
var无法使用的地方。 -
与
for、if或switch在同一行声明的变量在其语句结束时具有作用域。 -
在某些情况下,宽作用域比窄作用域更好——反之亦然。
让我们看看你是否掌握了这个...
实验:random-dates.go
修改 listing 4.7 以处理闰年:
-
生成一个随机的年份而不是总是使用
2018。 -
对于二月,对于闰年将
daysInMonth设置为 29,对于其他年份设置为 28。提示:你可以在case块中放置一个if语句。 -
使用
for循环生成并显示 10 个随机日期。
第 5 课. 顶石:火星票
欢迎来到第一个挑战。现在是时候将 unit 1 中涵盖的所有内容应用到自己的程序中。你的挑战是编写一个票生成器,在 Go Playground 中使用变量、常量、switch、if 和 for。它还应利用 fmt 和 math/rand 包来显示和排列文本以及生成随机数。
在计划前往火星的旅行时,能够在一个地方看到多个太空旅行公司的票价会很有用。存在一些网站会汇总航空公司的票价,但到目前为止还没有针对太空旅行公司的。不过,这对您来说不是问题。您可以使用 Go 来教会计算机解决这类问题。

首先构建一个原型,生成 10 个随机票并使用漂亮的标题以表格格式显示,如下所示:
Spaceline Days Trip type Price
======================================
Virgin Galactic 23 Round-trip $ 96
Virgin Galactic 39 One-way $ 37
SpaceX 31 One-way $ 41
Space Adventures 22 Round-trip $ 100
Space Adventures 22 One-way $ 50
Virgin Galactic 30 Round-trip $ 84
Virgin Galactic 24 Round-trip $ 94
Space Adventures 27 One-way $ 44
Space Adventures 28 Round-trip $ 86
SpaceX 41 Round-trip $ 72
表格应该有四列:
-
提供服务的太空旅行公司
-
前往火星的旅行天数(单程)
-
价格是否覆盖往返票
-
价格(单位:百万美元)
![图片]()
对于每张票,随机选择以下太空旅行公司之一:Space Adventures、SpaceX 或 Virgin Galactic。
使用 2020 年 10 月 13 日作为所有票的出发日期。那时火星距离地球 6210 万公里。
随机选择船只的速度,从 16 到 30 km/s。这将决定前往火星的旅行时间和票价。让速度更快的船只更贵,价格从 3600 万美元到 5000 万美元不等。往返票加倍价格。
完成后,将你的解决方案发布到 Get Programming with Go 论坛 forums.manning.com/forums/get-programming-with-go。如果你遇到困难,可以在论坛上自由提问,或者查看附录中的解决方案。
第 2 单元. 类型
在 x86 计算机上,文本"Go"和数字28487都使用相同的零和一(0110111101000111)表示。类型确定了这些位和字节的意义。一个是两个字符的字符串,另一个是 16 位的整数(2 字节)。字符串类型用于多语言文本,16 位整数是许多数字类型之一。
第 2 单元涵盖了 Go 提供的原始类型,包括文本、字符、数字和其他简单值。在适当的时候,这些课程会揭示其优缺点,帮助你选择最合适的类型。
第 6 课. 实数
阅读完第 6 课后,你将能够
-
使用两种实数类型
-
理解内存与精度的权衡
-
在你的储蓄罐中解决舍入误差
计算机使用 IEEE-754 浮点标准存储和处理像 3.14159 这样的实数。浮点数可以非常大或非常小:想想星系和原子。具有这种多功能性,像 JavaScript 和 Lua 这样的编程语言完全使用浮点数。计算机还支持整数,这是下一课的主题。
考虑这一点
想象一个有三个杯子的游乐场游戏。最近的杯子价值 0.10 到 1.00 美元,下一个杯子价值 1 到 10 美元,最远的杯子价值 10 到 100 美元。选择一个杯子,投掷多达 10 枚硬币。如果中间的杯子落地四枚硬币价值 4 美元,你将如何赢得 100 美元?
为了用固定数量的空间表示许多可能的实数,浮点数就像选择 2,048 个杯子中的一个,并在其中放置从一枚到数万亿枚硬币。一些位代表一个杯子或桶,而其他位代表该桶内的硬币或偏移量。
一个杯子可能代表非常小的数字,另一个代表非常大的数字。尽管每个杯子可以容纳相同数量的硬币,但一些杯子比其他杯子更精确地代表较小的数字范围,而其他杯子则代表较大的数字范围但精度较低。
6.1. 声明浮点变量
每个变量都有一个类型。当你用实数声明和初始化一个变量时,你正在使用浮点类型。以下三行代码是等效的,因为 Go 编译器会推断出days是float64,即使你没有指定它:
days := 365.2425 *1*
var days = 365.2425
var days float64 = 365.2425
- 1 短声明(在第 4 课中介绍过 lesson 4)
了解days具有float64类型是有价值的,但指定float64是多余的。你、我和 Go 编译器都可以通过查看右侧的值来推断days的类型。只要值是一个带小数点的数字,类型就会是float64。
提示
golint工具提供关于编码风格的提示。它通过以下消息来劝阻杂乱:
"should omit type float64 from declaration of var days;
it will be inferred from the right-hand side"
如果你用一个整数初始化一个变量,Go 不知道你想要浮点数,除非你明确指定浮点类型:
var answer float64 = 42
快速检查 6.1
Q1:
answer := 42.0推断的类型是什么?
| |
快速检查 6.1 答案
1:
实数被推断为
float64。
6.1.1. 单精度浮点数
Go 有两种浮点数类型。默认浮点数类型是float64,这是一个 64 位浮点数类型,使用 8 个字节的内存。某些语言使用双精度一词来描述 64 位浮点数类型。
float32类型使用的内存是float64的一半,但精度较低。这种类型有时被称为单精度。要使用float32,必须在声明变量时指定类型。以下列表显示了float32的使用。
列表 6.1. 64 位与 32 位浮点数:pi.go
var pi64 = math.Pi
var pi32 float32 = math.Pi
fmt.Println(pi64) *1*
fmt.Println(pi32) *2*
-
1 打印 3.141592653589793
-
2 打印 3.1415927
当处理大量数据时,例如 3D 游戏中的数千个顶点,使用float32来牺牲精度以节省内存可能是有意义的。
提示
math 包中的函数在float64类型上操作,所以除非你有充分的理由,否则请优先使用float64。
| |
快速检查 6.2
Q1:
单精度
float32使用多少字节的内存?
| |
QC 6.2 答案
1:
float32使用 4 个字节(或 32 位)。
6.1.2. 零值
在 Go 中,每种类型都有一个默认值,称为零值。当你声明一个变量但没有用值初始化它时,就会应用这个默认值,如下一个列表所示。
列表 6.2. 声明一个没有值的变量:default.go
var price float64
fmt.Println(price) *1*
- 1 打印 0
之前的列表声明了没有值的price,因此 Go 将其初始化为零。对计算机来说,它与以下内容相同:
price := 0.0
对程序员来说,这种差异很微妙。当你声明price := 0.0时,就像说价格是免费的。在列表 6.2 中未指定price的值,暗示实际值尚未到来。
快速检查 6.3
Q1:
float32的零值是多少?
| |
QC 6.3 答案
1:
默认值是零(
0.0)。
6.2. 显示浮点数类型
当使用Print或Println与浮点数类型一起时,默认行为是显示尽可能多的数字。如果你不希望这样,你可以使用带有%f格式动词的Printf来指定数字的数量,如下一个列表所示。
列表 6.3. 浮点数格式化打印:third.go
third := 1.0 / 3
fmt.Println(third) *1*
fmt.Printf("%v\n", third) *1*
fmt.Printf("%f\n", third) *2*
fmt.Printf("%.3f\n", third) *3*
fmt.Printf("%4.2f\n", third) *4*
-
1 打印 0.3333333333333333
-
2 打印 0.333333
-
3 打印 0.333
-
4 打印 0.33
%f 动词以宽度和精度格式化third的值,如图 6.1 所示。
图 6.1. %f 格式动词

精度指定小数点后应出现多少位数字;例如,%.2f为两位数字,如图 6.2 所示。
图 6.2. 以宽度为 4,精度为 2 格式化的输出

宽度 指定要显示的最小字符数,包括小数点以及小数点前后的数字(例如,0.33 的宽度为 4)。如果宽度大于所需字符数,Printf 将用空格填充左侧。如果未指定宽度,Printf 将使用显示值所需的字符数。
要用零而不是空格左对齐,请在宽度前加一个零,如下面的列表所示。
列表 6.4. 零填充:third.go
fmt.Printf("%05.2f\n", third) *1*
- 1 打印 00.33
快速检查 6.4
1
将 列表 6.3 输入 Go 演示场 main 函数的主体中。尝试在
Printf语句中为宽度和精度设置不同的值。2
0015.1021 的宽度和精度是多少?
| |
QC 6.4 答案
1
third := 1.0 / 3 fmt.Printf("%f\n", third) *1* fmt.Printf("%7.4f\n", third) *2* fmt.Printf("%06.2f\n", third) *3*
- 1 打印 0.333333
- 2 打印 0.3333
- 3 打印 000.33
2
宽度为 9,精度为 4,使用零填充
"%09.4f"。
6.3. 浮点精度
在数学中,一些有理数不能在十进制形式中准确表示。数字 0.33 只是 ⅓ 的近似值。不出所料,对近似值进行计算的结果也是近似的:
-
⅓ + ⅓ + ⅓ = 1
-
0.33 + 0.33 + 0.33 = 0.99
浮点数也受到舍入误差的影响,除了浮点硬件使用二进制表示(仅使用 0 和 1)而不是十进制(使用 1–9)。结果是,计算机可以准确地表示 ⅓,但其他数字会有舍入误差,如下面的列表所示。

列表 6.5. 浮点不精确:float.go
third := 1.0 / 3.0
fmt.Println(third + third + third) *1*
piggyBank := 0.1
piggyBank += 0.2
fmt.Println(piggyBank) *2*
-
1 打印 1
-
2 打印 0.30000000000000004
如您所见,浮点数不是表示金钱的最佳选择。一个替代方案是使用整型存储美分的数量,这将在下一课中介绍。
另一方面,即使你的 piggyBank 差了一分,嗯,那也不是关键任务。只要你能为火星之旅存够钱,你就满意了。为了把舍入误差扫到地毯下,你可以使用 Printf 并设置两位数的精度。
为了最小化舍入误差,我们建议你在除法之前进行乘法。这样结果往往更准确,如以下两个列表中的温度转换示例所示。
列表 6.6. 除法优先:rounding-error.go
celsius := 21.0
fmt.Print((celsius/5.0*9.0)+32, "° F\n") *1*
fmt.Print((9.0/5.0*celsius)+32, "° F\n") *1*
- 1 打印 69.80000000000001° F
列表 6.7. 乘法优先:temperature.go
celsius := 21.0
fahrenheit := (celsius * 9.0 / 5.0) + 32.0
fmt.Print(fahrenheit, "° F") *1*
- 1 打印 69.8° F
快速检查 6.5
Q1:
避免舍入误差的最佳方法是什么?
| |
QC 6.5 答案
1:
不要使用浮点数。
6.4. 比较浮点数
在 列表 6.5 中,piggyBank 包含了 0.30000000000000004,而不是期望的 0.30. 在需要比较浮点数时请记住这一点:
piggyBank := 0.1
piggyBank += 0.2
fmt.Println(piggyBank == 0.3) *1*
- 1 打印 false
而不是直接比较浮点数,确定两个数字之间的绝对差异,然后确保差异不是太大。为了取float64的绝对值,math包提供了一个Abs函数:
fmt.Println(math.Abs(piggyBank-0.3) < 0.0001) *1*
- 1 打印为 true
小贴士
单个操作浮点误差的上限称为机器 epsilon,对于float64是 2^(-52),对于float32是 2^(-23)。不幸的是,浮点误差会迅速累积。向一个空的piggyBank中添加 11 个一角硬币(每个 0.10 美元),与 1.10 美元相比,舍入误差超过了 2^(-52)。这意味着你最好选择一个针对你应用程序的特定容差——在这种情况下,0.0001。
快速检查 6.6
Q1:
如果你向一个空的
piggyBank类型为float64中添加 11 个一角硬币(每个 0.10 美元),最终余额是多少?
QC 6.6 答案
1:
piggyBank := 0.0 for i := 0; i < 11; i++ { piggyBank += 0.1 } fmt.Println(piggyBank) *1*
- 1 打印 1.0999999999999999
概述
-
Go 可以为你推断类型。特别是,Go 将为用实数初始化的变量推断
float64。 -
浮点类型用途广泛但并不总是准确。
-
你使用了 Go 的 15 种数值类型中的 2 种(
float64,float32)。
让我们看看你是否掌握了这些...
实验:piggy.go
为你的朋友存一些钱买礼物。编写一个程序,将镍币($0.05)、一角硬币($0.10)和 quarter($0.25)随机放入一个空的猪储蓄罐中,直到它包含至少 $20.00。在每次存款后显示猪储蓄罐的运行余额,并使用适当的宽度和精度进行格式化。
第 7 课:整数
在阅读第 7 课之后,你将能够
-
使用 10 种整数类型
-
选择正确的类型
-
使用十六进制和二进制表示
Go 提供了 10 种不同的整数类型,统称为整数。整数不会受到浮点类型精度问题的困扰,但它们不能存储分数数字,并且它们的范围有限。你选择的整数类型将取决于特定情况下所需值的范围。
考虑这一点
你可以用两个符号表示多少个数字?
如果符号可以通过位置单独识别,那么有四种可能的排列。两个符号,没有符号,一个符号,或者另一个符号。你可以表示四个数字。
计算机基于位。位可以是关闭的或开启的——0 或 1。八个位可以表示 256 个不同的值。要表示数字 4,000,000,000 需要多少位?
7.1. 声明整数变量
五种整数类型是有符号的,这意味着它们可以表示正负整数。最常见的整数类型是有符号整数,缩写为int:
var year int = 2018
其他五种整数类型是无符号的,这意味着它们仅用于正数。无符号整数的缩写为uint:
var month uint = 2
当使用类型推断时,Go 总是为字面量整数选择int类型。以下三行是等效的:
year := 2018
var year = 2018
var year int = 2018
小贴士
与 第 6 课 中的浮点类型一样,当可以推断时,最好不指定 int 类型。
| |
快速检查 7.1
Q1:
如果你的杯子里的水是半满的,你会使用哪种整型来表示杯中水的毫升数?
QC 7.1 答案
1:
uint类型(无符号整数)仅用于正整数。
7.1.1. 适用于各种场合的整型
整数,无论是带符号的还是无符号的,都有各种大小。大小影响它们的最大和最小值以及它们消耗的内存量。有八种与架构无关的类型,后缀为它们所需的位数,总结在 表 7.1 中。

表 7.1. 架构无关的整型
| 类型 | 范围 | 存储空间 |
|---|---|---|
| int8 | –128 到 127 | 8 位(一个字节) |
| uint8 | 0 到 255 | |
| int16 | –32,768 到 32,767 | 16 位(两个字节) |
| uint16 | 0 到 65535 | |
| int32 | –2,147,483,648 到 2,147,483,647 | 32 位(四个字节) |
| uint32 | 0 到 4,294,967,295 | |
| int64 | –9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 | 64 位(八字节) |
| uint64 | 0 到 18,446,744,073,709,551,615 |
有很多类型可供选择!在本课的后面部分,我们将展示一些示例,说明特定整型何时适用,以及如果程序超出可用范围会发生什么。
在 表 7.1 中未列出两种整型。int 和 uint 类型针对目标设备是最佳选择。Go Playground、Raspberry Pi 2 和旧款手机提供 32 位环境,其中 int 和 uint 都是 32 位值。任何近期计算机都将提供 64 位环境,其中 int 和 uint 将是 64 位值。
提示
如果你正在处理大于二十亿的数字,并且如果代码可以在旧款 32 位硬件上运行,请确保使用 int64 或 uint64 而不是 int 和 uint。
注意
虽然在某些设备上可能会认为 int 是 int32 而在其他设备上是 int64,但这些都是三种不同的类型。int 类型不是其他类型的别名。
| |
快速检查 7.2
Q1:
哪些整型支持值 –20,151,021?
QC 7.2 答案
1:
int32、int64和int类型都可以使用。
7.1.2. 了解你的类型
如果你好奇 Go 编译器推断出的类型,Printf 函数提供了 %T 格式说明符来显示变量的类型,如下面的列表所示。
列表 7.1. 检查变量的类型:inspect.go
year := 2018
fmt.Printf("Type %T for %v\n", year, year) *1*
- 1 打印出类型 int 对 2018
你可以告诉 Printf 使用第一个参数 [1] 作为第二个格式说明符,而不是重复变量两次:
days := 365.2425
fmt.Printf("Type %T for %[1]v\n", days) *1*
- 1 打印出类型 float64 对 365.2425
快速检查 7.3
Q1:
Go 如何推断引号之间的文本、整数、实数和单词
true(不带引号)的类型?将列表 7.1 扩展以声明具有不同值的变量,并运行程序以查看 Go 推断的类型。
| |
QC 7.3 答案
1:
a := "text" fmt.Printf("Type %T for %[1]v\n", a) *1* b := 42 fmt.Printf("Type %T for %[1]v\n", b) *2* c := 3.14 fmt.Printf("Type %T for %[1]v\n", c) *3* d := true fmt.Printf("Type %T for %[1]v\n", d) *4*
- 1 打印类型为 string 的文本
- 2 打印类型为 int 的 42
- 3 打印类型为 float64 的 3.14
- 4 打印类型为 bool 的 true
7.2. 用于 8 位颜色的uint8类型
在层叠样式表(CSS)中,屏幕上的颜色被指定为红、绿、蓝三元组,每个范围都是 0-255。这是使用uint8类型的完美情况,它是一个 8 位无符号整数,能够表示 0-255 的值:
var red, green, blue uint8 = 0, 141, 213
在此情况下,使用uint8而不是常规的int有以下好处:
-
使用
uint8,变量被限制在有效值的范围内,与 32 位整数相比,消除了超过四十亿个错误的可能性。 -
如果需要存储大量的颜色,例如在未压缩的图像中,使用 8 位整数可以实现相当大的内存节省。
Go 中的十六进制
CSS 中的颜色以十六进制而不是十进制指定。十六进制使用比十进制的 10 个数字多 6 个数字来表示数字。前 10 个数字与相同的 0 到 9 相同,但之后是 A 到 F。十六进制中的 A 相当于十进制的 10,B 相当于 11,依此类推,直到 F,它是 15。
十进制对于十指生物来说是一个很好的系统,但十六进制更适合计算机。一个十六进制数字消耗四个位,称为nibble。两个十六进制数字需要精确的八个位,即一个字节,这使得十六进制成为为uint8指定值的方便方式。
下表显示了一些十六进制数及其等效的十进制数。
十六进制和十进制值
| 十六进制 | 十进制 |
|---|---|
| A | 10 |
| F | 15 |
| 10 | 16 |
| FF | 255 |
要区分十进制和十六进制,Go 需要十六进制的前缀0x。这两行代码是等效的:
var red, green, blue uint8 = 0, 141, 213
var red, green, blue uint8 = 0x00, 0x8d, 0xd5
要以十六进制显示数字,可以使用Printf中的%x或%X格式动词:
fmt.Printf("%x %x %x", red, green, blue) *1*
- 1 打印 0 8d d5
要输出在.css 文件中感觉自然的颜色,十六进制值需要一些填充。与%v和%f格式动词一样,你可以指定最小位数(2 位)并使用%02x进行零填充:
fmt.Printf("color: #%02x%02x%02x;", red, green, blue) *1*
- 1 打印颜色:#008dd5;
| |
快速检查 7.4
Q1:
存储类型为
uint8的值需要多少字节?
| |
QC 7.4 答案
1:
8 位(无符号)整数只需要一个字节。
7.3. 整数环绕
整数没有使浮点数不准确的舍入错误,但所有整数类型都有不同的问题:范围有限。当超出该范围时,Go 中的整数类型会环绕。
8 位无符号整数 (uint8) 的范围是 0-255。超过 255 的增量将回绕到 0。以下列表递增了有符号和无符号 8 位整数,导致它们回绕。
列表 7.2. 整数环绕:integers-wrap.go
var red uint8 = 255
red++
fmt.Println(red) *1*
var number int8 = 127
number++
fmt.Println(number) *2*
-
1 打印 0
-
2 打印 -128
7.3.1. 查看位
要理解整数为什么回绕,请查看位。%b 格式动词将显示整数值的位。像其他格式动词一样,%b 可以用零填充到最小长度,如本列表所示。
列表 7.3. 显示位:bits.go
var green uint8 = 3
fmt.Printf("%08b\n", green) *1*
green++
fmt.Printf("%08b\n", green) *2*
-
1 打印 00000011
-
2 打印 00000100
快速检查 7.5
使用 Go Playground 来实验整数的包装行为:
1
在 列表 7.2 中,代码将
red和number增加 1。当你向任一变量添加更大的数字时会发生什么?2
当
red为 0 或number为 -128 时,递减会发生什么?3
包装也适用于 16 位、32 位和 64 位整数。如果你声明一个赋值为 65535 的最大值的
uint16,然后加 1,会发生什么?
QC 7.5 答案
1
// add a number larger than one var red uint8 = 255 red += 2 fmt.Println(red) *1* var number int8 = 127 number += 3 fmt.Println(number) *2*2
// wrap the other way red = 0 red-- fmt.Println(red) *3* number = -128 number-- fmt.Println(number) *4*3
// wrapping with a 16-bit unsigned integer var green uint16 = 65535 green++ fmt.Println(green) *5*
- 1 打印 1
- 2 打印 -126
- 3 打印 255
- 4 打印 127
- 5 打印 0
提示
数学包将 math.MaxUint16 定义为 65535,并为每个架构无关的整数类型定义了类似的 min/max 常量。记住,int 和 uint 可能是 32 位或 64 位,这取决于底层硬件。
在 列表 7.3 中,增加 green 导致 1 被进位,留下右边的零。结果是二进制的 00000100,或十进制的 4,如图 7.1 所示。
图 7.1. 二进制加法中的进位

当增加 255 时,也会发生相同的事情,但有一个关键的区别:只有八个位可用,所以被进位的 1 没有地方去,因此 blue 的值保持为 0,如图 7.2 所示。
列表 7.4. 整数环绕时的位:bits-wrap.go
var blue uint8 = 255
fmt.Printf("%08b\n", blue) *1*
blue++
fmt.Printf("%08b\n", blue) *2*
-
1 打印 11111111
-
2 打印 00000000
图 7.2. 进位应该去哪里?

在某些情况下,包装可能正是你想要的,但并不总是如此。避免包装的最简单方法是使用足够大的整数类型来存储你期望存储的值。
快速检查 7.6
Q1:
哪个格式动词可以让你查看位?
QC 7.6 答案
1:
%b格式动词以二进制形式输出整数。
7.3.2. 避免时间回绕
在基于 Unix 的操作系统上,时间表示为自 1970 年 1 月 1 日 UTC(协调世界时)以来的秒数。到 2038 年,自 1970 年 1 月 1 日以来的秒数将超过 20 亿,这是 int32 的容量。
幸运的是,int64 可以支持远超过 2038 年的日期。在这种情况下,int32 或 int 简直无法胜任。只有 int64 和 uint64 整数类型能够在所有平台上存储超过二十亿的数字。
列表 7.5 中的代码使用了 time 包中的 Unix 函数。它接受两个 int64 参数,对应于自 1970 年 1 月 1 日以来的秒数和纳秒数。使用一个足够大的值(超过 120 亿)可以证明在 Go 中日期超过 2038 年仍然可以正常工作。
列表 7.5. 64 位整数:time.go
package main
import (
"fmt"
"time"
)
func main() {
future := time.Unix(12622780800, 0)
fmt.Println(future) *1*
}
- 1 在 Go 操场中打印 2370-01-01 00:00:00 +0000 UTC
快速检查 7.7
Q1:
应该选择哪种整数类型来避免溢出?
QC 7.7 答案
1:
使用足够大的整数类型来存储你期望存储的值。
摘要
-
最常见的整数类型是
int和uint,但有些情况下需要更小或更大的类型。 -
整数类型需要仔细选择,以避免溢出,除非溢出是你想要的。
-
你已经了解了 Go 中 15 种数字类型中的 10 种(
int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64)。
让我们看看你是否掌握了这个...
实验:piggy.go
编写一个新的猪储蓄罐程序,使用整数来跟踪美分而不是美元的数量。随机将五分镍币(5¢)、一角硬币(10¢)和二十五分硬币(25¢)放入空的猪储蓄罐中,直到它包含至少 $20。
在每次存款后显示猪储蓄罐的运行余额(例如,$1.05)。
小贴士
如果你需要找到两个数字相除的余数,请使用取模运算符(%)。
第 8 课. 大数
在阅读 第 8 课 之后,你将能够
-
通过指定指数来保存零键
-
使用 Go 的
big包处理非常大的数字 -
使用大常量和字面值
计算机编程充满了权衡。浮点数类型可以存储任何大小的数字,但它们有时会缺乏精度和准确性。整数是准确的,但范围有限。如果你需要一个非常大且准确的数字怎么办?本课探讨了两种替代原生 float64 和 int 类型的方案。
考虑这一点
CPU 优化了整数和浮点数数学,但其他数字表示也是可能的。当你需要处理大数时,Go 有你需要的支持。
有哪些情况下整数太小,浮点数不够精确,或者另一种数字类型会更合适?
8.1. 达到上限
如果你还没有意识到,64 位整数非常巨大——比它们的 32 位对应物大得多。
为了获得一些视角,最近的恒星,半人马座阿尔法星,距离我们 41.3 万亿公里。万亿:这是一个后面跟着 12 个零的数字,或者 10¹²。你不需要费力地输入每个零,你可以在 Go 中使用指数来表示这样的数字,如下所示:
var distance int64 = 41.3e12
int32或uint32无法包含如此大的数字,但int64并不费力。现在你可以继续你的业务,也许计算一下到达半人马座α星需要多少天,这个任务将在下面的列表中解决。
列表 8.1. 到半人马座α星需要的天数:alpha.go
const lightSpeed = 299792 // km/s
const secondsPerDay = 86400
var distance int64 = 41.3e12
fmt.Println("Alpha Centauri is", distance, "km away.") *1*
days := distance / lightSpeed / secondsPerDay
fmt.Println("That is", days, "days of travel at light speed.") *2*
-
1 打印半人马座α星距离 41300000000000 公里。
-
2 打印这是以光速旅行 1594 天。
即使 64 位整数很大,还有更大的东西:空间。仙女座星系距离我们 24 千万亿(10¹⁸)公里。即使是最大的无符号整数(uint64)也只能包含高达 18 千万亿的数量。尝试声明一个超过 18 千万亿的变量会报告溢出错误:
var distance uint64 = 24e18 *1*
- 1 24000000000000000000 超过 uint64 的容量
但别慌——还有一些选择。你可以使用浮点数数学。这并不是一个坏主意,你已经知道浮点数是如何工作的。但还有另一种方法。下一节将探讨 Go 的big包。
注意
如果一个变量没有显式的类型,Go 将为包含指数的数字推断float64。
快速检查 8.1
Q1:
火星与地球之间的距离在 5600 万公里到 4010 万公里之间。使用指数(
e)语法将这些两个值表示为整数。
QC 8.1 答案
1:
var distance int = 56e6 distance = 401e6
8.2. 大包
big包提供了三种类型:
-
big.Int用于大整数,当 18 千万亿不够用时。 -
big.Float用于任意精度的浮点数。 -
big.Rat用于分数,如 1/3。
注意
你的代码也可以声明新的类型,但我们在第 13 课([kindle_split_024.html#ch13])再回到这个话题。
big.Int类型可以愉快地存储和操作到仙女座星系的距离,仅仅 24 千万亿公里。
选择使用big.Int要求你在方程中的所有地方都使用它,甚至包括你之前有的常数。NewInt函数接受一个int64并返回一个big.Int:
lightSpeed := big.NewInt(299792)
secondsPerDay := big.NewInt(86400)
NewInt对于像 24 千万亿这样的数字没有帮助。它不会适合在int64中,所以你可以从字符串创建一个big.Int:
distance := new(big.Int)
distance.SetString("24000000000000000000", 10)
创建一个新的big.Int后,通过调用SetString方法将其值设置为 24 千万亿。24 千万亿这个数字是以 10(十进制)为基数的,所以第二个参数是 10。
注意
方法与函数类似。你将在第 13 课([kindle_split_024.html#ch13])中了解所有关于它们的内容。new内置函数用于指针,这在第 26 课([kindle_split_040.html#ch26])中有介绍。
在所有值就绪后,Div方法执行必要的除法,以便结果可以显示,如下面的列表所示。
列表 8.2. 到仙女座星系的距离:andromeda.go
package main
import (
"fmt"
"math/big"
)
func main() {
lightSpeed := big.NewInt(299792)
secondsPerDay := big.NewInt(86400)
distance := new(big.Int)
distance.SetString("24000000000000000000", 10)
fmt.Println("Andromeda Galaxy is", distance, "km away.") *1*
seconds := new(big.Int)
seconds.Div(distance, lightSpeed)
days := new(big.Int)
days.Div(seconds, secondsPerDay)
fmt.Println("That is", days, "days of travel at light speed.") *2*
}
-
1 打印仙女座星系距离 24000000000000000000 公里。
-
2 打印这是以光速旅行 926568346 天。
正如你所见,这些大型类型与原生的int和float64类型相比,更难以处理。它们也运行得更慢。这是为了能够准确表示任何大小的数字所做出的权衡。
快速检查 8.2
Q1:
有两种方法可以创建包含数字 86,400 的
big.Int?
| |
QC 8.2 答案
1:
使用
NewInt函数构建big.Int:secondsPerDay := big.NewInt(86400)或者使用
SetString方法:secondsPerDay := new(big.Int) secondsPerDay.SetString("86400", 10)
8.3. 不寻常大小的常量
常量可以像变量一样声明类型。就像变量一样,一个uint64常量不可能包含像 24 千兆这样的数字:
const distance uint64 = 24000000000000000000 *1*
- 1 常量 24000000000000000000 溢出 uint64
当你声明一个没有类型的常量时,事情变得有趣。对于变量,Go 使用类型推断来确定类型,在 24 千兆的情况下,会溢出int类型。常量是不同的。常量不是推断类型,而是可以是未类型化的。以下行不会引发溢出错误:
const distance = 24000000000000000000
常量使用const关键字声明,但你的程序中的每个字面量值也是一个常量。这意味着可以直接使用不寻常大小的数字,如下所示。

列表 8.3. 不寻常大小的字面量:constant.go
fmt.Println("Andromeda Galaxy is", 24000000000000000000/299792/86400, "light
days away.") *1*
- 1 打印仙女座星系距离为 926568346 光日。
常量和字面量的计算是在编译期间而不是在程序运行时进行的。Go 编译器是用 Go 编写的。在底层,未类型化的数值常量由big包支持,使得可以进行所有常规操作,包括超过 18 千兆的数字,如下所示。
列表 8.4. 不寻常大小的常量:constant.go
const distance = 24000000000000000000
const lightSpeed = 299792
const secondsPerDay = 86400
const days = distance / lightSpeed / secondsPerDay
fmt.Println("Andromeda Galaxy is", days, "light days away.") *1*
- 1 打印仙女座星系距离为 926568346 光日。
只要适合,常量值可以分配给变量。int不能包含 24 千兆,但 926,568,346 可以很好地放入:
km := distance *1*
days := distance / lightSpeed / secondsPerDay *2*
-
1 常量 24000000000000000000 溢出 int。
-
2 926568346 可以放入 int 中。
对于不寻常大小的常量有一些注意事项。尽管 Go 编译器利用big包来处理未类型化的数值常量,但常量和big.Int值并不是可互换的。列表 8.2 显示了一个包含 24 千兆的big.Int,但你不能显示distance常量,因为会引发溢出错误:
fmt.Println("Andromeda Galaxy is", distance, "km away.") *1*
- 1 常量 24000000000000000000 溢出 int。
非常大的常量当然很有用,但它们不能替代big包。
快速检查 8.3
Q1:
常量和字面量的计算何时进行?
| |
QC 8.3 答案
1:
Go 编译器在编译过程中会简化包含常量和字面量的等式。
摘要
-
当原生类型无法满足需求时,
big包为你提供了保障。 -
使用未类型化的常量,可以实现大事,而且所有数值字面量也是未类型化的常量。
-
无类型常量在传递给函数时必须转换为有类型变量。
让我们看看你是否掌握了这个...
实验:canis.go
大犬座矮星系是距离我们太阳最近的已知星系,距离为 236,000,000,000,000,000 公里(尽管有些人争论它是否是一个星系)。使用常量将这个距离转换为光年。
第 9 课. 多语言文本
在阅读完第 9 课后,你将能够
-
访问和操作单个字母
-
加密和解密秘密信息
-
为多语言世界编写你的程序
从"Hello, playground"开始,你一直在你的程序中使用文本。单个字母、数字和符号被称为字符。当你将字符连接在一起并将它们放在引号之间时,它被称为字面字符串。

考虑这一点
你知道计算机用 1 和 0 表示数字。如果你是一台计算机,你会如何表示字母表和人类语言?
如果你说是用数字,你是对的。字母表的字符有数值,这意味着你可以像操作数字一样操作它们。
虽然并非完全直接,但每个书写语言的字符和无数的 emoji 加起来有数千个字符。有一些技巧可以以空间高效和灵活的方式表示文本。
9.1. 声明字符串变量
用引号包裹的字面值被推断为string类型,所以以下三行是等效的:
peace := "peace"
var peace = "peace"
var peace string = "peace"
如果你声明一个变量而没有提供值,它将使用其类型的零值初始化。string类型的零值是一个空字符串(""):
var blank string
9.1.1. 原始字符串字面量
字符串字面量可能包含转义序列,如第 2 课中提到的\n。为了避免将\n替换为新行,你可以用反引号()而不是引号("`)包裹文本,如下面的列表所示。反引号表示原始字符串字面量。
列表 9.1. 原始字符串字面量:raw.go
fmt.Println("peace be upon you\nupon you be peace")
fmt.Println(`strings can span multiple lines with the \n escape sequence`)
之前的列表显示了以下输出:
peace be upon you
upon you be peace
strings can span multiple lines with the \n escape sequence
与传统的字符串字面量不同,原始字符串字面量可以跨越多行源代码,如下一个列表所示。
列表 9.2. 多行原始字符串字面量:raw-lines.go
fmt.Println(`
peace be upon you
upon you be peace`)
运行列表 9.2 将产生以下输出,包括用于缩进的制表符:
peace be upon you
upon you be peace
如下列表所示,字面字符串和原始字符串都产生字符串。
列表 9.3. 字符串类型:raw-type.go
fmt.Printf("%v is a %[1]T\n", "literal string") *1*
fmt.Printf("%v is a %[1]T\n", `raw string literal`) *2*
-
1 打印字面字符串是字符串
-
2 打印原始字符串字面量是字符串
快速检查 9.1
Q1:
对于 Windows 文件路径
C:\go,你会使用字符串字面量还是原始字符串字面量,为什么?
QC 9.1 答案
1:
使用原始字符串字面量
`C:\go`,因为"C:\go"会因为未知转义序列错误而失败。
9.2. 字符、码点、符文和字节
Unicode 联盟为超过一百万个独特的字符分配了数字值,称为 码点。例如,65 是大写字母 A 的码点,128515 是一个笑脸
。
要表示单个 Unicode 码点,Go 提供了 rune,它是 int32 类型的别名。
byte 是 uint8 类型的别名。它旨在用于二进制数据,尽管 byte 可以用于由 ASCII 定义的英语字符,ASCII 是 Unicode 的一个较老的 128 个字符子集。
类型别名
别名是同一类型的另一个名称,因此 rune 和 int32 可以互换。尽管 byte 和 rune 从 Go 的开始就存在了,但 Go 1.9 引入了声明自己的类型别名的功能。语法如下所示:
type byte = uint8
type rune = int32
byte 和 rune 的行为与它们所代表的整数类型相同,如下所示。
列表 9.4. rune 和 byte:rune.go
var pi rune = 960
var alpha rune = 940
var omega rune = 969
var bang byte = 33
fmt.Printf("%v %v %v %v\n", pi, alpha, omega, bang) *1*
- 1 打印 960 940 969 33
要显示字符而不是它们的数值,可以使用 %c 格式说明符与 Printf 一起使用:
fmt.Printf("%c%c%c%c\n", pi, alpha, omega, bang) *1*
- 1 打印 πάω!
提示
任何整数类型都可以与 %c 一起使用,但 rune 别名表明数字 960 代表一个字符。
与记住 Unicode 码点相比,Go 提供了字符字面量。只需将字符用单引号 'A' 括起来即可。如果没有指定类型,Go 将推断为 rune,因此以下三行是等效的:
grade := 'A'
var grade = 'A'
var grade rune = 'A'
grade 变量仍然包含一个数值,在这种情况下是 65,即大写 'A' 的码点。字符字面量也可以与 byte 别名一起使用:
var star byte = '*'
快速检查 9.2
1
ASCII 编码了多少个字符?
2
byte是哪种类型的别名?rune呢?3
破折号 (
*)、笑脸和重音符号 é 的码点是什么?
| |
QC 9.2 答案
1
128 个字符。
2
byte是uint8类型的别名。rune是int32类型的别名。3
var star byte = '*' fmt.Printf("%c %[1]v\n", star) *1* smile := '' fmt.Printf("%c %[1]v\n", smile) *2* acute := 'é' fmt.Printf("%c %[1]v\n", acute) *3*
- 2 打印 * 42
- 2 打印
128515
- 3 打印 é 233
9.3. 拉动字符串
提线木偶师通过拉动线来操纵木偶,但 Go 中的字符串并不容易受到操纵。可以将变量分配给不同的字符串,但字符串本身不能被更改:
peace := "shalom"
peace = "salām"
你的程序可以访问单个字符,但不能修改字符串中的字符。以下列表使用方括号 [] 来指定字符串的索引,它访问单个字节(ASCII 字符)。索引从零开始。
列表 9.5. 字符串索引:index.go
message := "shalom"
c := message[5]
fmt.Printf("%c\n", c) *1*
- 1 打印 m
Go 中的字符串是 不可变的,就像 Python、Java 和 JavaScript 中的字符串一样。与 Ruby 中的字符串和 C 中的字符数组不同,你无法在 Go 中修改字符串:
message[5] = 'd' *1*
- 1 无法分配给消息[5]
快速检查 9.3
Q1:
编写一个程序来打印
"shalom"字符串的每个字节(ASCII 字符),每行一个字符。
QC 9.3 答案
1:
message := "shalom" for i := 0; i < 6; i++ { c := message[i] fmt.Printf("%c\n", c) }
9.4. 使用凯撒密码操作字符
在公元 2 世纪,发送秘密信息的一种有效方法是将每个字母都进行位移,所以'a'变成'd','b'变成'e',以此类推。结果可能看起来像外语:
L fdph, L vdz, L frqtxhuhg.
尤利乌斯·凯撒
事实证明,使用计算机操作字符作为数值是非常容易的,如下面的列表所示。

列表 9.6. 操作单个字符:caesar.go
c := 'a'
c = c + 3
fmt.Printf("%c", c) *1*
- 1 打印 d
列表 9.6 中的代码有一个问题。它没有考虑到所有关于 xylophones、yaks 和 zebras 的消息。为了满足这一需求,原始的凯撒密码会回绕,所以'x'变成'a','y'变成'b','z'变成'c'。由于英语字母表中有 26 个字母,这是一个简单的问题:
if c > 'z' {
c = c - 26
}
为了解密这个凯撒密码,应该减去 3 而不是加 3。但是,你需要考虑到c < 'a'的情况,通过加 26 来处理。真麻烦。
快速检查 9.4
Q1:
如果
c是小写字母'g',表达式c = c - 'a' + 'A'的结果是什么?
QC 9.4 答案
1:
字母被转换为大写:
c := 'g' c = c - 'a' + 'A' fmt.Printf("%c", c) *1*
- 1 打印 G
9.4.1. 一种现代变体
ROT13(旋转 13)是 20 世纪的一种凯撒密码变体。它有一个区别:它加 13 而不是 3。使用 ROT13,加密和解密是同一个方便的操作。
假设,在扫描天空中寻找外星通讯的过程中,SETI 研究所接收到了以下信息的传输:
message := "uv vagreangvbany fcnpr fgngvba"
我们怀疑这条message实际上是用 ROT13 加密的英文文本。姑且称之为一种直觉。在你能够破解这个密码之前,还有一件事你需要知道。这条message长度为 30 个字符,这可以通过内置的len函数来确定:
fmt.Println(len(message)) *1*
- 1 打印 30
注意
Go 有一系列内置函数,不需要导入语句。len函数可以确定各种类型的长度。在这种情况下,len返回字符串的字节长度。
下面的列表将解密来自太空的信息。在 Go 游乐场中运行它,以找出外星人说了什么。
列表 9.7. ROT13 密码:rot13.go
message := "uv vagreangvbany fcnpr fgngvba"
for i := 0; i < len(message); i++ { *1*
c := message[i]
if c >= 'a' && c <= 'z' { *2*
c = c + 13
if c > 'z' {
c = c - 26
}
}
fmt.Printf("%c", c)
}
-
1 遍历每个 ASCII 字符
-
2 保持空格和标点符号不变
注意,前面列表中的 ROT13 实现仅适用于 ASCII 字符(字节)。它会在西班牙语或俄语的消息中产生混淆。下一节将探讨解决这个问题的方法。
快速检查 9.5
1
当内置的
len函数传递一个字符串时,它会做什么?2
将列表 9.7 输入到 Go 游乐场。这条信息说了什么?
QC 9.5 答案
1
len函数返回字符串的字节长度。2
hi 国际空间站
9.5. 将字符串解码为符文
Go 中的字符串使用 UTF-8 编码,这是 Unicode 代码点的几种编码之一。UTF-8 是一种有效的可变长度编码,其中单个代码点可能使用 8 位、16 位或 32 位。通过使用可变长度编码,UTF-8 使得从 ASCII 到 UTF-8 的转换变得简单,因为 ASCII 字符与其 UTF-8 编码的对应字符相同。
注意
UTF-8 是万维网占主导地位的字符编码。它由 Go 的设计者之一 Ken Thompson 在 1992 年发明。
列表 9.7 中的 ROT13 程序访问了 message 字符串的各个字节(8 位),而没有考虑到多字节字符(16 位或 32 位)。这就是为什么它对英语字符(ASCII)工作得很好,但对于俄语和西班牙语会产生混乱的结果。你可以做得更好,朋友。
支持其他语言的第一步是在操作之前将字符解码为 rune 类型。幸运的是,Go 提供了解码 UTF-8 编码字符串的函数和语言特性。
utf8 包提供了确定字符串在符文长度而不是字节长度以及解码字符串的第一个字符的函数。DecodeRuneInString 函数返回第一个字符和该字符消耗的字节数,如 列表 9.8 所示。
注意
与许多编程语言不同,Go 中的函数可以返回多个值。多个返回值将在第十二部分中讨论。
列表 9.8. utf8 包:spanish.go
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
question := "¿Cómo estás?"
fmt.Println(len(question), "bytes") *1*
fmt.Println(utf8.RuneCountInString(question), "runes") *2*
c, size := utf8.DecodeRuneInString(question)
fmt.Printf("First rune: %c %v bytes", c, size) *3*
}
-
1 打印 15 个字节
-
2 打印 12 个符文
-
3 打印第一个符文:¿ 2 个字节
Go 语言提供了 range 关键字来遍历各种集合(在第 4 单元中介绍)。它还可以解码 UTF-8 编码的字符串,如下所示。
列表 9.9. 解码符文:spanish-range.go
question := "¿Cómo estás?"
for i, c := range question {
fmt.Printf("%v %c\n", i, c)
}
在每次迭代中,变量 i 和 c 被分配给字符串中的索引和该位置的代码点(符文)。
如果您不需要索引,空白标识符(下划线)允许您忽略它:
for _, c := range question {
fmt.Printf("%c ", c) *1*
}
- 1 打印 ¿ C ó m o e s t á s ?
快速检查 9.6
1
英语字母表
"abcdefghijklmnopqrstuvwxyz"中有多少个符文?有多少个字节?2
符文
'¿'中有多少个字节?
QC 9.6 答案
1
英语字母表中包含 26 个符文和 26 个字节。
2
符文
'¿'中有 2 个字节。
概述
-
在原始字符串字面量(
')中,转义序列如\n被忽略。 -
字符串是不可变的。可以访问单个字符,但不能更改。
-
字符串使用一种称为 UTF-8 的可变长度编码,其中每个字符占用 1-4 个字节。
-
byte是uint8类型的别名,而rune是int32类型的别名。 -
range关键字可以将 UTF-8 编码的字符串解码为 runes。
让我们看看你是否掌握了这一点...
实验:caesar.go
解密朱利叶斯·凯撒的引言:
L fdph, L vdz, L frqtxhuhg.
朱利叶斯·凯撒
你的程序需要将大写和小写字母向左或向右移动 -3 位。记住,'a' 变成 'x','b' 变成 'y','c' 变成 'z',同样适用于大写字母。
实验:international.go
使用 ROT13 对西班牙语信息“Hola Estación Espacial Internacional”进行加密。修改列表 9.7 以使用range关键字。现在当你对西班牙语文本使用 ROT13 时,带重音的字符会被保留。
第 10 课。类型之间的转换
阅读完第 10 课后,你将能够
- 在数值、字符串和布尔值类型之间进行转换
前面的课程涵盖了布尔值、字符串和十几种不同的数值类型。如果你有不同的类型变量,你必须在使用前将它们的值转换为相同的类型。
考虑这一点
假设你正在杂货店,手里拿着配偶给你的购物清单。第一项是牛奶,但你应该买牛奶、杏仁奶还是大豆奶?应该是有机的、脱脂的、1%、2%、全脂的、蒸发奶还是炼乳?需要多少加仑?你是打电话给配偶询问,还是随便挑一样?
如果你一直打电话询问每个细节,你的配偶可能会感到厌烦。是冰山生菜还是罗马生菜?是红皮土豆还是黄皮土豆?哦,那 5 磅还是 10 磅?另一方面,如果你“自己思考”并带回来巧克力牛奶和薯条,那可能不会那么顺利。
如果你的配偶是程序员,而你在这个场景中是编译器,你认为 Go 的方法会是什么?
10.1. 类型不混合
变量的 类型 确定了适合它的行为。数字可以相加,字符串可以连接。要连接两个字符串,请使用加号运算符:
countdown := "Launch in T minus " + "10 seconds."
如果你尝试将数字与字符串连接,Go 编译器将报告错误:
countdown := "Launch in T minus " + 10 + " seconds." *1*
- 1 无效操作:字符串和整型不匹配
在其他语言中混合类型
当面对两种或更多不同类型时,一些编程语言会尽力猜测程序员的意图。JavaScript 和 PHP 都可以从字符串 "10" 中减去 1:
"10" - 1 *1*
- 在 JavaScript 和 PHP 中实现 1 到 9
Go 编译器会因类型不匹配错误而拒绝 "10" - 1。在 Go 中,你首先需要将 "10" 转换为整数。strconv 包中的 Atoi 函数可以进行转换,但如果 string 不包含有效的数字,它将返回错误。在你处理错误的时候,Go 版本已经是四行长,这并不十分方便。
话虽如此,如果 "10" 是用户输入或来自外部来源,JavaScript 和 PHP 版本也应该检查它是否是一个有效的数字。
在那些强制类型转换的语言中,对于没有记住大量隐式行为的任何人来说,代码的行为都难以预测。在 Java 和 JavaScript 中,加号运算符 (+) 都会将数字强制转换为字符串以进行连接,而 PHP 则将值强制转换为数字并执行数学运算:
"10" + 2 *1*
- 1 在 JavaScript 或 Java 中是“102”,在 PHP 中是 12
再次,Go 会报告类型不匹配错误。
当尝试使用整数和浮点数类型的混合进行计算时,也会发生类型不匹配的例子。像 365.2425 这样的实数使用浮点类型表示,而 Go 推断整数是整数:
age := 41 *1*
marsDays := 687 *1*
earthDays := 365.2425 *2*
fmt.Println("I am", age*earthDays/marsDays, "years old on Mars.") *3*
-
1 年龄和火星日数是整数。
-
2 earthDays 是浮点类型。
-
3 无效操作:类型不匹配
如果所有三个变量都是整数,计算将成功,但此时 earthDays 需要为 365 而不是更准确的 365.2425。或者,如果 age 和 marsDays 是浮点类型(分别为 41.0 和 687.0),计算也将成功。Go 不会假设你更喜欢哪种类型,但你可以在下一节中显式地在类型之间进行转换。
快速检查 10.1
Q1:
在 Go 中
"10" - 1是什么?
| |
快速检查 10.1 答案
1:
编译器错误:无效操作:“10” - 1(类型不匹配)
10.2. 数字类型转换
类型转换很简单。如果你需要将整数 age 转换为浮点类型进行计算,可以用新类型包裹变量:
age := 41
marsAge := float64(age)
不同类型的变量不能混合,但通过类型转换,以下列表中的计算可以完成。
列表 10.1. 火星年龄: mars-age.go
age := 41
marsAge := float64(age)
marsDays := 687.0
earthDays := 365.2425
marsAge = marsAge * earthDays / marsDays
fmt.Println("I am", marsAge, "years old on Mars.") *1*
- 1 打印我在火星上 21.797587336244543 年。
你还可以将浮点类型转换为整数,尽管小数点后的数字将被截断,而不进行四舍五入:
fmt.Println(int(earthDays)) *1*
- 1 打印 365
在无符号整数类型和有符号整数类型之间,以及在不同大小的类型之间进行类型转换是必要的。将类型转换为范围更大的类型,例如从 int8 到 int32,总是安全的。其他整数转换存在一些风险。uint32 可以包含 40 亿的值,但 int32 只支持略超过 20 亿的数字。同样,int 可能包含负数,但 uint 不能。
Go 要求在代码中显式声明类型转换的原因是,每次使用类型转换时,都要考虑可能的后果。
快速检查 10.2
1
将变量
red转换为无符号 8 位整数的代码是什么?2
比较
age > marsAge的结果是什么?
| |
QC 10.2 答案
1
类型转换将是
uint8(red)。2
类型不匹配
int和float64
10.3. 小心转换类型
1996 年,无人驾驶的 Arianne 5 火箭在发射后 40 秒偏离了飞行路径,解体并爆炸。据报道,原因是 float64 到 int16 的类型转换错误,其值超过了 32,767——这是 int16 可以持有的最大值。未处理的故障导致飞行控制系统失去了方向数据,使其偏离航线,解体并最终自毁。
我们没有看到 Arianne 5 的代码,也不是火箭科学家,但让我们看看 Go 如何处理相同的类型转换。如果值在范围内,如下面的列表所示,则没有问题。

列表 10.2. Ariane 类型转换:ariane.go
var bh float64 = 32767
var h = int16(bh) *1*
fmt.Println(h)
- 1 待办事项:添加火箭科学
如果 bh 的值为 32,768,这超出了 int16 的范围,结果就是我们已经在 Go 中的整数中看到的那样:它会回绕,成为 int16 可能的最小数字,-32768。
用于 Arianne 5 的 Ada 语言表现不同。float64 到 int16 的类型转换,其值超出范围,导致软件异常。根据报告,这个特定的计算仅在起飞前有意义,因此在这种情况下,Go 的方法可能更好,但通常最好避免错误的数据。
要检测将类型转换为 int16 是否会导致无效值,math 包提供了 min/max 常量:
if bh < math.MinInt16 || bh > math.MaxInt16 {
// handle out of range value
}
注意
这些 min/max 常量是无类型的,允许将浮点值 bh 与 MaxInt16 进行比较。第八部分 讨论了无类型常量更多内容。
| |
快速检查 10.3
Q1:
什么代码将确定变量
v是否在 8 位无符号整数的范围内?
| |
QC 10.3 答案
1:
v := 42 if v >= 0 && v <= math.MaxUint8 { v8 := uint8(v) fmt.Println("converted:", v8) *1* }
- 1 打印转换结果:42
10.4. 字符串转换
将 rune 或 byte 转换为 string 时,可以使用与数值转换相同的类型转换语法,如下面的列表所示。这会得到与在 第九部分 中引入的 %c 格式说明符相同的结果,用于显示 runes 和 bytes 作为字符。
列表 10.3. 将 rune 转换为 string:rune-convert.go
var pi rune = 960
var alpha rune = 940
var omega rune = 969
var bang byte = 33
fmt.Print(string(pi), string(alpha), string(omega), string(bang)) *1*
- 1 打印 πάω!
将数值代码点转换为字符串与任何整数类型的工作方式相同。毕竟,rune 和 byte 只是 int32 和 uint8 的别名。
将数字转换为 string 时,每个数字都必须转换为代码点,从 0 字符的 48 开始,到 9 字符的 57 结束。幸运的是,strconv(字符串转换)包中的 Itoa 函数会为你完成这项工作,如下面的列表所示。
列表 10.4. 整数到 ASCII:itoa.go
countdown := 10
str := "Launch in T minus " + strconv.Itoa(countdown) + " seconds."
fmt.Println(str) *1*
- 1 打印“发射倒计时 10 秒。”
注意
Itoa 是整数到 ASCII 的缩写。Unicode 是旧 ASCII 标准的超集。前 128 个代码点相同,包括数字(在此处使用)、英语字母和常用标点符号。
将数字转换为字符串的另一种方法是使用 Sprintf,它是 Printf 的一个堂兄弟,返回一个 string 而不是显示它:
countdown := 9
str := fmt.Sprintf("Launch in T minus %v seconds.", countdown)
fmt.Println(str) *1*
- 1 打印 T 减 9 秒准备发射。
要进行相反的转换,strconv 包提供了 Atoi 函数(ASCII 到整数)。由于字符串可能包含垃圾数据或太大的数字,Atoi 函数可能会返回一个错误:
countdown, err := strconv.Atoi("10")
if err != nil {
// oh no, something went wrong
}
fmt.Println(countdown) *1*
- 1 打印 10
err 的 nil 值表示没有发生错误,一切正常。课程 28 讲解了关于错误的风险话题。
快速检查 10.4
Q1:
列举两个可以将整数转换为字符串的函数。
| |
快速检查 10.4 答案
1:
Itoa和Sprintf都可以将整数转换为字符串。
| |
类型是静态的
在 Go 语言中,一旦声明了一个变量,它就有了一个类型,并且这个类型不能被改变。这被称为 静态类型,这使得编译器更容易优化,因此你的程序运行得更快。但是尝试使用不同类型的值来使用变量会导致 Go 编译器报告错误:
var countdown = 10
countdown = 0.5 *1*
countdown = fmt.Sprintf("%v seconds", countdown) *1*
- 1 错误:倒计时只能存储整数。
使用动态类型而不是静态类型的语言,如 JavaScript、Python 和 Ruby。在这些语言中,每个值都有一个关联的类型,变量可以持有任何类型的值。它们会允许 countdown 的类型在程序执行过程中改变。
Go 语言在类型不确定的情况下有一个逃生门。例如,Println 函数可以接受字符串和数值类型。课程 12 更详细地探讨了 Println 函数。
10.5. 将布尔值转换为
Print 函数族将布尔值 true 和 false 显示为文本。因此,下面的列表使用 Sprintf 函数将布尔变量 launch 转换为文本。如果你想要转换为数值或不同的文本,一个简单的 if 语句效果最好。
列表 10.5. 将布尔值转换为字符串:launch.go
launch := false
launchText := fmt.Sprintf("%v", launch)
fmt.Println("Ready for launch:", launchText) *1*
var yesNo string
if launch {
yesNo = "yes"
} else {
yesNo = "no"
}
fmt.Println("Ready for launch:", yesNo) *2*
-
1 打印准备发射:false
-
2 打印准备发射:no
逆转换需要更少的代码,因为你可以直接将条件的结果分配给变量,如下面的列表所示。
列表 10.6. 将字符串转换为布尔值:tobool.go
yesNo := "no"
launch := (yesNo == "yes")
fmt.Println("Ready for launch:", launch) *1*
- 1 打印准备发射:false
如果你尝试使用 string(false)、int(false) 或类似的方式转换布尔值,或者对于 bool(1) 或 bool("yes"),Go 编译器会报告错误。
注意
在没有专用 bool 类型的编程语言中,1 和 0 通常分别代表 true 和 false。Go 中的布尔值没有数值等价物。
| |
快速检查 10.5
Q1:
你会如何将布尔值转换为整数,其中 1 表示真,0 表示假?
| |
快速检查 10.5 答案
1:
使用一个简单的
if语句:launch := true var oneZero int if launch { oneZero = 1 } else { oneZero = 0 } fmt.Println("Ready for launch:", oneZero) *1*
- 1 打印准备发射:1
摘要
-
类型之间的转换是显式的,以避免歧义。
-
strconv包提供了将字符串转换为其他类型以及从其他类型转换为字符串的函数。
让我们看看你是否掌握了这个...
实验:input.go
编写一个程序,将字符串转换为布尔值:
-
字符串
true,“yes”或“1”是true。 -
字符串
false,“no”或“0”是false。 -
对于任何其他值,显示错误信息。
小贴士
switch语句在每个case中接受多个值,如第 3 课中所述。
第 11 课:总结:维吉尼亚密码
维吉尼亚密码(见en.wikipedia.org/wiki/Vigenere_cipher)是 16 世纪凯撒密码的一个变体。在这个挑战中,你需要编写一个程序来使用密钥解密文本。
在描述维吉尼亚密码之前,让我们重新审视一下你已经使用过的凯撒密码。在凯撒密码中,明文消息通过将每个字母向前移动三个位置来加密。解密结果消息的方向相反。
将每个英语字母分配一个数值,其中 A = 0,B = 1,一直到 Z = 25。考虑到这一点,移动 3 可以表示为字母 D(D = 3)。
要解密表 11.1 中的文本,从字母 L 开始,向 D 方向移动。因为 L = 11,D = 3,所以 11 - 3 的结果是 8,即字母 I。如果你需要解密字母 A,它应该绕回到 X,就像你在第 9 课中看到的那样。

表 11.1. 凯撒密码
| L | F | D | P | H | L | V | D | Z | L | F | R | Q | T | X | H | U | H | G |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| D | D | D | D | D | D | D | D | D | D | D | D | D | D | D | D | D | D | D |
凯撒密码和 ROT13 容易受到所谓的频率分析的影响。在英语中经常出现的字母,如 E,在加密文本中也会频繁出现。通过在加密文本中寻找模式,可以破解密码。
为了防止潜在的密码破解者,维吉尼亚密码根据重复的密钥而不是像 3 或 13 这样的常数来移动每个字母。密钥会一直重复到消息的末尾,就像表 11.2 中显示的 GOLANG 密钥一样。
表 11.2. 维吉尼亚密码
| C | S | O | I | T | E | U | I | W | U | I | Z | N | S | R | O | C | N | K | F | D |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| G | O | L | A | N | G | G | O | L | A | N | G | G | O | L | A | N | G | G | O | L |
现在你已经知道了维吉尼亚密码是什么,你可能注意到,使用密钥 D 的维吉尼亚密码等同于凯撒密码。同样,ROT13 的密钥是 N(N = 13)。需要更长的密钥才能发挥作用。
实验:decipher.go
编写一个程序来解密表 11.2 中显示的加密文本。为了简化,文本和密钥都是大写英文字母:
cipherText := "CSOITEUIWUIZNSROCNKFD"
keyword := "GOLANG"
-
strings.Repeat函数可能会很有用。试一试,但也尝试在不导入任何其他包(除了fmt用于打印解密消息)的情况下完成这个练习。 -
尝试使用循环中的
range关键字来完成这个练习,然后再不使用它。记住,range关键字将字符串拆分为 runes,而像keyword[0]这样的索引会产生一个 byte。提示
你只能对相同类型的值执行操作,但你可以将一种类型转换为另一种类型(
string、byte、rune)。 -
为了在字母表的边缘循环,凯撒密码练习使用了比较。通过使用模运算符(
%)而不使用任何if语句来解决这个练习。
提示
如果你还记得,模运算给出了两个数相除的余数。例如,27 % 26 的结果是 1,将数字保持在 0–25 的范围内。但是要注意负数,因为 -3 % 26 仍然是 -3。
完成练习后,查看附录中的我们的解决方案。它们如何比较?使用 Go Playground 的分享按钮,并在 Get Programming with Go 论坛上发布你的解决方案的链接。
使用维吉尼亚密码加密文本并不比解密文本更困难。只需将密钥字的字母添加到纯文本消息的字母中,而不是从中减去。
实验:cipher.go
要发送加密消息,编写一个程序,使用密钥字加密纯文本:
plainText := "your message goes here"
keyword := "GOLANG"
奖励:与其将你的纯文本消息全部大写且没有空格,不如使用 strings.Replace 和 strings.ToUpper 函数在加密之前删除空格并将字符串转换为大写。
一旦你加密了一条纯文本消息,通过使用相同的密钥字解密加密文本来检查你的工作。
使用密钥字 "GOLANG" 加密一条消息并将其发布到 forums.manning.com/forums/get-programming-with-go 的论坛上。
注意
声明:维吉尼亚密码只是好玩,但不要用它来发送重要的秘密。21 世纪有更安全的发送消息的方式。
单元 3. 构建块
编程是将一个大的不可能的任务分解成几个非常小的可能任务的过程。
Jazzwant
函数 是计算机程序的基本构建块。你可以调用像 Printf 这样的函数来格式化和显示值。最终出现在你屏幕上的像素是由 Go 和你的操作系统中的一层层函数传递的。
你也可以编写函数。函数帮助你组织代码,重用功能,并以更小的部分来思考问题。
不仅如此,通过学习如何在 Go 中声明函数和方法,你将能够探索标准库提供的丰富功能,这些功能在 golang.org/pkg 上有文档记录。
第 12 课. 函数
在阅读 第 12 课 之后,你将能够
-
识别函数声明的各个部分
-
编写可重用函数来构建更大的程序
这节课首先检查了之前课程中使用过的函数的标准库文档。
一旦你熟悉了声明函数的语法,你将为气象站程序编写函数。火星表面的罗弗环境监测站(REMS)收集气象数据。你将编写可能成为 REMS 程序一部分的函数,例如转换温度。

考虑这一点
做一个三明治。听起来很简单,但涉及许多步骤。洗生菜,切番茄,等等。也许你会进一步收获谷物,磨成面粉,然后烤面包,或者也许这些功能是由农民和面包师傅提供的。
使用每个步骤的函数来分解过程。然后,如果你需要为披萨准备番茄片,这个函数就可以重用。
你日常生活中还有什么可以分解成函数的?
12.1. 函数声明
Go 包文档在 golang.org/pkg 上列出了标准库中每个包声明的函数。有很多实用的函数——这本书可能无法全部涵盖。
要在自己的项目中使用这些函数,你通常需要阅读文档中的函数声明来了解如何调用该函数。在仔细审查了 Intn、Unix、Atoi、Contains 和 Println 的声明后,你将能够在探索其他函数时应用你新获得的知识,并在自己编写函数时使用这些知识。
你在 第 2 课 中使用了 Intn 函数来生成伪随机数。导航到 golang.org/pkg 和 math/rand 包以找到 Intn 函数。你还可以使用搜索框来查找 Intn。
rand 包中 Intn 的声明看起来是这样的:
func Intn(n int) int
作为复习,这里有一个使用 Intn 函数的例子:
num := rand.Intn(10)
在 图 12.1 中,声明部分被识别,以及调用 Intn 函数的语法。func 关键字让 Go 知道这是一个函数声明。然后是函数名 Intn,它以大写字母开头。
图 12.1. Intn 函数声明和调用 Intn 函数

在 Go 中,以大写字母开头的函数、变量和其他标识符是 导出的,并可供其他包使用。rand 包中也包含以小写字母开头的函数,但它们不能从 main 包中访问。
Intn 函数接受一个 参数,该参数被括号包围。参数是一个变量名后跟一个类型,与变量声明一致:
var n int
当调用 Intn 函数时,整数 10 被作为单个 参数 传递,同样被括号包围。该参数对应 Intn 所期望的单个参数。如果没有传递参数,或者参数不是 int 类型,Go 编译器会报告错误。
提示
参数和参数是数学术语,有细微的区别。一个函数接受参数并通过参数被调用,尽管有时人们可能互换使用这些术语。
Intn 函数返回单个结果,一个 int 类型的伪随机整数。结果被返回给调用者,用于初始化新声明的变量 num。
Intn 函数只接受一个参数,但函数可以通过逗号分隔的列表接受多个参数。如果你还记得 第 7 课,time 包中的 Unix 函数接受两个 int64 参数,分别对应自 1970 年 1 月 1 日起的秒数和纳秒数。文档中的声明看起来像这样:
func Unix(sec int64, nsec int64) Time
下面是使用两个参数调用 Unix 函数的示例,这两个参数分别对应 sec 和 nsec 参数:
future := time.Unix(12622780800, 0)
Unix 函数返回的结果是 Time 类型。得益于类型推断,调用 Unix 的代码不需要指定结果类型,这会使代码更加简洁。
注意
第 13 课 展示了如何声明新类型,如 time.Time 和 big.Int。
time 包声明并导出 Time 类型,该类型以大写字母开头,就像 Unix 函数一样。通过使用大写字母来指示导出内容,很明显 Time 类型可以从其他包中访问。
Unix 函数接受两个相同类型的参数,如下所述:
func Unix(sec int64, nsec int64) Time
但当在函数声明中列出参数时,只有当类型发生变化时才需要指定类型,因此它可以写成这样:
func Unix(sec, nsec int64) Time
这个快捷方式是可选的,但它在其他地方也被使用,例如 strings 包中的 Contains 函数,它接受两个 string 类型的参数:
func Contains(s, substr string) bool
提示
在 golang.org/pkg 的文档中,有时会有可以扩展的示例,你可以在 gobyexample.com 找到更多示例。如果你在学习 Go 的同时在自己的项目中前进,这些示例可能非常有价值。
许多编程语言都有接受多个参数的函数,但 Go 函数也可以返回多个结果。首次在 第 10 课 中展示的 Atoi 函数将字符串转换为数字,并返回两个结果,这里分别赋值给 countdown 和 err:
countdown, err := strconv.Atoi("10")
strconv 包的文档声明 Atoi 如下:
func Atoi(s string) (i int, err error)
两个结果被指定在括号中,就像参数列表一样,每个结果都有一个名称后跟一个类型。在函数声明中,你也可以列出没有名称的结果类型:
func Atoi(s string) (int, error)
注意
error 类型是一个用于错误的内置类型,第 28 课 将深入介绍。
你从本书开始就一直在使用的函数是 Println。它是一个非常独特的函数,因为它可以接受一个参数,也可以接受两个或更多参数。它还可以接受不同类型的参数,包括整数和字符串:
fmt.Println("Hello, playground")
fmt.Println(186, "seconds")
文档中的函数声明可能看起来有点奇怪,因为它使用了我们尚未介绍的功能:
func Println(a ...interface{}) (n int, err error)
Println 函数接受一个参数 a,但你已经看到传递多个参数是可能的。更具体地说,你可以向 Println 函数传递可变数量的参数,这由省略号 (...) 表示。有一个专门的术语来描述这一点:Println 被称为 可变参数 函数。参数 a 是传递给函数的参数集合。我们将在 第 18 课 中回到可变参数函数。
a 参数的类型是 interface{},被称为 空接口 类型。我们将在 第 24 课 中介绍接口,但现在你知道这种特殊类型是使 Println 能够接受 int、float64、string、time.Time 或任何其他类型而不会在 Go 编译器中报告错误的原因。
可变参数函数和空接口的组合,写作 ...interface{},意味着你可以传递任意数量和类型的参数给 Println。它能够很好地显示你向它投掷的任何内容。
注意
到目前为止,我们一直在忽略 Println 返回的两个结果,尽管忽略错误被认为是一种不良实践。良好的错误处理实践将在 第 28 课 中介绍。
| |
快速检查 12.1
1
你是用参数还是参数调用函数?
2
函数接受参数还是参数?
3
以大写字母开头的函数(
Contains)与以小写字母开头的函数(contains)有何不同?4
函数声明中的省略号(
...)表示什么?
| |
QC 12.1 答案
1
参数
2
参数
3
小写表示只能在其声明的包中使用的函数,而大写函数是导出的,可以在任何地方使用。
4
该函数是可变参数的。你可以传递给它任意数量的参数。
12.2. 编写函数
到目前为止,这本书中的代码都放在了 main 函数中。当处理更大的应用程序,如环境监测程序时,将问题分解成更小的部分变得很有价值。将代码组织成函数可以使代码更容易理解、重用和维护。
从传感器读数中读取的温度数据应以对地球人有意义的方式报告。传感器提供开尔文尺度的数据,其中 0° K 是绝对零度,即可能达到的最低温度。下一段代码中的函数将温度转换为摄氏度。一旦编写了转换函数,就可以在需要温度转换时重复使用它。
列表 12.1. 开氏到摄氏度:kelvin.go
package main
import "fmt"
// kelvinToCelsius converts °K to °C
func kelvinToCelsius(k float64) float64 { *1*
k -= 273.15
return k
}
func main() {
kelvin := 294.0
celsius := kelvinToCelsius(kelvin) *2*
fmt.Print(kelvin, "° K is ", celsius, "° C") *3*
}
-
1 声明一个接受一个参数并返回一个结果的函数
-
2 将 kelvin 作为第一个参数传递给函数
-
3 打印 294° K 等于 20.850000000000023° C
列表 12.1 中的 kelvinToCelsius 函数接受一个名为 k 的参数,类型为 float64。遵循 Go 的约定,kelvinToCelsius 的注释从函数名开始,然后是它所做的工作。
此函数返回一个 float64 类型的值。计算结果通过 return 关键字返回给调用者,然后在 main 函数中用此值初始化一个新的 celsius 变量。
注意,同一包内的函数调用不需要指定包名。
隔离可能是一件好事
列表 12.1 中的 kelvinToCelsius 函数与其他函数隔离。它的唯一输入是它接受的参数,唯一输出是它返回的结果。它不对外部状态进行任何修改。这样的函数是无副作用的,也是最容易理解、测试和重用的。
kelvinToCelsius 函数确实修改了变量 k,但 k 和 kelvin 是完全独立的变量,因此在函数内部为 k 赋新值不会影响 main 中的 kelvin 变量。这种行为被称为按值传递,因为 k 参数是用 kelvin 参数的值初始化的。按值传递有助于函数之间的边界,有助于隔离一个函数与另一个函数。
我们给变量取了不同的名字,但即使参数和参数有相同的名字,按值传递也适用。
此外,kelvinToCelsius 中的变量 k 与其他函数中名为 k 的任何变量完全独立,这要归功于变量作用域。作用域在第 4 课中有所介绍,但为了重申,函数声明中的参数和函数体内声明的变量具有函数作用域。在不同函数中声明的变量完全独立,即使它们具有相同的名称。
| |
快速检查 12.2
Q1:
将代码拆分为函数有哪些优点?
| |
QC 12.2 答案
1:
函数是可重用的,它们通过函数作用域为变量提供隔离,并为它们执行的操作提供名称,这使得代码更容易跟踪和理解。
摘要
-
函数通过名称、参数列表和结果列表进行声明。
-
大写函数名称和类型可供其他包使用。
-
每个参数或结果都是一个名称后跟一个类型,尽管当多个命名参数或结果具有相同类型时,类型可以省略。结果也可以无名称地列出为类型。
-
函数调用以声明函数的包的名称为前缀,除非函数是在调用它的同一包中声明的。
-
函数通过与它们接受的参数对应的参数进行调用。结果通过
return关键字返回给调用者。
让我们看看你是否掌握了这一点...
实验:functions.go
使用 play.golang.org 上的 Go Playground 输入列表 12.1 并声明额外的温度转换函数:
-
重新使用
kelvinToCelsius函数将 233° K 转换为摄氏度。 -
编写并使用一个
celsiusToFahrenheit温度转换函数。提示:转换为华氏度的公式是:(c * 9.0 / 5.0) + 32.0。 -
编写一个
kelvinToFahrenheit函数并验证它将 0° K 转换为大约 -459.67° F。
你在你的新函数中使用了 kelvinToCelsius 和 celsiusToFahrenheit,还是编写了一个具有新公式的独立函数?两种方法都是完全有效的。
第 13 课. 方法
在阅读第 13 课之后,你将能够
-
声明新类型
-
将函数重写为方法
方法类似于增强类型以提供额外行为的函数。在你可以声明一个方法之前,你需要声明一个新的类型。本课将 第 12 课 中的 kelvinToCelsius 函数转换为一个具有方法的类型。
起初,它可能看起来像方法只是函数已做事情的另一种语法,你会是对的。方法提供了另一种组织代码的方式,对于本课的例子来说,这是一种更令人满意的方式。后面的课程,特别是第 5 单元的课程,展示了方法如何与其他语言特性结合以带来新的功能。
考虑这一点
当你在计算器上输入数字与在打字机上输入数字时,预期的行为相当不同。Go 有内置的功能以独特的方式操作数字和文本(+),如第十部分所示。
如果你想表示一种新的类型并将行为与之捆绑在一起呢?float64 类型过于通用,不足以充分表示温度计,而狗的 bark() 声音与树的 bark 声音完全不同。函数有它的位置,但类型和方法提供了另一种组织代码和表示周围世界的有用方式。
在开始本节课之前,四处看看并考虑你周围的类型及其各自的行为。
13.1. 声明新类型
Go 声明了许多类型,其中许多在第 2 单元中介绍。有时这些类型不足以描述你想要持有的值的类型。
温度不是一个 float64,尽管它可能有其底层表示。温度是以摄氏度、华氏度或开尔文为单位的测量值。声明新类型不仅使代码更清晰,还可以帮助防止错误。
type 关键字用于声明一个带有名称和底层类型的新类型,如下所示。
列表 13.1. Celsius 类型:celsius.go
type celsius float64 *1*
var temperature celsius = 20
fmt.Println(temperature) *2*
-
1 底层类型是 float64。
-
2 打印 20
数值字面量 20,像所有数值字面量一样,是一个 未指定类型的 常量。它可以赋值给 int、float64 或任何其他数值类型的变量。celsius 类型是一个新的数值类型,其行为和表示与 float64 相同,因此前述列表中的赋值是有效的。
你也可以向温度添加值,并且通常可以像使用 float64 一样使用它,如下所示。
列表 13.2. celsius 类型的行为类似于 float64:celsius-addition.go
type celsius float64
const degrees = 20
var temperature celsius = degrees
temperature += 10
celsius 类型是一个独特的类型,而不是像第九部分中提到的那些类型别名。如果你尝试用 float64 使用它,你会得到一个类型不匹配的错误:
var warmUp float64 = 10
temperature += warmUp *1*
- 1 无效操作:类型不匹配
要添加 warmUp,必须首先将其转换为 celsius 类型。这个版本是有效的:
var warmUp float64 = 10
temperature += celsius(warmUp)
能够定义自己的类型可以非常有助于提高代码的可读性和可靠性。以下列表演示了 celsius 和 fahrenheit 类型不能意外地比较或组合。
列表 13.3. 类型不能混合
type celsius float64
type fahrenheit float64
var c celsius = 20
var f fahrenheit = 20
if c == f { *1*
}
c += f *1*
- 1 无效操作:celsius 和 fahrenheit 类型不匹配
快速检查 13.1
Q1:
声明新类型,如
celsius和fahrenheit,有哪些优点?
| |
QC 13.1 答案
1:
新类型可以更好地描述它包含的值,例如
celsius而不是float64。拥有独特的类型有助于避免愚蠢的错误,比如将华氏度值添加到摄氏度值上。
13.2. 带上你的类型
上一节声明了新的 celsius 和 fahrenheit 类型,将温度域引入代码中,同时弱化了底层存储表示。温度是表示为 float64 还是 float32 对变量包含的值几乎没有什么影响,而像 celsius、fahrenheit 和 kelvin 这样的类型则传达了它们的目的。
一旦声明了一个类型,您就可以在任何可以使用预定义 Go 类型(int、float64、string 等等)的地方使用它,包括函数参数和结果,如下面的列表所示。
列表 13.4. 带有自定义类型的函数:temperature-types.go
package main
import "fmt"
type celsius float64
type kelvin float64
// kelvinToCelsius converts °K to °C
func kelvinToCelsius(k kelvin) celsius {
return celsius(k - 273.15) *1*
}
func main() {
var k kelvin = 294.0 *2*
c := kelvinToCelsius(k)
fmt.Print(k, "° K is ", c, "° C") *3*
}
-
1 需要进行类型转换。
-
2 参数必须是 kelvin 类型。
-
3 打印 294° K 是 20.850000000000023° C
kelvinToCelsius 函数只接受 kelvin 类型的参数,这可以防止愚蠢的错误。它不会接受错误类型的参数,例如 fahrenheit、kilometers 或甚至 float64。Go 语言是一种务实的语言,因此仍然可以传递字面值或无类型的常量。与其写 kelvinToCelsius(kelvin(294)),您可以直接写 kelvinToCelsius(294)。
从 kelvinToCelsius 返回的结果是 celsius 类型,而不是 kelvin 类型,因此必须在返回之前将类型转换为 celsius。
快速检查 13.2
Q1:
编写一个
celsiusToKelvin函数,该函数使用 列表 13.4 中定义的celsius和kelvin类型。使用它将 127° C(太阳照射的月球表面温度)转换为开尔文度。
| |
QC 13.2 答案
1:
func celsiusToKelvin(c celsius) kelvin { return kelvin(c + 273.15) } func main() { var c celsius = 127.0 k := celsiusToKelvin(c) fmt.Print(c, "° C is ", k, "° K") *1* }
- 1 打印 127° C 是 400.15° K
13.3. 使用方法向类型添加行为
纵然这是疯狂,但其中自有方法。
莎士比亚,《哈姆雷特》*
几十年来,传统的面向对象语言都教导方法属于类。Go 语言则不同。实际上,Go 语言中没有类,甚至没有对象,但 Go 语言还是有方法的。这听起来可能有些奇怪,甚至有些疯狂,但 Go 语言中的方法实际上比过去任何语言都更加灵活。
类似于 kelvinToCelsius、celsiusToFahrenheit、fahrenheitToCelsius 和 celsiusToKelvin 的函数可以完成工作,但我们可以做得更好。在它们的位置声明几个方法将使温度转换代码更加简洁。

您可以将方法与同一包中声明的任何类型关联,但不能与预定义类型(int、float64 等等)关联。您已经看到了如何声明一个类型:
type kelvin float64
kelvin 类型与其底层类型 float64 具有相同的行为。您可以对 kelvin 值进行加法、乘法和其他操作,就像浮点数一样。声明将 kelvin 转换为 celsius 的方法就像声明一个函数一样简单。它们都以 func 关键字开始,函数体与方法体相同:
func kelvinToCelsius(k kelvin) celsius { *1*
return celsius(k - 273.15)
}
func (k kelvin) celsius() celsius { *2*
return celsius(k - 273.15)
}
-
1 kelvinToCelsius 函数
-
2 关于 kelvin 类型的摄氏度方法
celsius方法不接受任何参数,但它有一个类似参数的东西在名称之前。它被称为接收器,如图图 13.1 所示。方法和函数都可以接受多个参数,但方法必须恰好有一个接收器。在celsius方法体内,接收器表现得就像任何其他参数一样。
图 13.1. 方法声明

使用方法的语法与调用函数不同:
var k kelvin = 294.0
var c celsius
c = kelvinToCelsius(k) *1*
c = k.celsius() *2*
-
1 调用
kelvinToCelsius函数 -
2 调用
celsius方法
方法使用点符号调用,这看起来就像在另一个包中调用函数。但在这个情况下,正确的类型的变量后面跟着一个点和方法名称。
现在温度转换是kelvin类型的celsius方法,名称如kelvinToCelsius是多余的。一个包只能有一个具有给定名称的函数,并且它不能与类型名称相同,所以返回celsius类型的celsius函数是不可能的。但每个温度类型都可以提供一个celsius方法,所以例如,fahrenheit类型可以增强如下:
type fahrenheit float64
// celsius converts °F to °C
func (f fahrenheit) celsius() celsius {
return celsius((f - 32.0) * 5.0 / 9.0)
}
这创造了一种很好的对称性,每种温度类型都可以有一个celsius方法来转换为摄氏度。
快速检查 13.3
Q1:
识别这个方法声明中的接收器:
func (f fahrenheit) celsius() celsius
| |
QC 13.3 答案
1:
接收器是
f类型的fahrenheit。
概述
-
声明自己的类型可以帮助提高可读性和可靠性。
-
方法就像通过在方法名称之前指定的接收器与类型相关联的函数。方法和函数一样可以接受多个参数并返回多个结果,但它们必须始终恰好有一个接收器。在方法体内,接收器表现得就像任何其他参数一样。
-
方法调用的语法使用点符号,后面跟着适当的类型的变量,然后是点、方法名称和任何参数。
让我们看看你是否掌握了这个...
实验:methods.go
编写一个程序,包含celsius、fahrenheit和kelvin类型和方法,以将任何温度类型转换为任何其他温度类型。
第 14 课. 一等函数
在阅读了第 14 课之后,你将能够
-
将函数赋值给变量
-
将函数传递给函数
-
编写创建函数的函数
在 Go 中,你可以将函数赋值给变量,将函数传递给函数,甚至编写返回函数的函数。函数是一等的——它们在整数、字符串和其他类型工作的所有地方都能工作。
本课探讨了作为理论上的 Rover 环境监测站(REMS)程序一部分的一等函数的潜在用途,该程序从(假的)温度传感器读取数据。
考虑这一点
一个玉米卷食谱需要莎莎酱。你可以翻到食谱书的第 93 页制作自制的莎莎酱,或者打开商店里的一罐莎莎酱。
首类函数就像需要莎莎酱的玉米卷。作为代码,makeTacos 函数需要调用莎莎酱的函数,无论是 makeSalsa 还是 openSalsa。莎莎酱函数也可以独立使用,但没有莎莎酱的玉米卷就不完整。
除了食谱和温度传感器之外,还有哪些函数可以通过函数进行自定义的例子?
14.1. 将函数分配给变量
天气站传感器提供 150–300° K 的空气温度读数。一旦你有了数据,你可以使用函数将开尔文转换为其他温度单位,但除非你的计算机(或树莓派)上连接了传感器,否则获取数据会有点问题。
目前你可以使用一个返回伪随机数的假传感器,但随后你需要一种方法来交替使用 realSensor 或 fakeSensor。下面的代码示例正是如此。通过这种方式设计程序,还可以插入不同的真实传感器,例如,用于监测地面和空气温度。
代码列表 14.1. 可互换的传感器函数:sensor.go
package main
import (
"fmt"
"math/rand"
)
type kelvin float64
func fakeSensor() kelvin {
return kelvin(rand.Intn(151) + 150)
}
func realSensor() kelvin {
return 0 *1*
}
func main() {
sensor := fakeSensor *2*
fmt.Println(sensor())
sensor = realSensor
fmt.Println(sensor())
}
-
1 待办:实现一个真实传感器
-
2 将函数分配给变量
在之前的代码示例中,sensor 变量被分配给了 fakeSensor 函数本身,而不是函数调用的结果。函数和方法调用总是带有括号,例如 fakeSensor(),但这里并非如此。
现在调用 sensor() 将会根据 sensor 被分配到哪个函数而实际调用 realSensor 或 fakeSensor。
sensor 变量是函数类型,其中该函数不接受任何参数并返回一个 kelvin 结果。当不依赖类型推断时,sensor 变量可以这样声明:
var sensor func() kelvin
注意
你可以在代码列表 14.1 中将 sensor 重新分配给 realSensor,因为它与 fakeSensor 的函数签名相匹配。这两个函数具有相同数量和类型的参数以及返回值。
| |
快速检查 14.1
1
你如何区分将函数分配给变量与将函数调用的结果分配给变量?
2
如果存在一个返回摄氏温度的
groundSensor函数,它能否被分配到代码列表 14.1 中的sensor?
| |
QC 14.1 答案
1
函数和方法调用总是带有括号(例如,
fn()),而函数本身可以通过指定不带括号的函数名来分配。2
不。参数和返回值必须具有相同的类型才能重新分配传感器变量。Go 编译器将报告错误:不能在赋值中使用 groundSensor。
14.2. 将函数传递给其他函数
变量可以引用函数,并且变量可以被传递给函数,这意味着 Go 允许你将函数传递给其他函数。
为了每秒记录温度数据,列表 14.2 声明了一个新的measureTemperature函数,它接受一个传感器函数作为参数。它定期调用传感器函数,无论是fakeSensor还是realSensor。
能够传递函数的能力为你提供了一种强大的方式来分割你的代码。如果没有一等函数,你可能会得到包含几乎相同代码的measureRealTemperature和measureFakeTemperature函数。
列表 14.2. 函数作为参数:function-parameter.go
package main
import (
"fmt"
"math/rand"
"time"
)
type kelvin float64
func measureTemperature(samples int, sensor func() kelvin) { *1*
for i := 0; i < samples; i++ {
k := sensor()
fmt.Printf("%v° K\n", k)
time.Sleep(time.Second)
}
}
func fakeSensor() kelvin {
return kelvin(rand.Intn(151) + 150)
}
func main() {
measureTemperature(3, fakeSensor) *2*
}
-
1 measureTemperature 接受一个函数作为第二个参数。
-
2 将函数的名称传递给另一个函数
measureTemperature函数接受两个参数,第二个参数的类型为func() kelvin。这种声明看起来像同一类型的变量声明:
var sensor func() kelvin
main函数能够将函数的名称传递给measureTemperature。
快速检查 14.2
Q1:
将函数传递给其他函数的能力有什么好处?
| |
QC 14.2 答案
1:
一等函数提供了另一种分割和重用代码的方法。
14.3. 声明函数类型
可以声明一个新的函数类型来压缩和澄清引用它的代码。你使用了kelvin类型来传达温度的单位,而不是底层表示。同样也可以为正在传递的函数做同样的事情:
type sensor func() kelvin
而不是接受无参数并返回kelvin值的函数,代码是关于sensor函数的。这种类型可以用来压缩其他代码,因此声明
func measureTemperature(samples int, s func() kelvin)
现在可以写成这样:
func measureTemperature(samples int, s sensor)
在这个例子中,可能看起来没有改进,因为你现在需要知道当查看这一行代码时sensor是什么。但如果sensor在多个地方使用,或者如果函数类型有多个参数,使用类型将显著减少混乱。
快速检查 14.3
Q1:
将以下函数签名重写为使用函数类型:
func drawTable(rows int, getRow func(row int) (string, string))
| |
QC 14.3 答案
1:
type getRowFn func(row int) (string, string) func drawTable(rows int, getRow getRowFn)
14.4. 闭包和匿名函数
一个匿名函数,在 Go 中也称为函数字面量,是一个没有名称的函数。与常规函数不同,函数字面量是闭包,因为它们保留了周围作用域中变量的引用。

你可以将匿名函数赋值给变量,然后像使用任何其他函数一样使用该变量,如下面的列表所示。
列表 14.3. 匿名函数:masquerade.go
package main
import "fmt"
var f = func() { *1*
fmt.Println("Dress up for the masquerade.")
}
func main() {
f() *2*
}
-
1 将匿名函数赋值给变量
-
2 打印为化装舞会打扮。
你声明的变量可以在包的作用域内,或者在函数内部,如下一个列表所示。
列表 14.4. 匿名函数:funcvar.go
package main
import "fmt"
func main() {
f := func(message string) { *1*
fmt.Println(message)
}
f("Go to the party.") *2*
}
-
1 将匿名函数赋值给变量
-
2 打印去参加派对。
你甚至可以一步声明和调用匿名函数,如下面的列表所示。
列表 14.5. 匿名函数:anonymous.go
package main
import "fmt"
func main() {
func() { *1*
fmt.Println("Functions anonymous")
}() *2*
}
-
1 声明一个匿名函数
-
2 调用函数
匿名函数在需要即时创建函数时非常有用。这种情况之一是从另一个函数返回一个函数。虽然函数可以返回现有的命名函数,但声明和返回一个新的匿名函数更有用。
在列表 14.6 中,calibrate函数调整了空气温度读数中的错误。使用一等函数,calibrate接受一个假传感器或真实传感器作为参数,并返回一个替换函数。每次调用新的sensor函数时,都会调用原始函数,并通过偏移量调整读数。
列表 14.6. 传感器校准:calibrate.go
package main
import "fmt"
type kelvin float64
// sensor function type
type sensor func() kelvin
func realSensor() kelvin {
return 0 *1*
}
func calibrate(s sensor, offset kelvin) sensor {
return func() kelvin { *2*
return s() + offset
}
}
func main() {
sensor := calibrate(realSensor, 5)
fmt.Println(sensor()) *3*
}
-
1 待办:实现真实传感器
-
2 声明并返回一个匿名函数
-
3 打印 5
前面的列表中的匿名函数使用了闭包。它引用了calibrate函数接受的参数s和offset变量。即使在calibrate函数返回之后,闭包捕获的变量仍然存在,因此对sensor的调用仍然可以访问这些变量。匿名函数封装了作用域内的变量,这就是为什么称之为闭包的原因。
由于闭包保留了对周围变量的引用而不是它们值的副本,因此对这些变量的更改会反映在匿名函数的调用中:
var k kelvin = 294.0
sensor := func() kelvin {
return k
}
fmt.Println(sensor()) *1*
k++
fmt.Println(sensor()) *2*
-
1 打印 294
-
2 打印 295
请记住这一点,尤其是在使用for循环中的闭包时。
快速检查 14.4
1
Go 中匿名函数的另一个名称是什么?
2
闭包提供了哪些常规函数没有的功能?
| |
QC 14.4 答案
1
在 Go 中,匿名函数也称为函数字面量。
2
闭包保留了对周围作用域中变量的引用。
摘要
-
当函数被视为一等公民时,它们为拆分和重用代码开辟了新的可能性。
-
要即时创建函数,请使用具有闭包的匿名函数。
看看你是否明白了...
实验:calibrate.go
将列表 14.6 输入 Go 游乐场以查看其效果:
-
而不是将 5 作为参数传递给
calibrate,声明并传递一个变量。修改该变量,你会注意到对sensor()的调用仍然结果是 5。这是因为offset参数是参数的副本(按值传递)。 -
使用来自列表 14.2 的
fakeSensor函数与calibrate一起创建一个新的sensor函数。多次调用新的sensor函数,并注意每次仍然调用原始的fakeSensor,导致随机值。
第 15 课:总结:温度表
编写一个显示温度转换表的程序。这些表格应使用等号(=)和管道符(|)来绘制线条,并包含一个标题部分:
=======================
| °C | °F |
=======================
| -40.0 | -40.0 |
| ... | ... |
=======================
程序应绘制两张表格。第一张表格有两列,第一列是摄氏度(°C),第二列是华氏度(°F)。从-40°C 通过 100°C 以 5°C 的步长循环,使用第 13 课中的温度转换方法来填充两列。
完成一张表格后,实现第二张表格,并将列反转,将华氏度(°F)转换为摄氏度(°C)。
绘制线条和填充值是您可以用于任何需要显示在两列表格中的数据的代码。使用函数将表格绘制代码与计算每行温度的代码分开。

实现一个drawTable函数,该函数接受一个一等函数作为参数,并调用它来获取每行绘制的数据。向drawTable传递不同的函数应导致显示不同的数据。
单元 4. 集合
集合只是事物的组合。你可能有一个音乐收藏。每张专辑有一组歌曲,每首歌曲有一组音符。如果你想要构建一个音乐播放器,你会很高兴地知道编程语言也有集合。
在 Go 语言中,你可以使用 单元 2 中介绍的原始类型来组合更有趣的 复合类型。这些复合类型允许你将值组合在一起,提供新的收集和访问数据的方式。
第 16 课. 星光熠熠
在阅读 第 16 课 之后,你将能够
-
声明和初始化数组
-
分配和访问数组的元素
-
遍历数组
数组是有序元素集合,具有固定长度。本课使用数组来存储我们太阳系中行星和矮行星的名称,但你也可以收集任何你喜欢的。
考虑这一点
你有收藏品吗?或者你过去有吗?可能是邮票、硬币、贴纸、书籍、鞋子、奖杯、电影或其他东西?
数组用于收集许多相同类型的事物。你可以用数组表示哪些集合?
16.1. 声明数组和访问它们的元素
以下 planets 数组恰好包含八个元素:
var planets [8]string
数组的每个元素都具有相同的类型。在这种情况下,planets 是一个字符串数组。
可以通过使用方括号 [] 并以 0 开始的索引来访问数组的单个元素,如图 16.1 所示,并在列表 16.1 中展示。
列表 16.1. 行星数组:array.go
var planets [8]string
planets[0] = "Mercury" *1*
planets[1] = "Venus"
planets[2] = "Earth"
earth := planets[2] *2*
fmt.Println(earth) *3*
-
1 分配索引 0 处的行星
-
2 获取索引 2 处的行星
-
3 打印地球
图 16.1. 索引从 0 到 7 的行星

尽管只有三个行星被分配,但 planets 数组有八个元素。数组的长度可以使用内置的 len 函数确定。其他元素包含它们类型的零值,即空字符串:
fmt.Println(len(planets)) *1*
fmt.Println(planets[3] == "") *2*
-
1 打印 8
-
2 打印 true
注意
Go 语言有一些内置函数不需要 import 语句。len 函数可以确定多种类型的长度。在这种情况下,它返回数组的尺寸。
快速检查 16.1
1
你如何访问
planets数组的第一个元素?2
新整数数组的元素默认值是什么?
QC 16.1 答案
1
planets[0]2
数组的元素最初是数组类型的零值,这意味着整数数组是
0。
16.2. 不要越界
八元素数组具有从 0 到 7 的索引。当 Go 编译器检测到访问此范围之外的元素时,将报告错误:
var planets [8]string
planets[8] = "Pluto" *1*
pluto := planets[8] *1*
- 1 无效的数组索引 8(超出 8 元素数组的范围)
如果 Go 编译器无法检测到错误,程序在运行时可能会发生 panic:
var planets [8]string
i := 8
planets[i] = "Pluto" *1*
pluto := planets[i] *1*
- 1 Panic: 运行时错误:索引越界
如果修改不属于 planets 数组的内存,程序将发生 panic 并崩溃,这仍然比导致未指定行为(如 C 编程语言中的情况)要好。
快速检查 16.2
Q1:
planets[11]会在编译时引起错误还是在运行时发生 panic?
QC 16.2 答案
1:
Go 编译器将检测无效的数组索引。
16.3. 使用组合字面量初始化数组
组合字面量 是一种简洁的语法,可以初始化任何复合类型,使用你想要的值。而不是声明一个数组并逐个分配元素,Go 的组合字面量语法将声明和初始化一个数组,如下面的列表所示。
列表 16.2. 小行星数组:dwarfs.go
dwarfs := [5]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
大括号 {} 包含五个以逗号分隔的字符串,用于填充新数组的元素。
对于较大的数组,将组合字面量拆分到多行可以提高可读性。并且作为便利,你可以要求 Go 编译器通过指定省略号 ... 而不是数字来计算组合字面量中的元素数量。以下列表中的 planets 数组仍然具有固定长度。
列表 16.3. 行星的全数组:composite.go
planets := [...]string{ *1*
"Mercury",
"Venus",
"Earth",
"Mars",
"Jupiter",
"Saturn",
"Uranus",
"Neptune", *2*
}
-
1 Go 编译器会计算元素数量。
-
2 需要尾随逗号。
快速检查 16.3
Q1:
列表 16.3 中有多少颗行星?使用内置的 len 函数来找出答案。
QC 16.3 答案
1:
planets数组有八个元素(8)。
16.4. 遍历数组
遍历数组的每个元素类似于在 第 9 课 中遍历字符串的每个字符,如下面的列表所示。
列表 16.4. 遍历数组:array-loop.go
dwarfs := [5]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
for i := 0; i < len(dwarfs); i++ {
dwarf := dwarfs[i]
fmt.Println(i, dwarf)
}
range 关键字通过更少的代码和更少的错误机会为数组的每个元素提供一个索引和值,如下面的列表所示。
列表 16.5. 使用 range 遍历数组:array-range.go
dwarfs := [5]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
for i, dwarf := range dwarfs {
fmt.Println(i, dwarf)
}
列表 16.4 和 16.5 产生相同的输出:
0 Ceres
1 Pluto
2 Haumea
3 Makemake
4 Eris
注意
记住,如果你不需要 range 提供的索引变量,可以使用空白标识符(下划线)。
快速检查 16.4
1
使用
range关键字遍历数组可以避免哪些错误?2
在什么情况下使用传统的
for循环而不是range更合适?
QC 16.4 答案
1
使用
range关键字,循环更简单,避免了越界等错误(例如,i <= len(dwarfs))。2
如果你需要自定义操作,比如反向迭代或访问每个第二个元素。
16.5. 数组是复制的
将数组分配给新变量或将它传递给函数会完全复制其内容,如下面的列表所示。
列表 16.6. 数组是值:array-value.go
planets := [...]string{
"Mercury",
"Venus",
"Earth",
"Mars",
"Jupiter",
"Saturn",
"Uranus",
"Neptune",
}
planetsMarkII := planets *1*
planets[2] = "whoops" *2*
fmt.Println(planets) *3*
fmt.Println(planetsMarkII) *4*
-
1 复制行星数组
-
2 为星际 bypass 让路
-
3 打印[水星 金星 哗啦 火星 木星 土星 天王星 海王星]
-
4 打印[水星 金星 地球 火星 木星 土星 天王星 海王星]
提示
如果你逃离地球的毁灭,你将需要在你的电脑上安装 Go。请参阅golang.org上的说明。
数组是值,函数通过值传递,这意味着以下列表中的terraform函数完全无效。
列表 16.7. 数组按值传递:terraform.go
package main
import "fmt"
// terraform accomplishes nothing
func terraform(planets [8]string) {
for i := range planets {
planets[i] = "New " + planets[i]
}
}
func main() {
planets := [...]string{
"Mercury",
"Venus",
"Earth",
"Mars",
"Jupiter",
"Saturn",
"Uranus",
"Neptune",
}
terraform(planets)
fmt.Println(planets) *1*
}
- 1 打印[水星 金星 地球 火星 木星 土星 天王星 海王星]
terraform函数正在操作planets数组的副本,因此修改不会影响main函数中的planets。
此外,重要的是要认识到数组的长度是其类型的一部分。类型[8]string和类型[5]string都是字符串集合,但它们是两种不同的类型。当尝试传递不同长度的数组时,Go 编译器将报告错误:
dwarfs := [5]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
terraform(dwarfs) *1*
- 1 不能将矮人(类型[5]string)用作 terraform 的参数类型[8]string
由于这些原因,数组不像下一课中将要介绍的slices那样经常用作函数参数。
快速检查 16.5
1
地球如何在列表 16.6 的
planetsMarkII中幸存?2
如何修改列表 16.7,以便在
main中的行星数组发生变化?
QC 16.5 答案
1
planetsMarkII变量接收了planets数组的副本,因此对任一数组的修改都是相互独立的。2
terraform函数可以返回修改后的[8]string数组,这样main就可以将行星重新分配给新值。切片和指针的第 17 课和第 26 课提供了其他替代方案。
16.6. 数组数组
你已经见过字符串数组,但你也可以有整数数组、浮点数数组和甚至数组数组。以下列表中的 8×8 棋盘是一个字符串数组数组。
列表 16.8. 棋盘:chess.go
var board [8][8]string *1*
board[0][0] = "r"
board[0][7] = "r" *2*
for column := range board[1] {
board[1][column] = "p"
}
fmt.Print(board)
-
1 八个八字符串数组的数组
-
2 在[row][column]坐标放置一枚车
快速检查 16.6
Q1:
考虑数独游戏。一个 9×9 整数网格的声明看起来会是什么样子?
QC 16.6 答案
1:
var grid [9][9]int
摘要
-
数组是有序元素集合,具有固定长度。
-
复合字面量提供了一种方便的方式来初始化数组。
-
range关键字可以遍历数组。 -
当访问数组的元素时,你必须保持在它的边界内。
-
当分配或传递给函数时,数组会被复制。
让我们看看你是否掌握了这个...
实验:chess.go
-
将 列表 16.8 扩展到使用字符
kqrbnp表示顶部的黑色棋子,以及使用大写KQRBNP表示底部的白色棋子,以显示所有棋子在起始位置。 -
编写一个函数,以优雅的方式显示棋盘。
-
用
[8][8]rune来表示棋盘,而不是字符串。回想一下,rune文字用单引号包围,可以用%c格式说明符打印。
第 17 课. 切片:数组的窗口
在阅读 第 17 课 之后,你将能够
-
使用切片通过窗口查看太阳系
-
使用标准库对切片进行排序。
我们太阳系中的行星被分为地行星、气态巨行星和冰态巨行星,如图 17.1 所示。你可以通过用 planets[0:4] 切片 planets 数组的头四个元素来专注于地行星。切片不会改变 planets 数组。它只是创建了一个窗口或视图。这个视图是一个名为 切片 的类型。
图 17.1. 切片太阳系

考虑这一点
如果你有收藏,它是按照某种方式组织的吗?例如,图书馆书架上的书可能是按照作者姓氏排序的。这种安排让你可以专注于他们写的其他书籍。
你可以用切片以同样的方式聚焦于集合的一部分。
17.1. 切片数组
切片用 半开区间 表示。例如,在下面的列表中,planets[0:4] 从索引 0 的行星开始,并继续到但不包括索引 4 的行星。
列表 17.1. 切片数组:slicing.go
planets := [...]string{
"Mercury",
"Venus",
"Earth",
"Mars",
"Jupiter",
"Saturn",
"Uranus",
"Neptune",
}
terrestrial := planets[0:4]
gasGiants := planets[4:6]
iceGiants := planets[6:8]
fmt.Println(terrestrial, gasGiants, iceGiants) *1*
- 1 打印 [水星 金星 地球 火星] [木星 土星] [天王星 海王星]
虽然 terrestrial、gasGiants 和 iceGiants 都是切片,但你仍然可以像数组一样索引切片:
fmt.Println(gasGiants[0]) *1*
- 1 打印木星
你也可以先切片数组,然后再切片结果切片:
giants := planets[4:8]
gas := giants[0:2]
ice := giants[2:4]
fmt.Println(giants, gas, ice) *1*
- 1 打印 [木星 土星 天王星 海王星] [木星 土星] [天王星 海王星]
terrestrial、gasGiants、iceGiants、giants、gas 和 ice 切片都是同一 planets 数组的视图。给切片的元素赋新值会修改底层 planets 数组。这种变化将通过其他切片可见:
iceGiantsMarkII := iceGiants *1*
iceGiants[1] = "Poseidon"
fmt.Println(planets) *2*
fmt.Println(iceGiants, iceGiantsMarkII, ice) *3*
-
1 复制 iceGiants 切片(行星数组的视图)
-
2 打印 [水星 金星 地球 火星 木星 土星 天王星 波塞冬]
-
3 打印 [天王星 波塞冬] [天王星 波塞冬] [天王星 波塞冬]
快速检查 17.1
1
切片数组会产生什么?
2
当使用
planets[4:6]切片时,结果中有多少个元素?
QC 17.1 答案
1
一个切片。
2
二。
17.1.1. 切片的默认索引
当切片数组时,省略第一个索引默认为数组的开始。省略最后一个索引默认为数组的长度。这使得 列表 17.1 中的切片可以写成以下列表所示。
列表 17.2. 默认索引:slicing-default.go
terrestrial := planets[:4]
gasGiants := planets[4:6]
iceGiants := planets[6:]
注意
切片索引不能为负。
你可能可以猜到省略两个索引会发生什么。allPlanets 变量是一个包含八个行星的切片:
allPlanets := planets[:]
切片字符串
数组的切片语法也适用于字符串:
neptune := "Neptune"
tune := neptune[3:]
fmt.Println(tune) *1*
- 1 打印调音
切片字符串的结果是另一个字符串。然而,将新值赋给 neptune 不会改变 tune 的值,反之亦然:
neptune = "Poseidon"
fmt.Println(tune) *1*
- 1 打印调音
注意,索引表示字节数,而不是 runes:
question := "¿Cómo estás?"
fmt.Println(question[:6]) *1*
- 1 打印 ¿Cóm
| |
快速检查 17.2
Q1:
如果地球和火星是唯一被殖民的行星,你将如何从
terrestrial中推导出colonized切片?
| |
QC 17.2 答案
1:
colonized := terrestrial[2:]
17.2. 切片复合字面量
Go 语言中的许多函数操作的是切片而不是数组。如果你需要一个显示底层数组中每个元素的切片,一个选项是声明一个数组,然后使用 [:] 来切片,如下所示:
dwarfArray := [...]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
dwarfSlice := dwarfArray[:]
切片数组是创建切片的一种方法,但你也可以直接声明一个切片。字符串切片的类型是 []string,括号中没有值。这与数组声明不同,数组声明总是指定括号中的固定长度或省略号。
在以下列表中,dwarfs 是使用熟悉的复合字面量语法初始化的切片。
列表 17.3. 从切片开始:dwarf-slice.go
dwarfs := []string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
仍然有一个底层数组。在幕后,Go 声明了一个五个元素的数组,然后创建了一个可以查看其所有元素的切片。
快速检查 17.3
Q1:
使用
%T格式说明符比较dwarfArray和dwarfs切片的类型。
| |
QC 17.3 答案
1:
fmt.Printf("array %T\n", dwarfArray) *1* fmt.Printf("slice %T\n", dwarfs) *2*
- 1 打印数组 [5]string
- 2 打印切片 []string
17.3. 切片的力量
如果有办法折叠时空结构,将世界聚集在一起以实现瞬间旅行,会怎么样?使用 Go 标准库和一些巧妙的方法,列表 17.4 中的 hyperspace 函数修改了一个 worlds 切片,移除了它们之间的(白色)空间。

列表 17.4. 将世界聚集在一起:hyperspace.go
package main
import (
"fmt"
"strings"
)
// hyperspace removes the space surrounding worlds
func hyperspace(worlds []string) { *1*
for i := range worlds {
worlds[i] = strings.TrimSpace(worlds[i])
}
}
func main() {
planets := []string{" Venus ", "Earth ", " Mars"} *2*
hyperspace(planets)
fmt.Println(strings.Join(planets, "")) *3*
}
-
1 这个论点是切片,而不是数组。
-
2 空间包围的行星
-
3 打印金星、地球和火星
worlds 和 planets 都是切片,尽管 worlds 是一个副本,但它们都指向同一个底层数组。
如果 hyperspace 改变了 worlds 切片指向的位置、开始或结束,这些更改对 planets 切片没有影响。但是 hyperspace 能够访问 worlds 指向的底层数组并更改其元素。这些更改对数组的其他(视图)切片是可见的。
切片在与其他语言的数组相比还有其他方面更灵活。切片有一个长度,但与数组不同,长度不是类型的一部分。你可以将任何大小的切片传递给 hyperspace 函数:
dwarfs := []string{" Ceres ", " Pluto"}
hyperspace(dwarfs)
数组很少直接使用。Gophers 更喜欢切片,因为它们具有多功能性,尤其是在向函数传递参数时。
快速检查 17.4
Q1:
在 golang.org/pkg 的 Go 文档中查找
TrimSpace和Join。它们提供了哪些功能?
| |
QC 17.4 答案
1:
1a
TrimSpace返回一个移除了前导和尾随空白字符的切片。1b
Join使用分隔符连接元素切片。
17.4. 带有方法的切片
在 Go 中,你可以定义一个具有底层切片或数组的类型。一旦你有了类型,你就可以向它附加方法。Go 声明类型方法的能力比其他语言中的类更灵活。
标准库中的 sort 包声明了一个 StringSlice 类型:
type StringSlice []string
附属于 StringSlice 的是一个 Sort 方法:
func (p StringSlice) Sort()
为了按字母顺序排列行星,以下列表将 planets 转换为 sort.StringSlice 类型,然后调用 Sort 方法。
列表 17.5. 排序字符串切片:sort.go
package main
import (
"fmt"
"sort"
)
func main() {
planets := []string{
"Mercury", "Venus", "Earth", "Mars",
"Jupiter", "Saturn", "Uranus", "Neptune",
}
sort.StringSlice(planets).Sort() *1*
fmt.Println(planets) *2*
}
-
1 按字母顺序排序行星
-
2 打印 [地球 木星 火星 水星 海王星 土星 天王星 金星]
为了使其更加简单,sort 包有一个 Strings 辅助函数,它执行类型转换并为你调用 Sort 方法:
sort.Strings(planets)
快速检查 17.5
Q1:
sort.StringSlice(planets)做了什么?
| |
QC 17.5 答案
1:
planets变量从[]string类型转换为StringSlice类型,该类型在sort包中声明。
摘要
-
切片是数组的窗口或视图。
-
range关键字可以遍历切片。 -
当分配或传递给函数时,切片共享相同的基础数据。
-
复合字面量提供了一种方便的方式来初始化切片。
-
你可以为切片附加方法。
让我们看看你是否掌握了这个...
实验:terraform.go
编写一个程序,通过在每个行星前添加 "New " 来改造字符串切片。使用你的程序来改造火星、天王星和海王星。
你的第一次迭代可以使用 terraform 函数,但你的最终实现应该引入一个 Planets 类型,并带有 terraform 方法,类似于 sort.StringSlice。
第 18 课. 更大的切片
在阅读完第 18 课后,你将能够
-
向切片中添加更多元素
-
调查长度和容量是如何工作的
数组有固定数量的元素,切片只是这些固定长度数组的视图。程序员经常需要一个可变长度的数组,它可以按需增长。通过结合切片和名为 append 的内置函数,Go 提供了可变长度数组的特性。本课深入探讨了它是如何工作的。
考虑这一点
你是否有过书籍超出书架容量,或者家庭超出住所或车辆容量的情况?
像书架一样,数组有一定的容量。切片可以关注数组中书籍的部分,并增长以达到书架的容量。如果书架满了,你可以用一个更大的书架替换它,并将所有书籍移过去。然后让切片指向新书架上的书籍,具有更大的容量。
18.1. append 函数
国际天文学联合会(IAU)承认我们太阳系中有五个矮行星,但可能有更多。要向 dwarfs 切片添加更多元素,请使用以下列表中所示的内置 append 函数。
列表 18.1. 更多矮行星:append.go
dwarfs := []string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
dwarfs = append(dwarfs, "Orcus")
fmt.Println(dwarfs) *1*
- 1 打印 [Ceres Pluto Haumea Makemake Eris Orcus]
append 函数是可变参数的,就像 Println 一样,所以你可以一次传递多个元素进行追加:
dwarfs = append(dwarfs, "Salacia", "Quaoar", "Sedna")
fmt.Println(dwarfs) *1*
- 1 打印 [Ceres Pluto Haumea Makemake Eris Orcus Salacia Quaoar Sedna]
dwarfs 切片最初是一个五元素数组的视图,但前面的代码又添加了四个更多元素。这是如何实现的?为了进行调查,你首先需要了解 容量 和内置函数 cap。
快速检查 18.1
Q1:
列表 18.1 中有多少个矮行星?可以使用哪个函数来确定这个数量?
| |
QC 18.1 答案
1:
该切片包含九个矮行星,可以使用
len内置函数来确定:fmt.Println(len(dwarfs)) *1*
- 1 打印 9
18.2. 长度和容量
通过切片可见的元素数量决定了其长度。如果一个切片的底层数组更大,切片仍然可能具有增长的空间。
下面的列表声明了一个函数,用于打印切片的长度和容量。
列表 18.2. Len 和 cap: slice-dump.go
package main
import "fmt"
// dump slice length, capacity, and contents
func dump(label string, slice []string) {
fmt.Printf("%v: length %v, capacity %v %v\n", label, len(slice),
cap(slice), slice)
}
func main() {
dwarfs := []string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
dump("dwarfs", dwarfs) *1*
dump("dwarfs[1:2]", dwarfs[1:2]) *2*
}
-
1 打印矮行星:长度 5,容量 5 [Ceres Pluto Haumea Makemake Eris]
-
2 打印 dwarfs[1:2]:长度 1,容量 4 [Pluto]
由 dwarfs[1:2] 创建的切片长度为 1,但可以容纳 4 个元素。
快速检查 18.2
Q1:
为什么
dwarfs[1:2]切片的容量为 4?
| |
QC 18.2 答案
1:
Pluto Haumea Makemake Eris即使长度为 1,也提供了 4 个元素的容量。
18.3. 研究 append 函数
使用 列表 18.2 中的 dump 函数,下一个列表显示了 append 如何影响容量。
列表 18.3. append 到切片:slice-append.go
dwarfs1 := []string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"} *1*
dwarfs2 := append(dwarfs1, "Orcus") *2*
dwarfs3 := append(dwarfs2, "Salacia", "Quaoar", "Sedna") *3*
-
1 长度 5,容量 5
-
2 长度 6,容量 10
-
3 长度 9,容量 10
支持 dwarfs1 的数组没有足够的空间(容量)来追加奥克鲁斯,因此 append 将 dwarfs1 的内容复制到具有两倍容量的新分配的数组中,如图 18.1(#ch18fig01)所示。dwarfs2 切片指向新分配的数组。额外的容量恰好提供了足够的空间进行下一个 append。
图 18.1. 当需要时,append 分配具有增加容量的新数组。

为了证明 dwarfs2 和 dwarfs3 指向与 dwarfs1 不同的数组,只需修改一个元素并打印出三个切片。
快速检查 18.3
Q1:
如果你修改了 列表 18.3 中的
dwarfs3的一个元素,dwarfs2或dwarfs1会改变吗?dwarfs3[1] = "Pluto!"
| |
QC 18.3 答案
1:
dwarfs3和dwarfs2发生了变化,但dwarfs1保持不变,因为它指向不同的数组。
18.4. 三个索引切片
Go 1.2 版本引入了 三个索引切片 来限制结果切片的容量。在下一个列表中,terrestrial 的长度和容量为 4。追加 Ceres 会导致分配新的数组,而 planets 数组保持不变。
列表 18.4. 切片后的容量:three-index-slicing.go
planets := []string{
"Mercury", "Venus", "Earth", "Mars",
"Jupiter", "Saturn", "Uranus", "Neptune",
}
terrestrial := planets[0:4:4] *1*
worlds := append(terrestrial, "Ceres")
fmt.Println(planets) *2*
-
1 长度 4,容量 4
-
2 打印 [水星 金星 地球 火星 木星 土星 天王星 海王星]
如果没有指定第三个索引,terrestrial 将具有容量 8。追加 Ceres 不会分配新的数组,而是覆盖 Jupiter:
terrestrial = planets[0:4] *1*
worlds = append(terrestrial, "Ceres")
fmt.Println(planets) *2*
-
1 长度 4,容量 8
-
2 打印 [水星 金星 地球 火星 灶神星 土星 天王星 海王星]
除非你想覆盖木星,否则在切片时应该默认使用三个索引切片。
快速检查 18.4
Q1:
何时应该使用三个索引切片?
| |
QC 18.4 答案
1:
何时不应使用三个索引切片?除非你明确想要覆盖基本数组的元素,否则使用三个索引切片设置容量要安全得多。
18.5. 使用 make 预分配切片
当没有足够的容量进行 append 时,Go 必须分配一个新的数组并复制旧数组的所有内容。你可以通过使用内置的 make 函数预先分配切片来避免额外的分配和复制。
下一个列表中的 make 函数指定了 dwarfs 切片的长度(0)和容量(10)。在 dwarfs 容量耗尽之前,最多可以追加 10 个元素,这会导致 append 分配一个新的数组。
列表 18.5. 创建切片:slice-make.go
dwarfs := make([]string, 0, 10)
dwarfs = append(dwarfs, "Ceres", "Pluto", "Haumea", "Makemake", "Eris")
容量参数是可选的。要从一个长度和容量为 10 的切片开始,可以使用 make([]string, 10)。这 10 个元素将包含它们类型的零值,在这种情况下是一个空字符串。append 内置函数将添加第 11 个元素。
快速检查 18.5
Q1:
使用
make创建切片有什么好处?
| |
QC 18.5 答案
1:
使用
make预分配可以设置初始容量,从而避免在扩大底层数组时进行额外的分配和复制。
18.6. 声明可变参数函数
Printf 和 append 是 可变参数 函数,因为它们接受可变数量的参数。要声明一个可变参数函数,使用最后一个参数的省略号 ...,如下所示。
列表 18.6. 可变参数函数:variadic.go
func terraform(prefix string, worlds ...string) []string {
newWorlds := make([]string, len(worlds)) *1*
for i := range worlds {
newWorlds[i] = prefix + " " + worlds[i]
}
return newWorlds
}
- 1 创建一个新的切片而不是直接修改世界
worlds 参数是一个包含零个或多个传递给 terraform 的参数的字符串切片:
twoWorlds := terraform("New", "Venus", "Mars")
fmt.Println(twoWorlds) *1*
- 1 打印 [新金星 新火星]
要传递一个切片而不是多个参数,使用省略号展开切片:
planets := []string{"Venus", "Mars", "Jupiter"}
newPlanets := terraform("New", planets...)
fmt.Println(newPlanets) *1*
- 1 打印 [新金星 新火星 新木星]
如果 terraform 要修改(或 修改)worlds 参数的元素,planets 切片也会看到这些更改。通过使用 newWorlds,terraform 函数避免了修改传递的参数。
快速检查 18.6
Q1:
三个使用省略号
...的用途是什么?
| |
QC 18.6 答案
1:
- 让 Go 编译器计算复合字面量中数组元素的数量。
- 使可变参数函数的最后一个参数捕获零个或多个参数作为一个切片。
- 将切片的元素展开为传递给函数的参数。
概述
-
切片有一个长度和一个容量。
-
当容量不足时,内置的
append函数将分配一个新的底层数组。 -
你可以使用
make函数来预分配一个切片。 -
可变参数函数接受多个参数,这些参数被放置在一个切片中。
让我们看看你是否明白了...
实验:capacity.go
编写一个程序,使用循环不断向切片中追加一个元素。每当切片容量改变时,打印出切片的容量。append 在底层数组空间不足时总是加倍容量吗?
第 19 课。永远多才多艺的映射
在阅读 第 19 课 之后,你将能够
-
将映射用作非结构化数据的集合
-
声明、访问和遍历映射
-
探索一些多用途映射类型的用法
当你在寻找某物时,映射很有用,我们不仅仅是在谈论谷歌地图 (www.google.com/mars/)。Go 提供了一个具有键映射到值的映射集合。而数组和切片是通过顺序整数索引的,映射键 可以是任何类型。
注意
这个集合有几个不同的名称:Python 中的字典、Ruby 中的散列和 JavaScript 中的对象。PHP 中的关联数组和 Lua 中的表既作为映射也作为常规数组。
映射在键在程序运行时确定的无结构数据中特别有用。用脚本语言编写的程序倾向于使用映射来处理 结构化数据,即键在事先已知的数据。第 21 课介绍了 Go 的结构类型,它更适合这些情况。
考虑这个
映射将键与值关联起来,这对于索引很有用。如果您知道一本书的标题,遍历数组中的每一本书可能需要一些时间,就像在图书馆或书店的每个通道的每个书架上查找一样。按书名键的映射在这一点上更快。
在哪些其他情况下,从键到值的映射可能很有用?
19.1. 声明映射
与数组切片不同,数组的键是连续的整数,映射的键可以是任何类型。在 Go 中,您必须指定键和值的类型。要声明一个具有 string 类型键和 int 类型值的映射,语法是 map[string]int,如图 19.1 所示。
Figure 19.1. 一个具有字符串键和整数值的地图

在 清单 19.1 中声明的 temperature 映射包含来自行星事实表的平均温度 (nssdc.gsfc.nasa.gov/planetary/factsheet/)。您可以使用复合字面量声明和初始化映射,就像其他集合类型一样。对于每个元素,指定适当的键和值。使用方括号 [] 通过键查找值,覆盖现有值或向映射中添加值。
Listing 19.1. 平均温度映射:map.go
temperature := map[string]int{
"Earth": 15, *1*
"Mars": -65,
}
temp := temperature["Earth"]
fmt.Printf("On average the Earth is %v° C.\n", temp) *2*
temperature["Earth"] = 16 *3*
temperature["Venus"] = 464
fmt.Println(temperature) *4*
-
1 复合字面量是映射的键值对。
-
2 打印 On average the Earth is 15° C.
-
3 一点气候变化
-
4 打印地图[Venus:464 Earth:16 Mars:-65]
如果您访问映射中不存在的键,结果将是该类型的零值 (int):
moon := temperature["Moon"]
fmt.Println(moon) *1*
- 1 打印 0
Go 提供了 逗号,ok 语法,您可以使用它来区分 "Moon" 在地图中不存在与存在于地图中且温度为 0° C 的情况:
if moon, ok := temperature["Moon"]; ok { *1*
fmt.Printf("On average the moon is %v° C.\n", moon)
} else {
fmt.Println("Where is the moon?") *2*
}
-
1 逗号,ok 语法
-
2 打印 Where is the moon?
moon 变量将包含在 "Moon" 键中找到的值或零值。如果键存在,则额外的 ok 变量将为 true,否则为 false。
注意
当使用逗号,ok 语法时,您可以使用您喜欢的任何变量名:
temp, found := temperature["Venus"]
快速检查 19.1
1
您会使用什么类型来声明一个具有 64 位浮点键和整数值的映射?
2
如果您修改 清单 19.1,使得
"Moon"键存在且值为0,使用逗号,ok 语法的结果是什么?
QC 19.1 答案
1
映射类型是
map[float64]int。2
ok的值将是true:temperature := map[string]int{ "Earth": 15, "Mars": -65, "Moon": 0, } if moon, ok := temperature["Moon"]; ok { fmt.Printf("On average the moon is %v° C.\n", moon) *1* } else { fmt.Println("Where is the moon?") }
- 1 打印 On average the moon is 0° C.
19.2. 地图不会被复制
正如你在第 16 课中学到的,数组在分配给新变量或传递给函数或方法时会被复制。对于 int 和 float64 这样的原始类型也是如此。
地图的行为不同。在下一个列表中,planets 和 planetsMarkII 共享相同的基本数据。正如你所看到的,对其中一个的更改会影响另一个。考虑到这种情况,这有点不幸。
列表 19.2. 指向相同的数据:whoops.go
planets := map[string]string{
"Earth": "Sector ZZ9",
"Mars": "Sector ZZ9",
}
planetsMarkII := planets
planets["Earth"] = "whoops"
fmt.Println(planets) *1*
fmt.Println(planetsMarkII) *1*
delete(planets, "Earth") *2*
fmt.Println(planetsMarkII) *3*
-
1 打印 map[Earth:whoops Mars:Sector ZZ9]
-
2 从地图中移除地球
-
3 打印 map[Mars:Sector ZZ9]
当内置的 delete 函数从一个地图中删除一个元素时,planets 和 planetsMarkII 都会受到这个变化的影响。如果你将一个地图传递给一个函数或方法,它可能会改变地图的内容。这种行为类似于多个切片指向同一个基本数组。
快速检查 19.2
1
为什么在列表 19.2 中
planets的更改也会反映在planetsMarkII中?2
内置的
delete函数做什么?
| |
QC 19.2 答案
1
planetsMarkII变量指向与planets相同的基本数据。2
delete函数从一个地图中删除一个元素。
19.3. 使用 make 预分配地图
地图在另一个方面与切片相似。除非你使用复合字面量初始化它们,否则地图需要使用内置的 make 函数进行分配。
对于地图,make 只接受一个或两个参数。第二个参数为键的数量预分配空间,就像切片的容量一样。使用 make 时,地图的初始长度始终为零:
temperature := make(map[float64]int, 8)
快速检查 19.3
Q1:
你认为使用
make预分配地图有什么好处?
| |
QC 19.3 答案
1:
就像切片一样,为地图指定一个初始大小可以在地图变大时节省计算机一些工作。
19.4. 使用地图计数
列表 19.3 中的代码确定从 MAAS API (github.com/ingenology/mars_weather_api) 读取的温度频率。如果 frequency 是一个切片,键需要是整数,并且基本数组需要预留空间来计数从未实际发生的温度。在这种情况下,地图显然是一个更好的选择。

列表 19.3. 温度频率:frequency.go
temperatures := []float64{
-28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
}
frequency := make(map[float64]int)
for _, t := range temperatures { *1*
frequency[t]++
}
for t, num := range frequency { *2*
fmt.Printf("%+.2f occurs %d times\n", t, num)
}
-
1 遍历一个切片(索引,值)
-
2 遍历一个地图(键,值)
使用 range 关键字进行迭代在切片、数组和地图中工作方式类似。对于每个迭代,地图提供键和值,而不是索引和值。请注意,Go 不保证地图键的顺序,所以输出可能在不同运行之间发生变化。
快速检查 19.4
Q1:
当遍历映射时,哪两个变量被填充?
QC 19.4 答案
1:
映射中每个元素的键和值。
19.5. 使用映射和切片对数据进行分组
我们不再确定温度的频率,而是将温度按 10°的间隔分组。为此,以下列表将映射从组映射到该组中的温度切片。
列表 19.4. 切片映射:group.go
temperatures := []float64{
-28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
}
groups := make(map[float64][]float64) *1*
for _, t := range temperatures {
g := math.Trunc(t/10) * 10 *2*
groups[g] = append(groups[g], t)
}
for g, temperatures := range groups {
fmt.Printf("%v: %v\n", g, temperatures)
}
-
1 具有 float64 键和[]float64 值的映射
-
2 将温度四舍五入到-20、-30 等
之前的列表产生如下输出:
30: [32]
-30: [-31 -33]
-20: [-28 -29 -23 -29 -28]
快速检查 19.5
Q1:
声明
var groups map[string][]int中键和值的类型是什么?
QC 19.5 答案
1:
groups映射的键是字符串类型,值是整数切片。
19.6. 将映射作为集合重用
集合 是一个类似于数组的集合,除了每个元素都保证只发生一次。Go 不提供集合集合,但你可以通过使用映射来即兴创作,如下面的列表所示。值并不重要,但 true 对于检查 集合成员 非常方便。如果一个温度在映射中,并且它的值为 true,那么它就是集合的成员。
列表 19.5. 一个临时的集合:set.go
var temperatures = []float64{
-28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
}
set := make(map[float64]bool) *1*
for _, t := range temperatures {
set[t] = true
}
if set[-28.0] {
fmt.Println("set member") *2*
}
fmt.Println(set) *3*
-
1 创建具有布尔值的映射
-
2 打印集合成员
-
3 打印 map[-31:true -29:true -23:true -33:true -28:true 32:true]
你可以看到,映射只包含每个温度的一个键,任何重复都被删除。但是,Go 中映射键的顺序是任意的,因此在使用之前,必须将温度转换回切片:
unique := make([]float64, 0, len(set))
for t := range set {
unique = append(unique, t)
}
sort.Float64s(unique)
fmt.Println(unique) *1*
- 1 打印 [-33 -31 -29 -28 -23 32]
快速检查 19.6
Q1:
你会如何检查 32.0 是否是
set的成员?
QC 19.6 答案
1:
if set[32.0] { // set member }
摘要
-
映射是用于非结构化数据的灵活集合。
-
复合字面量提供了一种方便的方式来初始化映射。
-
range关键字可以遍历映射。 -
当映射被赋值或传递给函数时,映射共享相同的基本数据。
-
当集合相互结合时,集合变得更加强大。
让我们看看你是否掌握了这个...
实验:words.go
编写一个函数来计算文本字符串中单词的频率,并返回一个包含单词及其计数的映射。该函数应将文本转换为小写,并从单词中去除标点符号。strings 包包含几个有助于此任务的函数,包括 Fields、ToLower 和 Trim。
使用你的函数来计算以下段落中单词的频率,然后显示任何出现多次的单词的计数。
就他的视线所能达到的地方,他看到的只有周围大植物的茎在紫色的阴影中后退,而在头顶上方,巨大的叶子多次透明地过滤阳光,形成了他行走的庄严辉煌的黄昏。每当他认为自己能够做到的时候,他就再次奔跑;地面继续柔软而有弹性,覆盖着同样的有弹性的杂草,这是他在马拉坎达第一次用手触摸的东西。一次或两次,一个小红动物从他面前逃窜,但除此之外,似乎树林中没有生命在动;没有什么可怕的事情——除了在远离人类触及或认知的数千里或数百万里的未知植被森林中无装备、独自漫游的事实。
C.S. Lewis, Out of the Silent Planet, (see mng.bz/V7nO)
第 20 课. 顶峰:生命的一角

对于这个挑战,你将构建一个模拟人口不足、过度人口和繁殖的康威生命游戏(见mng.bz/xOyY)。这个模拟是在一个二维细胞网格上进行的。因此,这个挑战侧重于切片。
每个细胞在水平、垂直和对角方向上都有八个相邻细胞。在每一代中,细胞根据活着的邻居数量来决定是生存还是死亡。
20.1. 新宇宙
对于你第一次实现生命游戏,将宇宙限制为固定大小。确定网格的尺寸并定义一些常量:
const (
width = 80
height = 15
)
接下来,定义一个Universe类型来存储二维细胞场。使用布尔类型,每个细胞要么是死的(false),要么是活的(true):
type Universe [][]bool
使用切片而不是数组,这样就可以通过函数或方法共享并修改宇宙。
注意
课程 26 介绍了指针,这是一种允许你直接通过函数和方法共享数组的替代方案。
编写一个NewUniverse函数,使用make分配并返回一个具有height行和每行width列的Universe:
func NewUniverse() Universe
新分配的切片将默认为零值,即false,因此宇宙开始时是空的。
20.1.1. 观察宇宙
编写一个方法,使用fmt包将宇宙打印到屏幕上。用星号表示活着的细胞,用空格表示死细胞。确保在打印每一行后换行:
func (u Universe) Show()
编写一个main函数来创建NewUniverse并显示它。在继续之前,确保你可以运行你的程序,即使宇宙是空的。
20.1.2. 种植活细胞
编写一个Seed方法,随机将大约 25%的细胞设置为活着的(true):
func (u Universe) Seed()
记得导入math/rand以使用Intn函数。完成后,更新main以使用Seed填充宇宙,并用Show显示你的作品。
20.2. 实现游戏规则
康威生命游戏的规则如下:
-
活着的细胞如果少于两个活着的邻居就会死亡。
-
有两个或三个活邻居的活细胞会活到下一代。
-
有超过三个活邻居的活细胞会死亡。
-
恰好有三个活邻居的死亡细胞变成活细胞。
要实现规则,将它们分解为三个步骤,每个步骤都可以是一个方法:
-
确定一个细胞是否活着的方法
-
能够计算活细胞邻居数量的能力
-
确定细胞在下一代应该活着还是死亡的逻辑
20.2.1. 死或活?
应该很容易确定一个细胞是死是活。只需在Universe切片中查找一个单元格。如果布尔值为true,则单元格是活着的。
在Universe类型上编写一个具有以下签名的Alive方法:
func (u Universe) Alive(x, y int) bool
当细胞在宇宙之外时,会出现一个复杂的情况。(-1,-1)是死是活?在一个 80 × 15 的网格上,(80,15)是死是活?
为了解决这个问题,使宇宙环绕。上面的邻居(0,0)将变成(0,14)而不是(0,–1),这可以通过将height加到y上来计算。如果y超过了网格的height,你可以转向我们用于闰年计算的模运算符(%)。使用%将y除以height并保留余数。对于x和width也是同样的道理。
20.2.2. 计算邻居数量
编写一个方法来计算给定单元格的活细胞邻居数量,从 0 到 8。而不是直接访问宇宙数据,使用Alive方法以便宇宙可以环绕:
func (u Universe) Neighbors(x, y int) int
一定要只计算相邻的邻居,而不是计算中的单元格。
20.2.3. 游戏逻辑
现在你已经可以确定一个单元格是否有两个、三个或更多邻居,你可以实现本节开头所示的规则。编写一个Next方法来完成这个操作:
func (u Universe) Next(x, y int) bool
不要直接修改宇宙。相反,返回单元格在下一代应该死亡还是活着。
20.3. 并行宇宙
要完成模拟,你需要遍历宇宙中的每个单元格并确定其Next状态应该是什么。
有一个陷阱。在计算邻居时,你的计数应该基于宇宙的先前状态。如果你直接修改宇宙,那些更改将影响周围单元格的邻居计数。
一个简单的解决方案是创建两个相同大小的宇宙。在读取宇宙 A 的同时设置宇宙 B 中的单元格。编写一个Step函数来执行此操作:
func Step(a, b Universe)
一旦宇宙 B 包含下一代,你可以交换宇宙并重复:
a, b = b, a
在显示新一代之前清除屏幕,打印"\x0c",这是一个特殊的 ANSI 转义序列。然后显示宇宙并使用time包中的Sleep函数来减慢动画。
注意
在 Go Playground 之外,你可能需要另一种机制来清除屏幕,例如在 macOS 上的"\033[H"。
现在你应该拥有编写完整的生命游戏模拟并在 Go Playground 中运行所需的一切。
当你完成时,请将你的解决方案的 Playground 链接分享到 Manning 论坛的forums.manning.com/forums/get-programming-with-go。
第 5 单元。状态和行为
在 Go 中,值代表状态,例如门是打开还是关闭。函数和方法定义行为——对状态的操作,例如打开门。
随着程序的增大,它们变得越来越难以管理和维护,除非你有合适的工具。
如果有几个门可以独立打开或关闭,将状态和行为捆绑在一起是有帮助的。编程语言还允许你表达抽象的概念,例如可以打开的东西。然后在炎热的夏日,你可以打开所有可以打开的东西,无论是门还是窗户。
有很多大词来描述这些概念:面向对象、封装、多态和组合。本单元的课程旨在阐明这些概念,并展示 Go 在面向对象设计方面相当独特的方法。
第 21 课。一点结构体
阅读完第 21 课后,你将能够
-
给火星上的坐标一点结构体
-
将结构体编码为流行的 JSON 数据格式
一个车辆由许多部分组成,这些部分可能具有相关的值(或状态)。发动机正在运行,车轮在转动,电池完全充电。为每个值使用单独的变量就像车辆在商店中拆解一样。同样,一栋建筑可能有打开的窗户和未锁的门。为了组装部件或构建结构,Go 提供了结构体类型。
考虑这一点
而集合是同一类型,结构体允许你将不同的事物组合在一起。环顾四周。你看到了什么可以用结构体来表示?
21.1. 声明一个结构体
一对坐标是采用一点结构的良好候选。纬度和经度总是一起出现。在一个没有结构体的世界中,计算两个位置之间距离的函数需要两对坐标:
func distance(lat1, long1, lat2, long2 float64) float64
虽然这确实可行,但传递独立的坐标容易出错,而且非常繁琐。纬度和经度是一个单一的单位,结构体允许你将它们视为这样的单位。
下一个列表中的curiosity结构体使用浮点字段声明纬度和经度。要为字段赋值或访问字段的值,使用带有变量名点字段名的点表示法,如下所示。
列表 21.1.介绍一点结构体:struct.go
var curiosity struct {
lat float64
long float64
}
curiosity.lat = -4.5895 *1*
curiosity.long = 137.4417 *1*
fmt.Println(curiosity.lat, curiosity.long) *2*
fmt.Println(curiosity) *3*
-
1 将值赋给结构体的字段
-
2 打印 -4.5895 137.4417
-
3 打印 {-4.5895 137.4417}
注意
Print函数族将显示结构体的内容。
火星好奇号探测器从布拉德伯里着陆点开始其旅程,位于南纬 4°35’22.2”,东经 137°26’30.1”。在第 21.1 节中,布拉德伯里着陆点的纬度和经度以十进制度数表示,北纬为正值,东经为正值,如图 21.1 节所示。
图 21.1. 十进制度数的纬度和经度

快速检查 21.1
1
结构体相较于单个变量有什么优势?
2
Bradbury Landing 大约在火星“海平面”下方 4,400 米。如果
curiosity有一个海拔字段,你会如何将其值设置为 -4400?
| |
QC 21.1 答案
1
结构体将相关值组合在一起,使得传递它们变得更加简单且错误率更低。
2
curiosity.altitude = -4400
21.2. 使用类型重复使用结构体
如果你需要具有相同字段的多余结构体,你可以定义一个类型,就像第 13 课中的 celsius 类型一样。以下列表中声明的 location 类型用于将 Spirit 探索车放置在 Columbia Memorial Station,并将 Opportunity 探索车放置在 Challenger Memorial Station。
列表 21.2. 位置类型:location.go
type location struct {
lat float64
long float64
}
var spirit location *1*
spirit.lat = -14.5684
spirit.long = 175.472636
var opportunity location *1*
opportunity.lat = -1.9462
opportunity.long = 354.4734
fmt.Println(spirit, opportunity) *2*
-
1 重复使用位置类型
-
2 打印 {-14.5684 175.472636} {-1.9462 354.4734}
快速检查 21.2
Q1:
你会如何修改列表 21.1 中的代码以使用 Curiosity 探索车在 Bradbury Landing 的
location类型?
| |
QC 21.2 答案
1:
var curiosity location curiosity.lat = -4.5895 curiosity.long = 137.4417
21.3. 使用复合字面量初始化结构体
初始化结构体的复合字面量有两种不同的形式。在列表 21.3 中,opportunity 和 insight 变量使用字段值对进行初始化。字段可以以任何顺序排列,未列出的字段将保留其类型的零值。这种形式可以容忍变化,即使在结构体中添加字段或重新排序字段,它也能继续正确工作。如果 location 类型增加了一个海拔字段,opportunity 和 insight 将默认为海拔零。
列表 21.3. 带字段值对的复合字面量:struct-literal.go
type location struct {
lat, long float64
}
opportunity := location{lat: -1.9462, long: 354.4734}
fmt.Println(opportunity) *1*
insight := location{lat: 4.5, long: 135.9}
fmt.Println(insight) *2*
-
1 打印 {-1.9462 354.4734}
-
2 打印 {4.5 135.9}
列表 21.4 中的复合字面量没有指定字段名称。相反,必须为结构体定义中列出的每个字段提供值,顺序与结构体定义中的顺序相同。这种形式最适合稳定且只有少量字段的类型。如果 location 类型增加了一个海拔字段,spirit 必须为海拔指定一个值,以便程序能够编译。混淆 lat 和 long 的顺序不会导致编译器错误,但程序将不会产生正确的结果。
列表 21.4. 仅带值的复合字面量:struct-literal.go
spirit := location{-14.5684, 175.472636}
fmt.Println(spirit) *1*
- 1 打印 {-14.5684 175.472636}
无论你如何初始化结构体,你都可以通过在 %v 格式动词前加上加号 + 来修改它,以打印出字段名称,如下一列表所示。这对于检查大型结构体特别有用。
列表 21.5. 打印结构体的键:struct-literal.go
curiosity := location{-4.5895, 137.4417}
fmt.Printf("%v\n", curiosity) *1*
fmt.Printf("%+v\n", curiosity) *2*
-
1 打印 {-4.5895 137.4417}
-
2 打印 {lat:-4.5895 long:137.4417}
快速检查 21.3
Q1:
在哪些方面,字段值复合字面量语法比仅值的形式更可取?
| |
QC 21.3 答案
1:
- 字段可以按任何顺序列出。
- 字段是可选的,如果没有列出,则采用零值。
- 在重新排序或添加字段到结构声明时,不需要进行任何更改。
21.4. 结构被复制
当好奇号探测器从布拉德伯里着陆点向东前往耶洛奈夫湾时,布拉德伯里着陆点的位置在现实生活中,以及在下一条列表中都没有改变。curiosity 变量使用 bradbury 中包含的值的副本进行初始化,因此这些值独立变化。
列表 21.6. 赋值会创建一个副本:struct-value.go
bradbury := location{-4.5895, 137.4417}
curiosity := bradbury
curiosity.long += 0.0106 *1*
fmt.Println(bradbury, curiosity) *2*
-
1 向东前往耶洛奈夫湾
-
2 打印 {-4.5895 137.4417} {-4.5895 137.4523}
快速检查 21.4
Q1:
如果将
curiosity传递给一个操作lat或long的函数,调用者会看到这些更改吗?
| |
QC 21.4 答案
1:
不,函数将接收
curiosity的一个副本,就像数组一样。
21.5. 结构切片
结构切片 []struct 是一个包含零个或多个值(一个切片)的集合,其中每个值基于一个结构,而不是像 float64 这样的原始类型。
如果一个程序需要一个火星探测车的着陆点集合,那么以下列表所示的两个单独的纬度和经度切片的方式是不正确的。
列表 21.7. 两个浮点数切片:slice-struct.go
lats := []float64{-4.5895, -14.5684, -1.9462}
longs := []float64{137.4417, 175.472636, 354.4734}
这已经看起来很糟糕了,尤其是在本课程早期引入的位置结构的基础上。现在想象一下,添加更多用于海拔等的切片。编辑前一个列表时的错误很容易导致切片之间的数据错位,甚至不同长度的切片。
一个更好的解决方案是创建一个包含每个值都是一个结构的单个切片。然后每个位置都是一个单独的单元,可以根据需要扩展着陆点的名称或其他字段,如下一个列表所示。
列表 21.8. 位置的一个切片:slice-struct.go
type location struct {
name string
lat float64
long float64
}
locations := []location{
{name: "Bradbury Landing", lat: -4.5895, long: 137.4417},
{name: "Columbia Memorial Station", lat: -14.5684, long: 175.472636},
{name: "Challenger Memorial Station", lat: -1.9462, long: 354.4734},
}
快速检查 21.5
Q1:
使用多个相互关联的切片有什么危险?
| |
QC 21.5 答案
1:
很容易导致数据在切片之间错位。
21.6. 将结构编码为 JSON
JavaScript 对象表示法,或 JSON (json.org),是一种由 Douglas Crockford 推广的标准数据格式。它基于 JavaScript 语言的子集,但在其他编程语言中得到广泛支持。JSON 通常用于 Web API(应用程序编程接口),包括提供好奇号探测器天气数据的 MAAS API (github.com/ingenology/mars_weather_api)。
json包中的Marshal函数在列表 21.9 中用于将location中的数据编码成 JSON 格式。Marshal返回 JSON 数据作为字节,这些字节可以通过网络发送或转换为字符串以供显示。它也可能返回一个错误,这是一个在课程 28 中讨论的主题。
列表 21.9. 编码位置:json.go
package main
import (
"encoding/json"
"fmt"
"os"
)
func main() {
type location struct {
Lat, Long float64 *1*
}
curiosity := location{-4.5895, 137.4417}
bytes, err := json.Marshal(curiosity)
exitOnError(err)
fmt.Println(string(bytes)) *2*
}
// exitOnError prints any errors and exits.
func exitOnError(err error) {
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
-
1 字段必须以大写字母开头。
-
2 打印 {“Lat”:-4.5895,“Long”:137.4417}
注意,JSON 键与location结构体的字段名相匹配。为了使这生效,json包要求字段必须导出。如果Lat和Long以小写字母开头,输出将是{}。
快速检查 21.6
Q1:
JSON 的缩写是什么?
| |
QC 21.6 答案
1:
JSON 代表 JavaScript 对象表示法。
21.7. 使用 struct 标签自定义 JSON
Go 的 json 包要求字段以大写字母开头,并且多词字段名按照惯例使用 CamelCase。你可能希望 JSON 键使用 snake_case,尤其是在与 Python 或 Ruby 交互时。结构体的字段可以用你希望 json 包使用的字段名进行标记。
从列表 21.9 到列表 21.10 的唯一变化是包含了改变Marshal函数输出的struct 标签。注意,Lat和Long字段仍然必须导出,以便json包能够看到它们。
列表 21.10. 自定义位置字段:json-tags.go
type location struct {
Lat float64 `json:"latitude"` *1*
Long float64 `json:"longitude"` *1*
}
curiosity := location{-4.5895, 137.4417}
bytes, err := json.Marshal(curiosity)
exitOnError(err)
fmt.Println(string(bytes)) *2*
-
1 Struct 标签改变了输出。
-
2 打印 {“latitude”:-4.5895,“longitude”:137.4417}
Struct 标签是与结构体字段相关联的普通字符串。原始字符串字面量(``)更可取,因为引号不需要用反斜杠转义,就像在不太易读的"json:"latitude\""中那样。
Struct 标签的格式为key:"value",其中键通常是包的名称。为了同时自定义 JSON 和 XML 中的Lat字段,struct 标签将是json:"latitude" xml:"latitude"。
如其名所示,struct 标签仅用于结构体的字段,尽管 json.Marshal 也会编码其他类型。
快速检查 21.7
Q1:
为什么在编码 JSON 时
Lat和Long字段必须以大写字母开头?
| |
QC 21.7 答案
1:
字段必须导出,以便
json包能够看到它们。
摘要
-
结构将值组合成一个单元。
-
结构体是当分配或传递给函数时复制的值。
-
复合字面量提供了一种方便的方式来初始化结构体。
-
Struct 标签用额外的信息装饰导出字段,这些信息可以被包使用。
-
json包使用 struct 标签来控制字段名的输出。
让我们看看你是否理解了...
实验:landing.go
编写一个程序,显示 列表 21.8 中三个火星着陆点的 JSON 编码。JSON 应包括每个着陆点的名称,并使用如 列表 21.10 所示的结构标签。
为了使输出更友好,请使用 json 包中的 MarshalIndent 函数。
第 22 课. Go 没有类
在阅读 第 22 课 后,你将能够
-
编写提供结构化数据行为的方法
-
应用面向对象设计原则
Go 语言与经典语言不同。它没有类和对象,并且省略了继承等特性。然而,Go 语言仍然提供了应用面向对象设计思想所需的功能。本节课探讨了结构与方法的结合。
考虑这一点
协同效应 是在创业圈中经常听到的术语。它的意思是“大于其各部分的总和”。Go 语言有类型、类型上的方法和结构。三者结合,提供了类在其他语言中提供的许多功能,而不需要将新概念引入语言中。
有哪些其他方面的 Go 语言表现出这种结合以创造更伟大事物的特性?
22.1. 将方法附加到结构体
在 第 13 课 中,你将 celsius 和 fahrenheit 方法附加到 kelvin 类型上以转换温度。同样,方法可以附加到你声明的其他类型上。无论底层类型是 float64 还是 struct,效果都是一样的。
首先,你需要声明一个类型,例如以下列表中的 coordinate 结构体。
列表 22.1. coordinate 类型:coordinate.go
// coordinate in degrees, minutes, seconds in a N/S/E/W hemisphere.
type coordinate struct {
d, m, s float64
h rune
}
布拉德利着陆点位于 DMS 格式(度、分、秒)中的 4°35’22.2” S,137°26’30.1” E。一分钟有 60 秒 ("),一度有 60 分钟 ('),但这些分钟和秒代表的是一个位置,而不是时间。
以下列表中的十进制方法将把 DMS 坐标转换为十进制度。
列表 22.2. 十进制方法:coordinate.go
// decimal converts a d/m/s coordinate to decimal degrees.
func (c coordinate) decimal() float64 {
sign := 1.0
switch c.h {
case 'S', 'W', 's', 'w':
sign = -1
}
return sign * (c.d + c.m/60 + c.s/3600)
}
现在,你可以使用友好的 DMS 格式提供坐标,并将它们转换为十进制度以进行计算:
// Bradbury Landing: 4°35'22.2" S, 137°26'30.1" E
lat := coordinate{4, 35, 22.2, 'S'}
long := coordinate{137, 26, 30.12, 'E'}
fmt.Println(lat.decimal(), long.decimal()) *1*
- 1 打印 -4.5895 137.4417
快速检查 22.1
Q1:
十进制方法中的接收器是什么?
QC 22.1 答案
1:
接收器是
c,类型为coordinate。
22.2. 构造函数
要从度、分、秒构建十进制度数的位置,可以使用 列表 22.2 中的十进制方法与复合字面量:
type location struct {
lat, long float64
}
curiosity := location{lat.decimal(), long.decimal()}
如果需要一个比值列表更复杂的复合字面量,请考虑编写一个构造函数。以下列表声明了一个名为 newLocation 的构造函数。
列表 22.3. 构建新位置:construct.go
// newLocation from latitude, longitude d/m/s coordinates.
func newLocation(lat, long coordinate) location {
return location{lat.decimal(), long.decimal()}
}
古典语言提供构造器作为特殊语言特性来构建对象。Python 有_init_,Ruby 有initialize,PHP 有__construct()。Go 没有构造器的语言特性。相反,newLocation是一个遵循约定的普通函数。

形式为newType或NewType的函数用于构建该类型的值。你将其命名为newLocation还是NewLocation取决于该函数是否导出以供其他包使用,如第 12 课所述。你像使用任何其他函数一样使用newLocation:
curiosity := newLocation(coordinate{4, 35, 22.2, 'S'},
coordinate{137, 26, 30.12, 'E'})
fmt.Println(curiosity) *1*
- 1 打印 {-4.5895 137.4417}
如果你想要从各种输入中构建位置,只需声明多个具有合适名称的函数——例如,对于度、分、秒和十进制度,可以分别命名为newLocationDMS和newLocationDD。
注意
有时构造函数命名为New,例如errors包中的New函数。因为函数调用以所属包为前缀,所以将函数命名为NewError会被读作errors.NewError,而不是更简洁、更可取的errors.New。
| |
快速检查 22.2
Q1:
你会给一个构建类型为
Universe的变量的函数起什么名字?
| |
QC 22.2 答案
1:
按照惯例,函数会被命名为
NewUniverse,或者如果不导出,则为newUniverse。
22.3. 类的替代方案
Go 没有像 Python、Ruby 和 Java 这样的经典语言的class。然而,具有少量方法的结构可以完成许多相同的目的。如果你眯着眼睛看,它们并没有那么不同。
为了强调这一点,从头开始构建一个新的world类型。它将有一个表示行星半径的字段,你将使用它来计算两个位置之间的距离,如下所示。
列表 22.4. 一个全新的world:world.go
type world struct {
radius float64
}
火星的平均半径为 3,389.5 千米。与其将 3389.5 声明为一个常量,不如使用world类型将火星声明为许多可能的世界之一:
var mars = world{radius: 3389.5}
然后将一个distance方法附加到world类型上,使其能够访问radius字段。它接受两个参数,都是location类型,并将返回千米距离:
func (w world) distance(p1, p2 location) float64 {
*1*
}
- 1 待办事项:使用 w.radius 进行一些数学运算
这将涉及一些数学运算,所以请确保导入math包,如下所示:
import "math"
位置类型使用度来表示纬度和经度,但标准库中的数学函数使用弧度。鉴于一个圆有 360°或 2π弧度,以下函数执行必要的转换:
// rad converts degrees to radians.
func rad(deg float64) float64 {
return deg * math.Pi / 180
}
现在进行距离计算。它使用包括正弦、余弦和反余弦在内的多个三角函数。如果你是数学爱好者,可以查找公式 (www.movable-type.co.uk/scripts/latlong.html) 并研究余弦定理来了解它是如何工作的。火星不是一个完美的球体,但这个公式在我们的目的上已经足够接近了:
// distance calculation using the Spherical Law of Cosines.
func (w world) distance(p1, p2 location) float64 {
s1, c1 := math.Sincos(rad(p1.lat))
s2, c2 := math.Sincos(rad(p2.lat))
clong := math.Cos(rad(p1.long - p2.long))
return w.radius * math.Acos(s1*s2+c1*c2*clong) *1*
}
- 1 使用世界的半径字段
如果你感到困惑,不要担心。计算距离的程序需要数学,但只要 distance 返回正确的结果,完全理解所有数学工作是如何进行的就不是必须的(尽管这是一个好主意)。
说到结果,要看到 distance 的实际应用,声明一些位置并使用之前声明的 mars 变量:
spirit := location{-14.5684, 175.472636}
opportunity := location{-1.9462, 354.4734}
dist := mars.distance(spirit, opportunity) *1*
fmt.Printf("%.2f km\n", dist) *2*
-
1 在火星上使用距离方法
-
2 打印 9669.71 公里
如果你得到不同的结果,请返回并确保代码的输入与显示的完全一致。缺少一个 rad 将导致计算错误。如果所有其他方法都失败了,请从 github.com/nathany/get-programming-with-go 下载代码,并接受复制粘贴。
distance 方法是从地球的公式中采用的,但使用的是火星的半径。通过在 world 类型上声明 distance 为一个方法,你可以计算其他世界的距离,例如地球。每个行星的半径可以在 表 22.2 中找到,如行星事实表(nssdc.gsfc.nasa.gov/planetary/factsheet/)所提供。
快速检查 22.3
Q1:
与更面向对象的方法相比,在
world类型上声明距离方法有什么好处?
QC 22.3 答案
1:
它提供了一种干净的方式来计算不同世界的距离,并且不需要将体积平均半径传递给距离方法,因为它已经可以访问
w.radius。
摘要
-
结合方法和结构提供了经典语言提供的大部分功能,而不引入新的语言特性。
-
构造函数是普通函数。
让我们看看你是否掌握了这个...
实验:landing.go
使用 列表 22.1,22.2,和 22.3 中的代码编写一个程序,为 表 22.1 中的每个位置声明一个 location。以十进制度数打印出每个位置。
实验:distance.go
使用 列表 22.4 中的 distance 方法编写一个程序,确定 表 22.1 中每对着陆点之间的距离。
哪两个着陆点是最近的?
哪两个位置相距最远?
为了确定以下位置之间的距离,你需要根据 表 22.2 声明其他世界:
-
找出从英国伦敦(51°30’N 0°08’W)到法国巴黎(48°51’N 2°21’E)的距离。
-
查找您所在城市到您国家首都的距离。
-
查找火星上夏普山(5°4’ 48”S, 137°51’E)和奥林匹斯山(18°39’N, 226°12’E)之间的距离。
表 22.1. 火星着陆点
| 探测器或着陆器 | 着陆点 | 纬度 | 经度 |
|---|---|---|---|
| 精神号 | 哥伦比亚纪念站 | 14°34’6.2” S | 175°28’21.5” E |
| 机遇号 | 挑战者纪念站 | 1°56’46.3” S | 354°28’24.2” E |
| 好奇号 | 布拉德利着陆点 | 4°35’22.2” S | 137°26’30.1” E |
| 印象号 | 爱丽斯平原 | 4°30’0.0” N | 135°54’0” E |
表 22.2. 各种行星的体积平均半径
| 行星 | 半径(km) |
|---|---|
| 水星 | 2439.7 |
| 金星 | 6051.8 |
| 地球 | 6371.0 |
| 火星 | 3389.5 |
| 木星 | 69911 |
| 土星 | 58232 |
| 天王星 | 25362 |
| 海王星 | 24622 |
第 23 课。组合和转发
阅读第 23 课后,您将能够
-
使用组合组合结构
-
将方法转发到其他方法
-
忘记经典继承
当你环顾四周的世界时,你所看到的一切都是由更小的部分组成的。人们倾向于有带有四肢的身体,四肢又依次有手指或脚趾。花朵有花瓣和花茎。火星探测器有轮子和履带,以及整个子系统,如火星探测器环境监测站(REMS)。每个部分都扮演着它的角色。
在面向对象的编程世界中,对象以相同的方式由更小的对象组成。计算机科学家称之为 对象组合 或简单地称为 组合。
仓鼠使用结构组合,Go 提供了一种称为 嵌入 的特殊语言特性来转发方法。本课通过 REMS 的虚构天气报告演示了组合和嵌入。

考虑这一点
设计层次结构可能很困难。动物王国的层次结构试图将具有相同行为的动物分组。一些哺乳动物在陆地上行走,而另一些则游泳,但蓝鲸也会哺乳幼崽。如何组织它们?改变层次结构也可能很困难,因为即使是微小的变化也可能产生广泛的影响。
组合是一种更简单、更灵活的方法:实现行走、游泳、哺乳和其他行为,并将适当的与每种动物关联。
作为额外奖励,如果您设计一个机器人,行走行为可以被重用。
23.1. 组合结构
天气报告包括各种数据,例如最高和最低温度、当前日(日),以及位置。一个简单的解决方案是在单个 report 结构中定义所有必要的字段,如下面的列表所示。
列表 23.1. 没有组合:unorganized.go
type report struct {
sol int
high, low float64
lat, long float64
}
查看第 23.1 节中的列表 23.1,report 是由不同数据混合而成的。当报告增长到包括更多数据,例如风速和风向、气压、湿度、季节、日出和日落时,它就会变得难以操控。
幸运的是,你可以通过结构体和组合将相关字段分组在一起。以下列表定义了一个由温度和位置结构体组成的 report 结构体。
列表 23.2. 结构体内部的结构:compose.go
type report struct {
sol int
temperature temperature *1*
location location
}
type temperature struct {
high, low celsius
}
type location struct {
lat, long float64
}
type celsius float64
- 1 温度字段是温度类型的结构体。
定义了这些类型后,天气报告是通过以下方式构建的,即从位置和温度数据中构建:
bradbury := location{-4.5895, 137.4417}
t := temperature{high: -1.0, low: -78.0}
report := report{sol: 15, temperature: t,
location: bradbury}
fmt.Printf("%+v\n", report) *1*
fmt.Printf("a balmy %v° C\n", report.temperature.high) *2*
-
1 打印 {sol:15 temperature:{high:-1 low:-78} location:{lat:-4.5895 long:137.4417}}
-
2 打印温暖 -1° C
再看看 列表 23.2。注意,high 和 low 明确指的是温度,而 列表 23.1 中的相同字段则是不明确的。
通过将天气报告构建为较小类型,你可以通过在每个类型上挂载方法来进一步组织你的代码。例如,为了计算平均温度,你可以编写如下列表中所示的方法。
列表 23.3. 一种平均方法:average.go
func (t temperature) average() celsius {
return (t.high + t.low) / 2
}
温度类型和平均方法可以独立于天气报告使用,如下所示:
t := temperature{high: -1.0, low: -78.0}
fmt.Printf("average %v° C\n", t.average()) *1*
- 1 打印平均 -39.5° C
当你创建一个天气报告时,平均方法可以通过链式调用温度字段来访问:
report := report{sol: 15, temperature: t}
fmt.Printf("average %v° C\n", report.temperature.average()) *1*
- 1 打印平均 -39.5° C
如果你想要通过 report 类型直接暴露平均温度,就没有必要在 列表 23.3 中重复逻辑。写一个将前向到实际实现的方法:
func (r report) average() celsius {
return r.temperature.average()
}
使用从报告到温度的前向方法,你可以在围绕较小类型结构代码的同时方便地访问 report.average()。本节课的剩余部分将探讨一个承诺使方法前向无懈可击的 Go 特性。
快速检查 23.1
Q1:
将 列表 23.1 与 23.2 进行比较。你更喜欢哪种代码,为什么?
| |
QC 23.1 答案
1:
列表 23.2 中的结构体组织得更好,通过将温度和位置拆分到单独的可重用结构体中。
23.2. 前向方法
方法前向可以使使用方法更加方便。想象一下询问好奇号火星上的天气。它可以将你的请求 前向 到 REMS 系统,然后该系统再将你的请求前向到温度计以确定空气温度。有了前向,你不需要知道方法的路径——你只需询问好奇号。
不太方便的是手动编写像 列表 23.3 中的从一种类型到另一种类型的前向方法。这种重复的代码,称为 样板代码,除了增加混乱之外,没有任何帮助。
幸运的是,Go 会为你使用 结构体嵌入 进行方法前向。要在结构体中嵌入一个类型,指定类型而不指定字段名,如下面的列表所示。
列表 23.4. 结构体嵌入:embed.go
type report struct {
sol int
temperature *1*
location
}
- 1 报告中嵌入的温度类型
所有在 temperature 类型上的方法都会自动通过 report 类型变得可访问:
report := report{
sol: 15,
location: location{-4.5895, 137.4417},
temperature: temperature{high: -1.0, low: -78.0},
}
fmt.Printf("average %v° C\n", report.average()) *1*
- 1 打印平均 -39.5° C
虽然没有指定字段名,但仍然存在一个与嵌入类型同名字段。你可以如下访问 temperature 字段:
fmt.Printf("average %v° C\n", report.temperature.average()) *1*
- 1 打印平均 -39.5° C
嵌入不仅转发方法。内部结构的字段可以从外部结构体访问。除了 report.temperature.high,你还可以如下使用 report.high 访问高温:
fmt.Printf("%v° C\n", report.high) *1*
report.high = 32
fmt.Printf("%v° C\n", report.temperature.high) *2*
-
1 打印 -1° C
-
2 打印 32° C
正如你所见,report.high 字段的更改反映在 report.temperature.high 上。这只是访问相同数据的另一种方式。
你可以在结构体中嵌入任何类型,而不仅仅是结构体。在下面的列表中,sol 类型有一个基础类型为 int,但它就像 location 和 temperature 结构体一样被嵌入。
列表 23.5. 嵌入其他类型:sol.go
type sol int
type report struct {
sol
location
temperature
}
声明在 sol 类型上的任何方法都可以通过 sol 字段或通过 report 类型访问:
func (s sol) days(s2 sol) int {
days := int(s2 - s)
if days < 0 {
days = -days
}
return days
}
func main() {
report := report{sol: 15}
fmt.Println(report.sol.days(1446)) *1*
fmt.Println(report.days(1446)) *1*
}
- 1 打印 1431
快速检查 23.2
1
哪些类型可以被嵌入到结构体中?
2
report.lat是否有效?如果是,它在 列表 23.4 中引用的是哪个字段?
QC 23.2 答案
1
任何类型都可以被嵌入到结构体中。
2
是的,
report.lat等同于report.location.lat。
23.3. 名称冲突
天气报告工作正常。然后有人想知道一辆漫游车在两个位置之间旅行需要多少天。好奇号漫游车每天大约行驶 200 米,所以你向位置类型添加一个 days 方法来进行计算,如下面的列表所示。
列表 23.6. 同名方法:collision.go
func (l location) days(l2 location) int {
// To-do: complicated distance calculation *1*
return 5
}
- 1 见 课程 22。
report 结构体嵌入 sol 和 location,这两个类型都有一个名为 days 的方法。
好消息是,如果你的代码中没有使用 report 上的 days 方法,一切都会继续正常工作。Go 编译器足够智能,只有在有问题时才会指出名称冲突。
如果正在使用 report 类型上的 days 方法,Go 编译器不知道是否应该将调用转发到 sol 上的方法还是 location 上的方法,因此它会报告一个错误:
d := report.days(1446) *1*
- 1 模糊选择器 report.days
解决一个 模糊选择器 错误很简单。如果你在 report 类型上实现了 days 方法,它将优先于嵌入类型的 days 方法。你可以手动转发到你选择的嵌入类型,或者执行其他行为:
func (r report) days(s2 sol) int {
return r.sol.days(s2)}
这不是你想要的继承
类似于 C++、Java、PHP、Python、Ruby 和 Swift 这样的经典语言可以使用组合,但它们也提供了一种称为继承的语言特性。
继承是思考软件设计的一种不同方式。使用 继承,漫游车是一种车辆类型,因此 继承 了所有车辆共享的功能。使用组合,漫游车有一个引擎和轮子以及各种其他部件,这些部件提供了漫游车所需的功能。卡车可以重用其中的一些部件,但没有任何车辆类型或层次结构从它派生出来。
组合通常被认为比使用继承构建的软件更灵活,允许更大的重用和更易于更改。这也不是一个新发现——这种智慧在 1994 年就已经发表了:
优先使用对象组合而不是类继承。
四人帮,《设计模式:可重用面向对象软件元素》
当人们第一次看到嵌入时,有些人最初认为它与继承相同,但实际上并非如此。这不仅是一种不同的思考软件设计的方式,还存在一个微妙的技术差异。
在 列表 23.3 中的 average() 的接收者始终是 temperature 类型,即使通过 report 转发也是如此。使用 委托 或继承,接收者可以是 report 类型,但 Go 既没有委托也没有继承。不过,这没关系,因为继承不是必需的:
使用经典继承始终是可选的;它解决的问题都可以用其他方式解决。
Sandi Metz,《Ruby 实用面向对象设计》
Go 是一种独立的新语言,能够摆脱过时的范式,因此它做到了。
快速检查 23.3
Q1:
如果多个嵌入类型实现了同名方法,Go 编译器是否会报告错误?
QC 23.3 答案
1:
Go 编译器只有在方法被使用时才会报告错误。
摘要
-
组合是将大型结构分解成小型结构并将它们组合起来的技术。
-
嵌入允许外部结构访问内部结构的字段。
-
当你在结构中嵌入类型时,方法会自动转发。
-
Go 会通知你由嵌入引起的名称冲突,但只有当这些方法被使用时。
让我们看看你是否明白了……
实验:gps.go
编写一个用于全球定位系统(GPS)的 gps 结构的程序。这个 struct 应该由当前位置、目的地位置和世界组成。
为 location 类型实现一个 description 方法,该方法返回一个包含名称、纬度和经度的字符串。world 类型应使用来自 第 22 课 的数学实现距离方法。
将两个方法附加到 gps 类型。首先,附加一个 distance 方法,用于查找当前位置和目的地位置之间的距离。然后实现一个 message 方法,返回一个字符串,描述剩余多少公里到达目的地。
作为最后一步,创建一个嵌入 gps 的 rover 结构体,并编写一个 main 函数来测试一切。初始化一个火星上的 GPS,当前位置为布拉德伯里着陆点 (-4.5895, 137.4417),目的地为伊里斯平原 (4.5, 135.9)。然后创建一个 curiosity 探索车,并打印出它的 message(这会转发到 gps)。
第 24 课. 接口
在阅读了第 24 课之后,你将能够
-
让你的类型说话
-
在实践中发现接口
-
在标准库中探索接口
-
从火星入侵中拯救人类
笔和纸并不是你用来记录最新洞察的唯一工具。附近的蜡笔和餐巾纸也可以起到作用。蜡笔、永久性记号笔和机械铅笔都可以满足你在便签本上写提醒、在建设纸上写标语或在日记中写条目的需求。写作非常灵活。
Go 标准库有一个用于书写的接口,名为 Writer。有了它,你可以写入文本、图像、逗号分隔值(CSV)、压缩存档等等。你可以将内容写入屏幕、磁盘上的文件或对网络请求的响应。借助单个接口,Go 可以将任何数量的内容写入任何数量的地方。Writer 非常灵活。
一支 0.5 毫米的蓝色墨水圆珠笔是一个具体的东西,而一支书写工具是一个更模糊的概念。使用接口,代码可以表达抽象概念,例如一个会写字的东西。想想它能做什么,而不是它是什么。这种思维方式,通过接口表达,将有助于你的代码适应变化。
考虑这一点
你周围有哪些具体的东西?你能用它们做什么?你能用其他东西做同样的事情吗?它们有什么共同的行为或接口?
24.1. 接口类型
大多数类型都关注它们存储的值:整数用于整数,字符串用于文本,等等。接口类型不同。接口关注的是类型能做什么,而不是它持有的值。
方法表达了类型提供的行为,因此接口是用一组类型必须满足的方法声明的。以下列表声明了一个接口类型的变量。
列表 24.1. 一组方法:talk.go
var t interface {
talk() string
}
变量 t 可以持有任何满足接口的任何类型的任何值。更具体地说,如果类型声明了一个名为 talk 的方法,该方法不接受任何参数并返回一个字符串,则该类型将满足接口。
以下列表声明了两个满足这些要求的类型。
列表 24.2. 满足接口:talk.go
type martian struct{}
func (m martian) talk() string {
return "nack nack"
}
type laser int
func (l laser) talk() string {
return strings.Repeat("pew ", int(l))
}
虽然 martian 是一个没有字段的结构体,而 laser 是一个整数,但两种类型都提供了一个 talk 方法,因此可以将它们分配给 t,如下所示。
列表 24.3. 多态:talk.go
var t interface {
talk() string
}
t = martian{}
fmt.Println(t.talk()) *1*
t = laser(3)
fmt.Println(t.talk()) *2*
-
1 打印 nack nack
-
2 打印 pew pew pew
变形变量 t 能够变成 martian 或 laser 的形式。计算机科学家说,接口提供了 多态性,这意味着“多种形状”。

注意
与 Java 不同,在 Go 中 martian 和 laser 并没有明确声明它们实现了接口。这种做法的好处将在本课的后面部分介绍。
通常,接口被声明为可重用的命名类型。有一个命名接口类型的命名约定:以 -er 后缀命名:talker 是任何会说话的东西,如以下列表所示。
列表 24.4. talker 类型:shout.go
type talker interface {
talk() string
}
接口类型可以在其他类型可以使用的任何地方使用。例如,以下 shout 函数有一个类型为 talker 的参数。
列表 24.5. 大声说出所说的话:shout.go
func shout(t talker) {
louder := strings.ToUpper(t.talk())
fmt.Println(louder)
}
你可以使用满足 talker 接口的任何值与 shout 函数一起使用,无论是火星人还是激光,如以下列表所示。
列表 24.6. 大声喊叫:shout.go
shout(martian{}) *1*
shout(laser(2)) *2*
-
1 打印 NACK NACK
-
2 打印 PEW PEW
你传递给 shout 函数的参数必须满足 talker 接口。例如,crater 类型不满足 talker 接口,所以如果你期望碎石大声喊叫,Go 将拒绝编译你的程序:
type crater struct{}
shout(crater{}) *1*
- 1 碎石没有实现 talker 接口(缺少 talk 方法)
当你需要更改或扩展代码时,接口会展现出其灵活性。当你声明一个新的具有 talk 方法的类型时,shout 函数将与之配合工作。任何只依赖于接口的代码都可以保持不变,即使实现被添加和修改。
值得注意的是,接口可以与结构体嵌入一起使用,这是第 23 课中介绍的语言特性。例如,以下列表在 starship 中嵌入 laser。
列表 24.7. 嵌入满足接口:starship.go
type starship struct {
laser
}
s := starship{laser(3)}
fmt.Println(s.talk()) *1*
shout(s) *2*
-
1 打印 pew pew pew
-
2 打印 PEW PEW PEW
当一艘星际飞船说话时,激光负责说话。嵌入 laser 给 starship 一个将转发到 laser 的 talk 方法。现在星际飞船也满足 talker 接口,允许它与 shout 一起使用。
组合和接口结合使用是一种非常强大的设计工具。
比尔·文纳,JavaWorld(见 mng.bz/B5eg)
快速检查 24.1
1
修改 列表 24.4 中的激光的
talk方法,以防止火星人的枪支发射,从而拯救人类免受入侵。2
通过声明一个新的具有
talk方法并返回“whir whir”的rover类型来扩展 列表 24.4。使用您的新的类型与shout函数一起使用。
| |
QC 24.1 答案
1
func (l laser) talk() string { return strings.Repeat("toot ", int(l)) }2
type rover string func (r rover) talk() string { return string(r) } func main() { r := rover("whir whir") shout(r) *1* }
- 1 打印 WHIR WHIR
24.2. 发现接口
使用 Go 语言,你可以开始编写代码,并在编写过程中发现接口。任何代码都可以实现接口,即使是已经存在的代码。本节将带您通过一个示例进行说明。
以下列表从年份和小时数推导出一个虚构的 stardate。
列表 24.8. Stardate 计算:stardate.go
package main
import (
"fmt"
"time"
)
// stardate returns a fictional measure of time for a given date.
func stardate(t time.Time) float64 {
doy := float64(t.YearDay())
h := float64(t.Hour()) / 24.0
return 1000 + doy + h
}
func main() {
day := time.Date(2012, 8, 6, 5, 17, 0, 0, time.UTC)
fmt.Printf("%.1f Curiosity has landed\n", stardate(day)) *1*
}
- 1 打印 1219.2 Curiosity 已着陆
列表 24.8 中的 stardate 函数仅限于地球日期。为了解决这个问题,下一列表声明了一个 stardate 接口。
列表 24.9. Stardate 接口:stardater.go
type stardater interface {
YearDay() int
Hour() int
}
// stardate returns a fictional measure of time.
func stardate(t stardater) float64 {
doy := float64(t.YearDay())
h := float64(t.Hour()) / 24.0
return 1000 + doy + h
}
新的 stardate 函数在 列表 24.9 中继续使用地球日期,因为标准库中的 time.Time 类型满足 stardater 接口。Go 中的接口是隐式满足的,这在处理你未编写的代码时特别有帮助。
注意
在像 Java 这样的语言中,这是不可能的,因为 java.time 需要明确声明它 implements stardater。
在 stardater 接口就位的情况下,列表 24.9 可以通过添加一个满足接口的 sol 类型来扩展,该类型具有 YearDay 和 Hour 方法,如以下列表所示。
列表 24.10. Sol 实现:stardater.go
type sol int
func (s sol) YearDay() int {
return int(s % 668) *1*
}
func (s sol) Hour() int {
return 0 *2*
}
-
1 火星年中有 668 个火星日。
-
2 小时未知。
现在,stardate 函数同时操作地球日期和火星日,如下一列表所示。
列表 24.11. 使用中:stardater.go
day := time.Date(2012, 8, 6, 5, 17, 0, 0, time.UTC)
fmt.Printf("%.1f Curiosity has landed\n", stardate(day)) *1*
s := sol(1422)
fmt.Printf("%.1f Happy birthday\n", stardate(s)) *2*
-
1 打印 1219.2 Curiosity 已着陆
-
2 打印 1086.0 生日快乐
快速检查 24.2
Q1:
隐式满足的接口有哪些优势?
QC 24.2 答案
1:
你可以声明一个由你未编写的代码满足的接口,这提供了更多的灵活性。
24.3. 满足接口
标准库导出了一些单方法接口,你可以在自己的代码中实现它们。
Go 鼓励使用组合而非继承,使用简单、通常只有一个方法的接口...这些接口作为组件之间干净、可理解的边界。
Rob Pike, “Go at Google: Language Design in the Service of Software Engineering” (see talks.golang.org/2012/splash.article)
例如,fmt 包声明了一个 Stringer 接口如下:
type Stringer interface {
String() string
}
如果一个类型提供了一个 String 方法,Println、Sprintf 和其他方法将使用它。以下列表提供了一个 String 方法来控制 fmt 包如何显示位置。
列表 24.12. 满足 Stringer:stringer.go
package main
import "fmt"
// location with a latitude, longitude in decimal degrees.
type location struct {
lat, long float64
}
// String formats a location with latitude, longitude.
func (l location) String() string {
return fmt.Sprintf("%v, %v", l.lat, l.long)
}
func main() {
curiosity := location{-4.5895, 137.4417}
fmt.Println(curiosity) *1*
}
- 1 打印 -4.5895, 137.4417
除了 fmt.Stringer,标准库中流行的接口还包括 io.Reader、io.Writer 和 json.Marshaler。
小贴士
io.ReadWriter 接口提供了一个类似于 第 23 课 中的结构嵌入的接口嵌入示例。与结构不同,接口没有字段或附加方法,因此接口嵌入节省了一些打字,但几乎没有其他好处。
快速检查 24.3
Q1:
在
coordinate类型上编写一个String方法,并使用它以更可读的格式显示坐标。type coordinate struct { d, m, s float64 h rune }你的程序应该输出:
Elysium Planitia 位于北纬 4°30'0.0",东经 135°54'0.0"处
| |
QC 24.3 答案
1:
// String formats a DMS coordinate. func (c coordinate) String() string { return fmt.Sprintf("%v°%v'%.1f\" %c", c.d, c.m, c.s, c.h) } // location with a latitude, longitude in decimal degrees. type location struct { lat, long coordinate } // String formats a location with latitude, longitude. func (l location) String() string { return fmt.Sprintf("%v, %v", l.lat, l.long) } func main() { elysium := location{ lat: coordinate{4, 30, 0.0, 'N'}, long: coordinate{135, 54, 0.0, 'E'}, } fmt.Println("Elysium Planitia is at", elysium) *1* }
- 1 打印 Elysium Planitia 位于北纬 4°30’0.0” N,东经 135°54’0.0” E
24.4. 概述
-
接口类型通过一组方法指定所需的行为。
-
任何包中的新或现有代码都会隐式满足接口。
-
结构将满足嵌入类型满足的接口。
-
按照标准库的示例,努力保持接口小巧。
让我们看看你是否掌握了这个...
实验:marshal.go
编写一个程序,以 JSON 格式输出坐标,扩展先前的快速检查工作。JSON 输出应提供每个坐标的十进制度数(DD)以及度、分、秒格式:
{
"decimal": 135.9,
"dms": "135°54'0.0\" E",
"degrees": 135,
"minutes": 54,
"seconds": 0,
"hemisphere": "E"
}
这可以通过满足json.Marshaler接口来自定义 JSON,而不需要修改坐标结构。你编写的MarshalJSON方法可以使用json.Marshal。
注意
要计算十进制度数,你需要使用第 22 课中引入的decimal方法。
第 25 课:火星动物庇护所
在遥远的未来,人类可能能够在目前这个尘土飞扬的红星球上舒适地生活。火星距离太阳更远,因此温度更低。加热这个星球可能是改造火星气候和表面的第一步。一旦水流开始流动,植物开始生长,就可以引入生物。
可以在热带种植树木;可以引入昆虫和一些小型动物。人类仍然需要气罩来提供氧气并防止肺部二氧化碳含量过高。
莱纳德·大卫,《火星:我们在红色星球上的未来》
目前火星大气中大约有 96%的二氧化碳(参见en.wikipedia.org/wiki/Atmosphere_of_Mars)。改变这一点可能需要非常非常长的时间。火星将仍然是一个不同的世界。
现在是时候发挥你的想象力了。你认为如果一艘装满地球动物的方舟被引入到经过改造的火星上会发生什么?随着气候适应以支持生命,可能会出现哪些生命形式?

你的任务是创建火星上第一个动物庇护所的模拟。制作几种动物类型。每种动物都应该有一个名字,并遵循Stringer接口以返回它们的名称。
每种动物都应该有移动和进食的方法。移动方法应返回移动的描述。进食方法应返回动物喜欢的随机食物名称。
实现昼夜循环,并运行模拟三个 24 小时日(72 小时)。所有动物应从日落睡到日出。对于每天中的每一小时,随机选择一个动物执行一个随机动作(移动或进食)。对于每个动作,打印出动物所执行动作的描述。
你的实现应该使用结构和接口。
第 6 单元。向下进入地鼠洞
是时候动手实践,更深入地学习 Go 编程了。
你需要考虑内存的组织和共享方式,这将带来新的控制层次和责任。你会了解到 nil 可以是有益的,同时避免可怕的 nil 指针解引用。你还会看到在错误处理中表现出勤奋如何使你的程序更加可靠。
第 26 课。几个指针
阅读完 第 26 课 后,你将能够
-
声明和使用指针
-
理解指针和随机访问内存(RAM)之间的关系
-
知道何时使用——以及何时不使用——指针
在任何街区走一走,你很可能会遇到带有单独地址和街道标志的房屋,这些标志会指引你的方向。你可能会遇到一个关闭的商店,上面挂着道歉的标志:“抱歉,我们搬家了!”指针有点像商店橱窗里的标志,指示你前往不同的地址。

指针 是一个指向另一个变量地址的变量。在计算机科学中,指针是一种 间接引用,间接引用可以是一个强大的工具。
计算机科学中的所有问题都可以通过另一层间接引用来解决...
大卫·惠勒
指针非常有用,但多年来它们与大量的焦虑相关联。过去的语言——特别是 C 语言——对安全性的强调很少。许多崩溃和安全漏洞都可以追溯到指针的误用。这导致了几个不向程序员暴露指针的语言的产生。
Go 确实有指针,但强调内存安全。Go 没有像 悬垂指针 这样的问题。这就像去你最喜欢的商店的地址,却发现它意外地被一个新赌场的停车场取代了。
如果你之前遇到过指针,深呼吸。这不会那么糟糕。如果你是第一次遇到,放松。Go 是一个学习指针的安全场所。
考虑这一点
就像指示访客前往新地址的商店招牌一样,指针指示计算机在哪里查找值。还有什么其他情况你会被指示去别处寻找?
26.1. 和号和星号
Go 中的指针采用了 C 语言中广泛使用的语法。有两个符号需要注意,即和号(&)和星号(*),尽管星号具有双重作用,你很快就会看到。
地址运算符,由和号表示,确定变量在内存中的地址。变量将它们的值存储在计算机的 RAM 中,而存储值的地点被称为其 内存地址。以下列表以十六进制数字打印内存地址,尽管你的计算机上的地址会有所不同。
列表 26.1. 地址运算符:memory.go
answer := 42
fmt.Println(&answer) *1*
- 1 打印 0x1040c108
这是计算机存储 42 的内存位置。幸运的是,你可以使用变量名 answer 来检索值,而不是使用计算机使用的内存地址。
注意
你不能取一个字面量字符串、数字或布尔值的地址。Go 编译器会对 &42 或 &"another level of indirection" 报错。
地址运算符 (&) 提供了一个值的内存地址。相反的操作称为 解引用,它提供了内存地址所引用的值。以下列表通过在 address 变量前加一个星号 (*) 来解引用 address 变量。
列表 26.2. 解引用运算符:memory.go
answer := 42
fmt.Println(&answer) *1*
address := &answer
fmt.Println(*address) *2*
-
1 打印 0x1040c108
-
2 打印 42
在前面的列表和 图 26.1 中,address 变量持有 answer 的内存地址。它不持有答案(42),但它知道在哪里可以找到它。
注意
C 中的内存地址可以通过指针算术(例如 address++)进行操作,但 Go 禁止不安全操作。
图 26.1. address 指向 answer

快速检查 26.1
1
fmt.Println(*&answer)对于 列表 26.2 显示了什么?2
Go 编译器如何知道解引用和乘法的区别?
| |
QC 26.1 答案
1
它打印 42,因为内存地址 (
&) 被解引用 (*) 回到值。2
乘法是一个 中缀 运算符,需要两个值,而解引用则前缀一个单个变量。
26.1.1. 指针类型
指针存储内存地址。
在 列表 26.2 中的 address 变量是一个类型为 *int 的指针,如以下列表中的 %T 格式动词所揭示。
列表 26.3. 指针类型:type.go
answer := 42
address := &answer
fmt.Printf("address is a %T\n", address) *1*
- *1 打印地址是 int
*int 中的星号表示该类型是一个指针。在这种情况下,它可以指向其他类型为 int 的变量。
指针类型可以出现在任何使用类型的地方,包括变量声明、函数参数、返回类型、结构字段类型等。在以下列表中,声明 home 中的星号 (*) 表示它是一个指针类型。
列表 26.4. 声明指针:home.go
canada := "Canada"
var home *string
fmt.Printf("home is a %T\n", home) *1*
home = &canada
fmt.Println(*home) *2*
-
*1 打印 home 是 string
-
2 打印 Canada
提示
在类型前加一个星号表示指针类型,而在变量名前加一个星号用于解引用变量所指向的值。
前一个列表中的 home 变量可以指向任何类型为 string 的变量。然而,Go 编译器不允许 home 指向任何其他类型的变量,例如 int。
注意
C 的类型系统很容易相信一个内存地址持有不同的类型。这在某些时候可能很有用,但 Go 再次避免了潜在的不安全操作。
| |
快速检查 26.2
1
你将使用什么代码来声明一个名为
address的变量,该变量可以指向整数?2
你如何在代码列表 26.4 中区分指针类型的声明和指针解引用?
QC 26.2 答案
1
var address *int2
在类型前加一个星号表示指针类型,而将星号加在变量名前用于解引用变量指向的值。
26.2. 指针用于指向
Charles Bolden 于 2009 年 7 月 17 日成为美国国家航空航天局的管理员。他的前任是 Christopher Scolese。通过用指针表示管理员角色,以下列表可以将 administrator 指向任何担任该角色的人(参见图 26.2)。
列表 26.5. 美国国家航空航天局管理员:nasa.go
var administrator *string
scolese := "Christopher J. Scolese"
administrator = &scolese
fmt.Println(*administrator) *1*
bolden := "Charles F. Bolden"
administrator = &bolden
fmt.Println(*administrator) *2*
-
1 打印 Christopher J. Scolese
-
2 打印 Charles F. Bolden
图 26.2. administrator 指向 bolden

由于管理员变量指向 bolden 而不是存储副本,因此可以在一个地方修改 bolden 的值:
bolden = "Charles Frank Bolden Jr."
fmt.Println(*administrator) *1*
- 1 打印 Charles Frank Bolden Jr.
还可以通过解引用 administrator 来间接更改 bolden 的值:
*administrator = "Maj. Gen. Charles Frank Bolden Jr."
fmt.Println(bolden) *1*
- 1 打印 Maj. Gen. Charles Frank Bolden Jr.
将 major 分配给 administrator 将导致一个新的指针,它也指向 bolden 字符串(参见图 26.3):
major := administrator
*major = "Major General Charles Frank Bolden Jr."
fmt.Println(bolden) *1*
- 1 打印 Major General Charles Frank Bolden Jr.
图 26.3. administrator 和 major 指向 bolden

major 和 administrator 指针都持有相同的内存地址,因此它们是相等的:
fmt.Println(administrator == major) *1*
- 1 打印 true
Charles Bolden 于 2017 年 1 月 20 日被 Robert M. Lightfoot Jr. 接替。在此更改之后,administrator 和 major 不再指向相同的内存地址(参见图 26.4):
lightfoot := "Robert M. Lightfoot Jr."
administrator = &lightfoot
fmt.Println(administrator == major) *1*
- 1 打印 false
图 26.4. administrator 现在指向 lightfoot

将 major 的解引用值分配给另一个变量会创建一个字符串的副本。在创建副本后,对 bolden 的直接和间接修改都不会影响 charles 的值,反之亦然:
charles := *major
*major = "Charles Bolden"
fmt.Println(charles) *1*
fmt.Println(bolden) *2*
-
1 打印 Major General Charles Frank Bolden Jr.
-
2 打印 Charles Bolden
如果两个变量包含相同的字符串,它们被认为是相等的,就像以下代码中的 charles 和 bolden 一样。即使它们有不同的内存地址,这也是这种情况:
charles = "Charles Bolden"
fmt.Println(charles == bolden) *1*
fmt.Println(&charles == &bolden) *2*
-
1 打印 true
-
2 打印 false
在本节中,通过解引用 administrator 和 major 指针间接修改了 bolden 的值。这展示了指针可以做什么,尽管在这种情况下直接将值分配给 bolden 会更简单。
快速检查 26.3
1
在代码列表 26.5 中使用指针有什么好处?
2
描述语句
major := administrator和charles := *major的作用。
QC 26.3 答案
1
变更可以在一个地方进行,因为
administrator变量指向一个人而不是存储一个副本。2
变量
major是一个新的*string指针,它持有与administrator相同的内存地址,而charles是一个包含major所指向的值的副本的字符串。
26.2.1. 指向结构体
指针经常与结构体一起使用。因此,Go 语言的设计者选择为结构体的指针提供一些人体工程学上的便利。
与字符串和数字不同,复合字面量可以前缀地址运算符。在以下列表中,timmy变量持有指向person结构的内存地址。
列表 26.6. 人员结构:struct.go
type person struct {
name, superpower string
age int
}
timmy := &person{
name: "Timothy",
age: 10,
}
此外,访问结构体的字段时不需要解引用结构体。以下列表比编写(*timmy).superpower更可取。
列表 26.7. 复合字面量:struct.go
timmy.superpower = "flying"
fmt.Printf("%+v\n", timmy) *1*
- 1 打印 &{name:Timothy superpower:flying age:10}
快速检查 26.4
1
地址运算符的有效用途是什么?
- 文字字符串:
&"Timothy"- 文字整数:
&10- 复合字面量:
&person{name: "Timothy"}- 所有上述内容
2
timmy.superpower和(*timmy).superpower之间有什么区别?
| |
QC 26.4 答案
1
地址运算符对变量名和复合字面量有效,但对文字字符串或数字无效。
2
由于 Go 自动解引用字段,因此没有功能上的差异,但
timmy.superpower更容易阅读,因此更可取。
26.2.2. 指向数组
与结构体一样,数组的复合字面量可以前缀地址运算符(&)以创建指向数组的新的指针。数组也提供自动解引用,如下所示。
列表 26.8. 指向数组的指针:superpowers.go
superpowers := &[3]string{"flight", "invisibility", "super strength"}
fmt.Println(superpowers[0]) *1*
fmt.Println(superpowers[1:2]) *2*
-
1 打印 flight
-
2 打印 [invisibility]
在上一个列表中,数组在索引或切片时自动解引用。无需编写更繁琐的(*superpowers)[0]。
注意
与 C 语言不同,Go 中的数组和指针是完全独立的类型。
切片和映射的复合字面量也可以前缀地址运算符(&),但没有自动解引用。
快速检查 26.5
Q1:
除了
superpowers是数组的指针之外,还有其他写法可以写(*superpowers)[2:]吗?
| |
QC 26.5 答案
1:
由于数组自动解引用,
superpowers[2:]的写法相同。
26.3. 启用修改
指针用于在函数和方法边界之间启用修改。
26.3.1. 指针作为参数
在 Go 中,函数和方法参数是通过值传递的。这意味着函数始终操作传递参数的副本。当传递指针到函数时,函数接收传递参数的内存地址的副本。通过取消引用内存地址,函数可以修改指针指向的值。
在 列表 26.9 中,声明了一个参数类型为 *person 的 birthday 函数。这允许函数体取消引用指针并修改它指向的值。与 列表 26.7 一样,不需要显式取消引用 p 变量来访问 age 字段。下面列表中的语法比 (*p).age++ 更可取。
列表 26.9. 函数参数:birthday.go
type person struct {
name, superpower string
age int
}
func birthday(p *person) {
p.age++
}
birthday 函数要求调用者传递一个人的指针,如下面的列表所示。
列表 26.10. 函数参数:birthday.go
rebecca := person{
name: "Rebecca",
superpower: "imagination",
age: 14,
}
birthday(&rebecca)
fmt.Printf("%+v\n", rebecca) *1*
- 1 打印 {name:Rebecca superpower:imagination age:15}
快速检查 26.6
1
什么代码会返回 Timothy 11?请参阅 列表 26.6。
birthday(&timmy)birthday(timmy)birthday(*timmy)2
如果
birthday(p person)函数不使用指针,Rebecca 会是什么年龄?
QC 26.6 答案
1
timmy变量已经是指针,所以正确答案是 b.birthday(timmy)。2
如果
birthday不使用指针,Rebecca 将永远保持 14 岁。
26.3.2. 指针接收者
方法接收者类似于参数。在下一个列表中,birthday 方法使用指针作为接收者,这使得方法可以修改一个人的属性。这种行为就像 列表 26.9 中的 birthday 函数。
列表 26.11. 指针接收者:method.go
type person struct {
name string
age int
}
func (p *person) birthday() {
p.age++
}
在下面的列表中,声明一个指针并调用 birthday 方法会增加 Terry 的年龄。
列表 26.12. 使用指针的方法调用:method.go
terry := &person{
name: "Terry",
age: 15,
}
terry.birthday()
fmt.Printf("%+v\n", terry) *1*
- 1 打印 &{name:Terry age:16}
或者,下一个列表中的方法调用没有使用指针,但它仍然有效。Go 在使用点符号调用方法时,会自动确定变量的地址(&),因此不需要编写 (&nathan).birthday()。
列表 26.13. 无指针的方法调用:method.go
nathan := person{
name: "Nathan",
age: 17,
}
nathan.birthday()
fmt.Printf("%+v\n", nathan) *1*
- 1 打印 {name:Nathan age:18}
无论是否使用指针调用,列表 26.11 中声明的 birthday 方法都必须指定指针接收者——否则,age 不会增加。
结构体通常以指针的形式传递。对于 birthday 方法来说,修改一个人的属性而不是创建一个全新的个人是有意义的。然而,并非每个结构体都应该被修改。标准库在 time 包中提供了一个很好的例子。time.Time 类型的方法从不使用指针接收者,而是更喜欢返回一个新的时间,如下一个列表所示。毕竟,明天是新的日子。
列表 26.14. 新的一天:day.go
const layout = "Mon, Jan 2, 2006"
day := time.Now()
tomorrow := day.Add(24 * time.Hour)
fmt.Println(day.Format(layout)) *1*
fmt.Println(tomorrow.Format(layout)) *2*
-
1 打印 Tue, Nov 10, 2009
-
2 打印 Wed, Nov 11, 2009
小贴士
你应该始终如一地使用指针接收器。如果某些方法需要指针接收器,则使用该类型的所有方法的指针接收器(参见 golang.org/doc/faq#methods_on_values_or_pointers)。
| |
快速检查 26.7
Q1:
你如何知道
time.Time从不使用指针接收器?
| |
QC 26.7 答案
1:
列表 26.14 中的代码没有揭示
Add方法是否使用指针接收器,因为点符号在这两种情况下都是相同的。最好查看time.Time方法的文档(参见 golang.org/pkg/time/#Time)。
26.3.3. 内部指针
Go 提供了一个方便的功能,称为 内部指针,用于确定结构体内部字段的内存地址。以下列表中的 levelUp 函数修改了一个 stats 结构体,因此需要使用指针。
列表 26.15. levelUp 函数:interior.go
type stats struct {
level int
endurance, health int
}
func levelUp(s *stats) {
s.level++
s.endurance = 42 + (14 * s.level)
s.health = 5 * s.endurance
}
Go 中的地址操作符可以用来指向结构体中的一个字段,如下面的列表所示。
列表 26.16. 内部指针:interior.go
type character struct {
name string
stats stats
}
player := character{name: "Matthias"}
levelUp(&player.stats)
fmt.Printf("%+v\n", player.stats) *1*
- 1 打印 {level:1 endurance:56 health:280}
character 类型在结构定义中没有任何指针,但在需要时可以获取任何字段的内存地址。代码 &player.stats 提供了指向结构体内部的指针。
快速检查 26.8
Q1:
什么是内部指针?
| |
QC 26.8 答案
1:
指向结构体内部字段的指针。这是通过在结构体的字段上使用地址操作符来实现的,例如
&player.stats。
26.3.4. 修改数组
尽管切片通常比数组更受欢迎,但在不需要更改其长度的情况下,使用数组可能是合适的。第 16 课 中的棋盘就是这样的一个例子。以下列表演示了指针如何允许函数修改数组的元素。
列表 26.17. 重置棋盘:array.go
func reset(board *[8][8]rune) {
board[0][0] = 'r'
// ...
}
func main() {
var board [8][8]rune
reset(&board)
fmt.Printf("%c", board[0][0]) *1*
}
- 1 打印 r
在 第 20 课 中,对康威生命游戏的建议实现使用了切片,尽管世界的大小是固定的。有了指针,你可以重写生命游戏以使用数组。
快速检查 26.9
Q1:
在什么情况下使用数组的指针是合适的?
| |
QC 26.9 答案
1:
数组适用于具有固定维度的数据,例如棋盘。除非使用指针,否则在传递给函数或方法时数组会被复制,这允许修改。
26.4. 掩饰的指针
并非所有的修改都需要显式使用指针。Go 在一些内置集合的内部使用指针。
26.4.1. 地图是指针
课程 19 指出,在赋值或作为参数传递时,地图不会被复制。地图是伪装成指针的,所以指向地图是多余的。不要这样做:
func demolish(planets *map[string]string) *1*
- 1 不必要的指针
地图的键或值可以是指针类型,但很少有必要指向地图。
快速检查 26.10
Q1:
地图是指针吗?
| |
QC 26.10 答案
1:
是的,尽管从语法上看地图不像指针,但它们实际上是指针。无法使用非指针类型的地图。
26.4.2. 切片指向数组
课程 17 将切片描述为数组的窗口。为了指向数组中的一个元素,切片使用指针。
切片在内部表示为一个包含三个元素的结构的指针:一个指向数组的指针、切片的容量和长度。内部指针允许在将切片直接传递给函数或方法时修改底层数据。
只有在修改切片本身时(长度、容量或起始偏移量)显式指针才有用。在以下列表中,reclassify函数修改了planets切片的长度。如果reclassify没有利用指针,调用函数(main)将看不到这种变化。
列表 26.18. 修改切片:slice.go
func reclassify(planets *[]string) {
*planets = (*planets)[0:8]
}
func main() {
planets := []string{
"Mercury", "Venus", "Earth", "Mars",
"Jupiter", "Saturn", "Uranus", "Neptune",
"Pluto",
}
reclassify(&planets)
fmt.Println(planets) *1*
}
- 1 打印 [水星 金星 地球 火星 木星 土星 天王星 海王星]
与在列表 26.18 中修改传递的切片不同,一个更干净的方法是编写reclassify函数以返回一个新的切片。
快速检查 26.11
Q1:
想要修改接收到的数据的函数和方法需要哪种数据类型的指针?
| |
QC 26.11 答案
1:
结构体和数组。
26.5. 指针和接口
以下列表演示了martian和指向martian的指针都满足talker接口。
列表 26.19. 指针和接口:martian.go
type talker interface {
talk() string
}
func shout(t talker) {
louder := strings.ToUpper(t.talk())
fmt.Println(louder)
}
type martian struct{}
func (m martian) talk() string {
return "nack nack"
}
func main() {
shout(martian{}) *1*
shout(&martian{}) *1*
}
- 1 打印 NACK NACK
当方法使用指针接收者时,情况就不同了,如下面的列表所示。
列表 26.20. 指针和接口:interface.go
type laser int
func (l *laser) talk() string {
return strings.Repeat("pew ", int(*l))
}
func main() {
pew := laser(2)
shout(&pew) *1*
}
- 1 打印 PEW PEW
在前面的列表中,&pew是*laser类型,它满足shout所需的talker接口。但shout(pew)不工作,因为在这种情况下laser不满足接口。
快速检查 26.12
Q1:
指针何时满足接口?
| |
QC 26.12 答案
1:
值的指针满足该类型的非指针版本满足的所有接口。
26.6. 聪明地使用指针
指针可能很有用,但它们也增加了复杂性。当值可能从多个地方更改时,代码可能更难跟踪。
当有需要时使用指针,但不要过度使用。那些不暴露指针的编程语言通常在幕后使用它们,例如在组合几个对象的一个类时。使用 Go 语言,你可以决定何时使用指针,何时不使用它们。
快速检查 26.13
Q1:
为什么不应该过度使用指针?
| |
QC 26.13 答案
1:
不使用指针的代码可能更容易理解。
概述
-
指针存储内存地址。
-
地址运算符(
&)提供了变量的内存地址。 -
一个指针可以通过解引用(
*)来访问或修改它所指向的值。 -
指针是用前缀星号声明的类型,例如
*int。 -
使用指针在函数和方法边界之间修改值。
-
指针在结构和数组中最有用。
-
映射和切片在幕后使用指针。
-
内部指针可以指向结构体内部的字段,而无需将这些字段声明为指针。
-
当有需要时使用指针,但不要过度使用它们。
让我们看看你是否理解了...
实验:turtle.go
编写一个程序,使用海龟可以向上、向下、向左或向右移动。海龟应该存储一个(x, y)位置,其中正值向下和向右。使用方法来增加/减少适当的变量。一个main函数应该练习你编写的方法,并打印最终位置。
小贴士
方法接收者将需要使用指针来操作 x 和 y 值。
27 课.关于 nil 的诸多讨论
在阅读第 27 课之后,你将能够
-
用无物做些事情
-
理解 nil 带来的麻烦
-
看看 Go 如何改进 nil 的故事
词语nil是一个名词,意思是“无”或“零”。在 Go 编程语言中,nil是一个零值。回想一下单元 2,一个未赋值的整数将默认为 0。空字符串是字符串的零值,依此类推。一个没有指向任何地方的指针具有nil值。而且,nil标识符也是切片、映射和接口的零值。
许多编程语言都包含了 nil 的概念,尽管它们可能称之为 NULL、null 或 None。在 2009 年 Go 语言发布之前,语言设计者 Tony Hoare 做了一次题为“Null References: The Billion Dollar Mistake”的演讲。在他的演讲中(见mng.bz/dNzX),Hoare 声称自己在 1965 年发明了 null reference,并建议指向无意义的指针并不是他最聪明的想法之一。
注意
Tony Hoare 在 1978 年继续发明了通信顺序进程(CSP)。他的想法是 Go 语言并发的基础,也是单元 7 的主题。
在 Go 中,nil 相对友好,并且比过去的一些语言中要少见,但仍然有一些需要注意的注意事项。nil 也有一些意外的用途,这在弗朗西斯克·坎波伊在 2016 年 GopherCon 的演讲中提到过(见www.youtube.com/watch?v=ynoY2xz-F8s),为这个课程提供了灵感。
考虑这一点
考虑表示一个星座,其中每颗星星都包含指向其最近邻星星的指针。数学计算完成后,每颗星星都会指向某个地方,找到最近星星只需通过快速指针解引用即可。
但在所有计算完成之前,指针应该指向哪里呢?这就是 nil 派上用场的一个情况。nil 可以暂时替代最近的星星,直到情况明了。
还有什么情况中,一个指向空处的指针可能是有用的?
27.1. 空指针导致恐慌
如果一个指针没有指向任何地方,尝试解引用该指针将不会起作用,正如列表 27.1 所演示的那样。解引用空指针,程序将会崩溃。一般来说,人们都不喜欢崩溃的应用程序。
我称之为我的十亿美元的错误。
托尼·霍尔
列表 27.1. 空指针导致恐慌:panic.go
var nowhere *int
fmt.Println(nowhere) *1*
fmt.Println(*nowhere) *2*
-
1 打印
-
2 潜在恐慌:空指针解引用
避免恐慌相当直接。这是一个通过if语句防止空指针解引用的问题,如下面的列表所示。
列表 27.2. 防止恐慌:nopanic.go
var nowhere *int
if nowhere != nil {
fmt.Println(*nowhere)
}
公平地说,程序可能因为许多原因而崩溃,而不仅仅是空指针解引用。例如,除以零也会导致恐慌,补救措施是相似的。尽管如此,考虑到过去 50 年编写过的所有软件,意外的空指针解引用可能对用户和程序员来说都是相当昂贵的。nil 的存在确实给程序员带来了更多的决策。代码是否应该检查 nil,如果是的话,检查应该在何处进行?如果值是 nil,代码应该做什么?这一切是否使nil成为一个坏词?

没有必要捂住耳朵或完全避开 nil。实际上,nil 可以相当有用,本节课的剩余部分将证明这一点。此外,Go 中的 nil 指针比某些其他语言中的 null 指针要少见,而且有方法可以在适当的时候避免使用它们。
快速检查 27.1
Q1:
*string类型的零值是什么?
QC 27.1 答案
1:
指针的零值是
nil。
27.2. 保护你的方法
方法经常接收一个指向结构的指针,这意味着接收者可能是 nil,如下面的列表所示。无论是显式解引用指针(*p)还是通过访问结构体的字段(p.age)隐式解引用,nil 值都会导致恐慌。
列表 27.3. 空接收者:method.go
type person struct {
age int
}
func (p *person) birthday() {
p.age++ *1*
}
func main() {
var nobody *person
fmt.Println(nobody) *2*
nobody.birthday()
}
-
1 空指针解引用
-
2 打印
一个关键的观察结果是,当执行 p.age++ 行时,会引发 panic。删除该行,程序将运行。
注意
与在调用方法时会导致程序立即崩溃的 Java 中等效程序相比。
即使接收器具有 nil 值,Go 也会愉快地调用方法。nil 接收器的行为与 nil 参数没有区别。这意味着方法可以防止 nil 值,如下面的列表所示。
列表 27.4. 保护子句:guard.go
func (p *person) birthday() {
if p == nil {
return
}
p.age++
}
与之前列表中在 birthday 方法中检查 nil 接收器相比。
注意
在 Objective-C 中,在 nil 上调用方法不会崩溃,但不会调用该方法,而是返回零值。
您决定如何在 Go 中处理 nil。您的方法可以返回零值,或返回一个错误,或者让它崩溃。
快速检查 27.2
Q1:
如果
p是nil,访问字段(p.age)会做什么?
| |
QC 27.2 答案
1:
如果在字段访问之前没有检查
nil,则会引发 panic,导致程序崩溃。
27.3. nil 函数值
当一个变量被声明为函数类型时,其默认值为 nil。在下面的列表中,fn 具有函数类型,但没有分配给任何特定的函数。
列表 27.5. nil 的函数类型:fn.go
var fn func(a, b int) int
fmt.Println(fn == nil) *1*
- 1 打印 true
如果前面的列表调用 fn(1, 2),程序会因为 fn 没有分配给任何函数而引发 panic,即空指针解引用。
有可能检查函数值是否为 nil 并提供默认行为。在下一个列表中,sort.Slice 用于对字符串切片进行排序,其中包含一等 less 函数。如果为 less 参数传递 nil,则默认为按字母顺序排序的函数。
列表 27.6. 默认函数:sort.go
package main
import (
"fmt"
"sort"
)
func sortStrings(s []string, less func(i, j int) bool) {
if less == nil {
less = func(i, j int) bool { return s[i] < s[j] }
}
sort.Slice(s, less)
}
func main() {
food := []string{"onion", "carrot", "celery"}
sortStrings(food, nil)
fmt.Println(food) *1*
}
- 1 打印 [胡萝卜 青葱 洋葱]
快速检查 27.3
Q1:
在 列表 27.6 中编写一行代码以按从短到长的字符串对
food进行排序。
| |
QC 27.3 答案
1:
sortStrings(food, func(i, j int) bool { return len(food[i]) < len(food[j]) })
27.4. nil 切片
没有使用复合字面量或 make 内置函数声明的切片将具有 nil 值。幸运的是,range 关键字、len 内置函数和 append 内置函数都可以与 nil 切片一起使用,如下面的列表所示。
列表 27.7. 增长一个切片:slice.go
var soup []string
fmt.Println(soup == nil) *1*
for _, ingredient := range soup {
fmt.Println(ingredient)
}
fmt.Println(len(soup)) *2*
soup = append(soup, "onion", "carrot", "celery")
fmt.Println(soup) *3*
-
1 打印 true
-
2 打印 0
-
3 打印 [洋葱 胡萝卜 青葱]
空切片和 nil 切片不等价,但它们通常可以互换使用。下面的列表将 nil 传递给接受切片的函数,跳过了创建空切片的步骤。
列表 27.8. 从 nil 开始:mirepoix.go
func main() {
soup := mirepoix(nil)
fmt.Println(soup) *1*
}
func mirepoix(ingredients []string) []string {
return append(ingredients, "onion", "carrot", "celery")
}
- 1 打印 [洋葱 胡萝卜 青葱]
每次您编写接受切片的函数时,请确保 nil 切片具有与空切片相同的操作。
快速检查 27.4
Q1:
在空切片上执行哪些操作是安全的?
| |
QC 27.4 答案
1:
内置函数
len、cap和append可以安全地与空切片一起使用,range关键字也是如此。与空切片一样,直接访问空切片的元素(soup[0])将导致索引越界恐慌。
27.5. 空映射
与切片类似,未使用复合字面量或 make 内置函数声明的映射具有 nil 的值。映射可以在为空时读取,如以下列表所示,尽管向空映射写入将导致恐慌。
列表 27.9. 读取映射:map.go
var soup map[string]int
fmt.Println(soup == nil) *1*
measurement, ok := soup["onion"]
if ok {
fmt.Println(measurement)
}
for ingredient, measurement := range soup {
fmt.Println(ingredient, measurement)
}
- 1 打印 true
如果一个函数只从映射中读取,则可以传递 nil 而不是创建一个空映射。
快速检查 27.5
Q1:
对空映射执行什么操作会导致恐慌?
| |
QC 27.5 答案
1:
向空映射写入 (
soup["onion"] = 1) 将导致恐慌:向空映射的条目赋值。
27.6. 空接口
当变量声明为接口类型但没有赋值时,其零值是 nil。以下列表演示了接口类型和值都是 nil,并且变量与 nil 进行比较时相等。
列表 27.10. 空映射:interface.go
var v interface{}
fmt.Printf("%T %v %v\n", v, v, v == nil) *1*
- 1 打印
true
当一个具有接口类型的变量被赋予一个值时,接口内部指向该变量的类型和值。这导致了一个令人惊讶的行为,即空值不与 nil 相等。接口类型和值都需要是 nil,变量才等于 nil,如以下列表所示。
列表 27.11. Wat?:interface.go
var p *int
v = p
fmt.Printf("%T %v %v\n", v, v, v == nil) *1*
- *1 打印 int
false
%#v 格式动词是查看类型和值的简写,同时也揭示了变量包含 (*int)(nil) 而不是仅仅 <nil>,如 列表 27.12 所示。
列表 27.12. 检查 Go 表示:interface.go
fmt.Printf("%#v\n", v) *1*
- 1 打印 (int)(nil)*
为了避免在比较接口和空值时出现意外,最好显式使用 nil 标识符,而不是指向包含空值的变量。
快速检查 27.6
Q1:
当声明
var s fmt.Stringer时,s的值是什么?
| |
QC 27.6 答案
1:
值是
nil,因为fmt.Stringer是一个接口,接口的零值是nil。
27.7. 空值的替代方案
当一个值可以是空值时,可能会倾向于使用空值。例如,整数指针 (*int) 可以表示零和空值。指针旨在指向,所以仅仅为了提供一个空值而使用指针不一定是最优选择。
除了使用指针外,另一种选择是声明一个包含几个方法的简单结构。这需要更多的代码,但不需要指针或空值,如以下列表所示。
列表 27.13. 数字已设置:valid.go
type number struct {
value int
valid bool
}
func newNumber(v int) number {
return number{value: v, valid: true}
}
func (n number) String() string {
if !n.valid {
return "not set"
}
return fmt.Sprintf("%d", n.value)
}
func main() {
n := newNumber(42)
fmt.Println(n) *1*
e := number{}
fmt.Println(e) *2*
}
-
1 打印 42
-
2 打印 not set
快速检查 27.7
Q1:
采用列表 27.13 中方法的一些优点是什么?
| |
QC 27.7 答案
1:
它通过没有指针或
nil值完全避免了空指针解引用。valid布尔值有明确的目的,而nil的含义则不太明确。
摘要
-
空指针解引用会导致你的程序崩溃。
-
方法可以防止接收
nil值。 -
为传递给函数的参数提供默认行为。
-
一个
nil切片通常可以与空切片互换。 -
一个
nil映射可以从中读取但不能写入。 -
如果一个接口看起来是
nil,确保类型和值都是nil。 -
nil不是表示“无”的唯一方式。
看看你是否明白了...
实验:knights.go
一位骑士阻挡了亚瑟的道路。我们的英雄空手而归,用 nil 值表示 leftHand *item。实现一个 character 结构体,具有 pickup(i *item) 和 give(to *character) 等方法。然后使用你在本课程中学到的知识编写一个脚本,让亚瑟拿起一个物品并将其交给骑士,显示每个动作的适当描述。
课程 28。犯错误是人类的天性
阅读完课程 28 后,你将能够
-
编写文件和处理失败
-
用创意处理错误
-
创建并识别特定错误
-
保持冷静,继续前进
喊声响起。学生和老师从教室里出来,走向最近的出口,在集合点聚集。眼前没有危险,也没有东西着火。这是另一个例行的消防演习。每个人都为真正的紧急情况做好了更好的准备。
文件未找到,格式无效,服务器不可达。当出现问题时,软件会做什么?也许问题可以被解决,让操作像往常一样继续。也许最好的行动方案是安全退出,关闭门,或者作为最后的手段从四楼窗户冲出去。
制定计划很重要。考虑可能发生的错误,如何传达这些错误以及处理它们的步骤。Go 将错误处理放在首位,鼓励你思考失败以及如何处理它。就像第十次消防演习一样,错误处理有时可能感觉平淡无奇,但最终会导致可靠的软件。
本课程探讨了处理错误的一些方法,并深入探讨了错误是如何产生的。最后,它通过对比 Go 的错误处理风格与其他编程语言的风格来结束。
考虑这一点
在 18 世纪初,亚历山大·蒲柏创作了一首包含现在广为人知的短语的诗:“犯错误是人类的天性”。花点时间考虑这个短语以及它可能如何与计算机编程相关。
犯错误是人类的天性;宽恕是神圣的。
亚历山大·蒲柏,《批评论文:第二部分》
我们的观点是:每个人都会犯错误。系统会失败。错误总是会发生。错误不是异常的,因此你应该预期事情可能会出错。重要的是你选择如何响应。承认错误,不要忽视它们。努力解决问题并继续前进。
28.1. 处理错误
在过去的编程语言中,单一返回值的限制使得错误处理变得有些晦涩。函数会重载相同的返回值来指示错误或成功的值,或者需要一个侧通道来传递错误,例如全局的 errno 变量。更糟糕的是,从函数到函数传递错误的机制不一致。
如同在 第 12 课 中提到的,Go 有多个返回值。虽然这不是专门针对错误处理的,但多个返回值提供了一个简单且一致的机制来将错误返回给调用函数。如果一个函数可以返回错误,惯例是使用最后一个返回值来表示错误。调用者应在调用函数后立即检查是否发生了错误。如果没有错误发生,错误值将是 nil。
为了演示错误处理,代码示例 28.1 调用了 ReadDir 函数。如果发生错误,err 变量不会是 nil,导致程序打印错误并立即退出。传递给 os.Exit 的非零值通知操作系统发生了错误。
如果 ReadDir 成功,files 将被分配给一个 os.FileInfo 切片,提供指定路径上文件和目录的信息。在这种情况下,指定了一个点,表示当前目录。
列表 28.1. 文件:files.go
files, err := ioutil.ReadDir(".")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
for _, file := range files {
fmt.Println(file.Name())
}
注意
当发生错误时,通常不应依赖其他返回值。它们可能被设置为它们类型的零值,但某些函数可能返回部分数据或完全不同的内容。
如果你将 代码示例 28.1 在 Go Playground 中运行,它将输出目录列表:
dev
etc
tmp
usr
要列出不同目录的内容,可以将 代码示例 28.1 中的当前目录(".")替换为另一个目录的名称,例如 "etc"。列表可能包含文件和目录。你可以使用 file.IsDir() 来区分两者。
快速检查 28.1
1
修改 代码示例 28.1 以读取一个虚构的目录,例如
"unicorns"。会显示什么错误信息?2
如果你使用
ReadDir在一个文件上,例如"/etc/hosts",而不是一个目录,会显示什么错误信息?
| |
QC 28.1 答案
1
open unicorns: 没有该文件或目录
2
readdirent: 无效参数
28.2. 精美的错误处理
鼓励 Gophers 考虑并处理函数返回的任何错误。用于处理错误的代码量可能会迅速增加。幸运的是,有几种方法可以减少错误处理代码的数量,而不会牺牲可靠性。
一些函数执行方程、数据转换和其他逻辑,而无需返回错误。然后有一些函数与文件、数据库和服务器进行通信。通信可能很混乱,可能会失败。减少错误处理代码的一种策略是将程序中无错误的子集与固有的错误易发代码隔离开。
但对于返回错误的代码呢?我们无法移除错误,但我们可以努力简化错误处理代码。为了演示,我们将编写一个小程序,将以下 Go 谚语写入文件,并改进错误处理,直到代码变得可接受。
错误是值。
不要只是检查错误,要优雅地处理它们。
不要恐慌。
使零值有用。
接口越大,抽象越弱。
interface{}没有说任何事情。Gofmt 的风格不是每个人的最爱,但
gofmt是每个人的最爱。文档是为用户准备的。
一点复制比一点依赖更好。
清晰比聪明好。
并发不是并行。
不要通过共享内存进行通信,而要通过通信共享内存。
通道进行编排;互斥锁进行序列化。
罗布·派克,Go 谚语(见 go-proverbs.github.io)
28.2.1. 写入文件
在写入文件时可能会出现许多问题。如果路径无效或存在权限问题,在开始写入之前创建文件可能会失败。一旦开始写入,设备可能会耗尽磁盘空间或被拔掉。此外,完成时必须关闭文件,以确保它成功刷新到磁盘,并避免资源泄漏。
注意
操作系统一次只能打开有限数量的文件,因此每个打开的文件都会减少这个限制。当文件意外地被留下打开时,这种资源的浪费是泄漏的一个例子。
列表 28.2 中的主函数调用 proverbs 创建文件,并通过显示错误并退出处理任何错误。不同的实现可以以不同的方式处理错误,例如提示用户输入不同的路径和文件名。尽管 proverbs 函数可以编写为在错误时退出,但让调用者决定如何处理错误是有用的。
列表 28.2. 调用 proverbs:proverbs.go
err := proverbs("proverbs.txt")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
proverbs 函数可能会返回一个错误,这是一个用于错误的特殊内置类型。该函数尝试创建一个文件。如果在这一点上发生错误,就没有必要关闭文件,因此它立即中止。函数的其余部分将行写入文件,并确保无论成功与否,文件都会被关闭,如下面的列表所示。
列表 28.3. 编写 Go 谚语:proverbs.go
func proverbs(name string) error {
f, err := os.Create(name)
if err != nil {
return err
}
_, err = fmt.Fprintln(f, "Errors are values.")
if err != nil {
f.Close()
return err
}
_, err = fmt.Fprintln(f, "Don't just check errors, handle them gracefully.")
f.Close()
return err
}
之前的列表中有相当多的错误处理代码——如此之多,以至于写出所有的 Go Proverbs 可能会变得相当乏味。
从积极的一面来看,处理错误的代码始终是缩进的,这使得在不需要阅读所有重复的错误处理的情况下扫描代码变得更容易。以这种方式缩进错误是 Go 社区中的一种常见模式,但我们可以在这种实现上做得更好。
快速检查 28.2
Q1:
为什么函数应该返回错误而不是退出程序?
| |
QC 28.2 答案
1:
返回错误给调用者一个决定如何处理错误的机会。例如,程序可能决定重试而不是退出。
28.2.2. defer 关键字
为了确保文件正确关闭,您可以使用 defer 关键字。Go 确保所有延迟操作都在包含函数返回之前执行。在下面的列表中,每个跟随 defer 的返回语句都将导致调用 f.Close() 方法。
列表 28.4. defer 清理:defer.go
func proverbs(name string) error {
f, err := os.Create(name)
if err != nil {
return err
}
defer f.Close()
_, err = fmt.Fprintln(f, "Errors are values.")
if err != nil {
return err
}
_, err = fmt.Fprintln(f, "Don't just check errors, handle them gracefully.")
return err
}
注意
之前列表的行为与列表 28.3 的行为相同。在不改变行为的情况下更改代码称为 重构。就像润色论文的第一稿一样,重构是编写更好代码的重要技能。
您可以推迟任何函数或方法,就像多重返回值一样,defer 并非专门用于错误处理。但它通过减少总是需要记住清理的负担来改进错误处理。多亏了 defer,处理错误的代码可以专注于当前错误,而无需关注其他任何事情。
defer 关键字使事情变得更好一些,但在每行写入后检查错误仍然很痛苦。是时候发挥创意了!
快速检查 28.3
Q1:
延迟操作将在何时被调用?
| |
QC 28.3 答案
1:
当从函数返回时将调用
Defer。
28.2.3. 创造性的错误处理
2015 年 1 月,一篇关于错误处理的精彩文章在 Go 博客上发布(blog.golang.org/errors-are-values)。文章描述了一种简单的方法,可以在不重复每行后的相同错误处理代码的情况下写入文件。
要应用此技术,您需要声明一个新的类型,我们在列表 28.5 中将其称为 safeWriter。如果 safeWriter 在写入文件时发生错误,它将存储错误而不是返回它。如果 writeln 发现之前已发生错误,后续尝试写入同一文件将被跳过。
列表 28.5. 存储错误值:writer.go
type safeWriter struct {
w io.Writer
err error *1*
}
func (sw *safeWriter) writeln(s string) {
if sw.err != nil {
return *2*
}
_, sw.err = fmt.Fprintln(sw.w, s) *3*
}
-
1 存储第一个错误的地方
-
2 如果之前发生错误,则跳过写入
-
3 写入一行并存储任何错误
使用 safeWriter,以下列表在不重复错误处理的情况下写入多行,但仍会返回发生的任何错误。
列表 28.6. 通往谚语的途径:writer.go
func proverbs(name string) error {
f, err := os.Create(name)
if err != nil {
return err
}
defer f.Close()
sw := safeWriter{w: f}
sw.writeln("Errors are values.")
sw.writeln("Don't just check errors, handle them gracefully.")
sw.writeln("Don't panic.")
sw.writeln("Make the zero value useful.")
sw.writeln("The bigger the interface, the weaker the abstraction.")
sw.writeln("interface{} says nothing.")
sw.writeln("Gofmt's style is no one's favorite, yet gofmt is everyone's
favorite.")
sw.writeln("Documentation is for users.")
sw.writeln("A little copying is better than a little dependency.")
sw.writeln("Clear is better than clever.")
sw.writeln("Concurrency is not parallelism.")
sw.writeln("Don't communicate by sharing memory, share memory by
communicating.")
sw.writeln("Channels orchestrate; mutexes serialize.")
return sw.err *1*
}
- 1 如果发生错误则返回错误
这是一种更干净地写入文本文件的方法,但这不是重点。相同的技巧可以应用于创建 zip 文件或执行完全不同的任务,而大思想甚至比单一技巧还要重要:
...错误是值,Go 编程语言的全部力量都可以用于处理它们。
Rob Pike, “Errors are values” (see blog.golang.org/errors-are-values)
精美的错误处理尽在你的掌握之中。
快速检查 28.4
Q1:
如果在 列表 28.6 写入“清晰胜过聪明”到文件时发生错误,接下来会有一系列什么事件发生?
| |
QC 28.4 答案
1:
- 错误存储在
sw结构中。writeln函数将被调用三次,但它将看到存储的错误,而不会尝试写入文件。- 存储的错误将被返回,并且
defer将尝试关闭文件。
28.3. 新的错误
如果一个函数接收到的参数不正确,或者发生其他错误,你可以创建并返回新的错误值来通知调用者问题。
为了演示新的错误,列表 28.7 为 Sudoku 逻辑谜题的构建奠定了基础,该谜题在一个 9 × 9 的网格上进行。网格上的每个方格可能包含从 1 到 9 的数字。此实现将使用固定大小的数组,数字零将表示空方格。
列表 28.7. Sudoku 网格:sudoku1.go
const rows, columns = 9, 9
// Grid is a Sudoku grid
type Grid [rows][columns]int8
包含错误构造函数的 errors 包(见 [golang.org/pkg/errors/](http://golang.org/pkg/errors/))可以接受一个字符串作为错误消息。使用它,列表 28.8 中的 Set 方法可以创建并返回一个“越界”错误。
提示
在方法开始时验证参数可以保护方法的其他部分免受坏输入的担忧。
列表 28.8. 验证参数:sudoku1.go
func (g *Grid) Set(row, column int, digit int8) error {
if !inBounds(row, column) {
return errors.New("out of bounds")
}
g[row][column] = digit
return nil
}
下一个列表中的 inBounds 函数确保 row 和 column 在网格边界内。它防止 Set 方法被细节拖累。
列表 28.9. 辅助函数:sudoku1.go
func inBounds(row, column int) bool {
if row < 0 || row >= rows {
return false
}
if column < 0 || column >= columns {
return false
}
return true
}
最后,下一个列表中的 main 函数创建一个网格并显示任何由于无效放置而产生的错误。
列表 28.10. 设置数字:sudoku1.go
func main() {
var g Grid
err := g.Set(10, 0, 5)
if err != nil {
fmt.Printf("An error occurred: %v.\n", err)
os.Exit(1)
}
}
提示
在错误消息中使用部分句子是常见的,这样在显示之前可以添加额外的文本来增强消息。
总是花时间编写有信息量的错误消息。将错误消息视为您程序的用户界面的一部分,无论是面向最终用户还是其他软件开发人员。短语“越界”是可以的,但“超出网格边界”可能更好。像“错误 37”这样的消息几乎没有任何帮助。
快速检查 28.5
Q1:
在函数开始时防止不良输入有什么好处?
| |
QC 28.5 答案
1:
函数的其余部分不需要考虑不良输入,因为它已经进行了检查。与其让它失败(例如,“运行时错误:索引越界”)不如返回一个友好的消息。
28.3.1. 哪个错误是哪个
许多 Go 包声明并导出它们可能返回的错误变量。要将此应用于数独网格,下面的列表在包级别声明了两个错误变量。
列表 28.11. 声明错误变量:sudoku2.go
var (
ErrBounds = errors.New("out of bounds")
ErrDigit = errors.New("invalid digit")
)
注意
按照惯例,错误消息被分配给以单词 Err 开头的变量。
声明 ErrBounds 后,你可以修改 Set 方法,使其返回它而不是创建一个新的错误,如下面的列表所示。
列表 28.12. 返回错误:sudoku2.go
if !inBounds(row, column) {
return ErrBounds
}
如果 Set 方法返回错误,调用者可以区分可能的错误,并针对特定错误进行不同的处理,如下面的列表所示。您可以使用 == 或 switch 语句将返回的错误与错误变量进行比较。
列表 28.13. 主函数中的哪个错误:sudoku2.go
var g Grid
err := g.Set(0, 0, 15)
if err != nil {
switch err {
case ErrBounds, ErrDigit:
fmt.Println("Les erreurs de paramètres hors limites.")
default:
fmt.Println(err)
}
os.Exit(1)
}
注意
errors.New 构造函数使用指针实现,所以前面列表中的 switch 语句是在比较内存地址,而不是错误消息中的文本。
| |
快速检查 28.6
Q1:
编写一个
validDigit函数,并使用它来确保Set方法只接受 1 到 9 之间的数字。
| |
QC 28.6 答案
1:
func validDigit(digit int8) bool { return digit >= 1 && digit <= 9 }
Set方法应包含此附加检查:if !validDigit(digit) { return ErrDigit }
28.3.2. 自定义错误类型
虽然 errors.New 很有帮助,但有时我们希望用比简单消息更复杂的内容来表示错误。Go 给你这样的自由。
error 类型是一个内置接口,如下面的列表所示。任何实现了返回字符串的 Error() 方法的类型都将隐式满足错误接口。作为接口,您可以创建新的错误类型。
列表 28.14. error 接口
type error interface {
Error() string
}
多个错误
在数独中,一个数字不能放置在特定位置可能有几个原因。前面的部分确立了两个规则:行和列必须在网格内,数字必须在 1 到 9 之间。如果调用者传递多个无效参数怎么办?
而不是一次返回一个错误,Set 方法可以执行多个验证,并一次性返回所有错误。在 列表 28.15 中的 SudokuError 类型是一个 error 切片。它通过一个将多个错误连接成一个字符串的方法满足 error 接口。
注意
按照惯例,像 SudokuError 这样的自定义错误类型以单词 Error 结尾。有时它们只是单词 Error,例如 url 包中的 url.Error。
列表 28.15. 自定义 error 类型:sudoku3.go
type SudokuError []error
// Error returns one or more errors separated by commas.
func (se SudokuError) Error() string {
var s []string
for _, err := range se {
s = append(s, err.Error()) *1*
}
return strings.Join(s, ", ")
}
- 1 将错误转换为字符串
为了使用 SudokuError,Set 方法可以被修改以验证边界和数字,同时返回两个错误,如下面的列表所示。
列表 28.16. 添加错误:sudoku3.go
func (g *Grid) Set(row, column int, digit int8) error { *1*
var errs SudokuError
if !inBounds(row, column) {
errs = append(errs, ErrBounds)
}
if !validDigit(digit) {
errs = append(errs, ErrDigit)
}
if len(errs) > 0 {
return errs
}
g[row][column] = digit
return nil *2*
}
-
1 返回类型是 error
-
2 返回 nil
如果没有错误发生,Set 方法返回 nil。这并没有从 列表 28.8 中改变,但重要的是要强调它不会返回一个空的 errs 切片。如果你不确定为什么,请回顾前一个课程中的 nil 接口。
Set 方法的签名也没有从 列表 28.8 中改变。在返回错误时,始终使用 error 接口类型,而不是像 SudokuError 这样的具体类型。
快速检查 28.7
Q1:
如果
Set方法在成功时返回一个空的errs切片会发生什么?
| |
QC 28.7 答案
1:
返回的
error接口不会是nil。即使错误切片为空,调用者也会认为发生了错误。
类型断言
因为 列表 28.16 在返回之前将 SudokuError 转换为 error 接口类型,你可能想知道如何访问单个错误。答案是使用 类型断言。使用类型断言,你可以将接口转换为底层具体类型。
在 列表 28.17 中的类型断言断言 err 是 SudokuError 类型,代码为 err.(SudokuError)。如果是,ok 将为真,errs 将是 SudokuError,从而可以访问此情况下的错误切片。记住,附加到 SudokuError 的单个错误是 ErrBounds 和 ErrDigit 变量,如果需要,可以进行比较。
列表 28.17. 类型断言:sudoku3.go
var g Grid
err := g.Set(10, 0, 15)
if err != nil {
if errs, ok := err.(SudokuError); ok {
fmt.Printf("%d error(s) occurred:\n", len(errs))
for _, e := range errs {
fmt.Printf("- %v\n", e)
}
}
os.Exit(1)
}
前面的列表将输出以下错误:
2 error(s) occurred:
- out of bounds
- invalid digit
注意
如果一个类型满足多个接口,类型断言也可以从一个接口转换为另一个接口。
| |
快速检查 28.8
Q1:
类型断言
err.(SudokuError)做什么?
| |
QC 28.8 答案
1:
它尝试将
err值从error接口类型转换为具体的SudokuError类型。
28.4. 不要恐慌
几种语言严重依赖异常来传递和处理错误。Go 没有异常,但它确实有一个类似的机制,称为panic。当发生panic时,程序将崩溃,就像其他语言中未处理的异常一样。

28.4.1. 其他语言中的异常
异常在行为和实现上都与 Go 的错误值有显著差异。
如果一个函数抛出异常而没有人来捕获它,异常将向上冒泡到调用函数,然后是那个函数的调用者,依此类推,直到达到调用栈的顶部(例如,main函数)。
异常是一种可以被认为是可选的错误处理方式。通常,不处理异常不需要任何代码,而选择异常处理可能需要相当多的专用代码。这是因为异常通常使用特殊的关键字,如try、catch、throw、finally、raise、rescue、except等,而不是使用现有的语言特性。
Go 中的错误值提供了一个简单、灵活的替代方案,可以用来构建可靠的软件。在 Go 中忽略错误值是一个有意识的决策,对阅读代码的人来说是显而易见的。
快速检查 28.9
Q1:
与异常相比,Go 的错误值有哪些两个优点?
QC 28.9 答案
1:
Go 鼓励开发者考虑错误,这可以导致更可靠的软件,而异常通常默认被忽略。错误值不需要特定的关键字,这使得它们更简单,同时也更灵活。
28.4.2. 如何使用 panic
如前所述,Go 确实有一个类似于异常的机制:panic。在其他语言中,数独中的无效数字可能引起异常,但在 Go 中,panic是罕见的。
如果世界即将结束,而你又忘记了在地球上信任的毛巾,那么或许panic是合理的。传递给panic的参数可以是任何类型,而不仅仅是像这里显示的字符串:
panic("I forgot my towel")
注意
尽管错误值通常比panic更可取,但panic通常比os.Exit更好,因为panic会运行任何延迟执行的函数,而os.Exit则不会。
在某些情况下,Go 会抛出panic而不是提供错误值,例如在除以零时:
var zero int
_ = 42 / zero *1*
- 1 运行时错误:整数除以零
快速检查 28.10
Q1:
你的程序应该在什么情况下 panic?
QC 28.10 答案
1:
panic 应该很少发生。
28.4.3. 保持冷静,继续前进
为了防止panic导致程序崩溃,Go 提供了recover函数,如代码列表 28.18 所示。
延迟函数在函数返回之前执行,即使在 panic 的情况下也是如此。如果一个延迟函数调用了recover,panic 将会停止,程序将继续运行。因此,recover在其他语言中的catch、except和rescue具有类似的作用。
列表 28.18. 保持冷静,继续前进:panic.go
defer func() {
if e := recover(); e != nil { *1*
fmt.Println(e) *2*
}
}()
panic("I forgot my towel") *3*
-
1 从 panic 中恢复
-
2 我忘了我的毛巾
-
3 引起 panic
注意
前面的列表使用了一个匿名函数,这是在第 14 课中介绍的主题。
快速检查 28.11
Q1:
recover内置函数在哪里可以使用?
QC 28.11 答案
1:
只有延迟函数才能使用
recover。
摘要
-
错误是与多个返回值和 Go 语言的其他部分交互的值。
-
如果你愿意发挥创意,处理错误有很大的灵活性。
-
通过满足
error接口,可以实现自定义错误类型。 -
defer关键字有助于在函数返回之前进行清理。 -
类型断言可以将接口转换为具体类型或另一个接口。
-
不要 panic——返回一个错误代替。
让我们看看你是否掌握了这个...
实验:url.go
在 Go 标准库中,有一个用于解析网络地址的函数(见golang.org/pkg/net/url/#Parse)。当使用无效的网络地址(例如包含空格的地址)时,显示url.Parse发生的错误:https://a b.com/。
使用Printf的%#v格式说明符来了解更多关于错误的信息。然后执行一个*url.Error类型断言来访问并打印底层结构的字段。
注意
一个 URL,或统一资源定位符,是万维网上一个页面的地址。
课 29. 顶峰:数独规则
数独是一种逻辑谜题,发生在 9 × 9 的网格上(见en.wikipedia.org/wiki/Sudoku)。每个方格可以包含从 1 到 9 的数字。数字零表示空方格。
网格被分成九个 3 × 3 的子区域。放置数字时,必须遵守某些约束。放置的数字不能出现在以下任何一种情况中:
-
它放置的水平行
-
它放置的垂直列
-
它放置的 3 × 3 子区域

使用固定大小(9 × 9)的数组来存储数独网格。如果一个函数或方法需要修改数组,请记住你需要传递一个指向数组的指针。
实现一个方法来在特定位置设置数字。如果放置数字违反了规则之一,该方法应返回一个错误。
还实现一个方法来清除一个方格中的数字。这个方法不需要遵守这些约束,因为可能有多个方格是空的(零)。
数独谜题开始时已经有一些数字被设置好了。编写一个构造函数来准备数独谜题,并使用复合字面量来指定初始值。以下是一个示例:
s := NewSudoku([rows][columns]int8{
{5, 3, 0, 0, 7, 0, 0, 0, 0},
{6, 0, 0, 1, 9, 5, 0, 0, 0},
{0, 9, 8, 0, 0, 0, 0, 6, 0},
{8, 0, 0, 0, 6, 0, 0, 0, 3},
{4, 0, 0, 8, 0, 3, 0, 0, 1},
{7, 0, 0, 0, 2, 0, 0, 0, 6},
{0, 6, 0, 0, 0, 0, 2, 8, 0},
{0, 0, 0, 4, 1, 9, 0, 0, 5},
{0, 0, 0, 0, 8, 0, 0, 7, 9},
})
起始数字固定在位置上,不能被覆盖或清除。修改你的程序,使其能够识别哪些数字是固定的,哪些是铅笔标记的。添加一个验证,使得对于任何固定的数字,设置和清除操作都会返回错误。初始为零的数字可以被设置、覆盖和清除。
你不需要为这个练习编写数独求解器,但一定要确保所有规则都正确实现。
单元 7. 并发编程
计算机擅长同时做很多事情。你可能想让计算机加快计算速度,同时下载许多网页,或者独立控制机器的不同部分。这种一次处理几件事情的能力被称为 并发。
Go 对并发的方法与其他大多数编程语言不同。任何 Go 代码都可以通过在 goroutine 中启动它来使其并发。Goroutines 使用 channels 进行通信和协调,使得多个并发任务朝着同一目标工作变得简单直接。
第 30 课. Goroutines 和并发
阅读完 第 30 课 后,你将能够
-
启动 goroutine
-
使用通道进行通信
-
理解通道管道
看,这是一个地鼠工厂!所有的地鼠都在忙着建造东西。嗯,几乎都在。角落里有一个正在睡觉的地鼠——或者也许他正在深思。这里有一个重要的地鼠:她在向其他地鼠下达命令。他们四处跑来跑去,执行她的命令,告诉别人该做什么,并最终向她报告他们的发现。一些地鼠正在从工厂发送东西。其他地鼠正在接收从外面送来的东西。

到目前为止,我们编写的所有 Go 代码都像这个工厂里单独一只地鼠一样,忙于自己的任务,不去打扰别人。Go 程序更像是整个工厂,许多独立任务都在各自做自己的事情,但为了某个共同目标相互沟通。这些 并发 任务可能包括从网络服务器获取数据、计算数百万位的 π 或控制机械臂。
在 Go 中,独立运行的任务被称为 goroutine。在本课中,你将学习如何启动任意数量的 goroutine,并通过 channels 之间进行通信。Goroutines 在其他语言中类似于 coroutines、fibers、processes 或 threads,尽管它们并不完全相同。它们创建起来非常高效,Go 使得协调许多并发操作变得简单。
考虑这一点
考虑编写一个执行一系列操作的程序。每个操作可能需要很长时间,并且可能需要在完成之前等待某些事情发生。它可以写成简单的、顺序的代码。但如果你想在同一时间做两个或更多的这些序列呢?
例如,你可能想让程序的一部分遍历电子邮件地址列表,并为每个地址发送一封电子邮件,而另一个任务则等待接收到的电子邮件并将它们存储在数据库中。你会如何编写这样的代码?
在某些语言中,你可能需要相当大幅度地更改代码。但在 Go 中,你可以为每个独立任务使用完全相同的代码。Goroutines 允许你同时运行任意数量的操作。
30.1. 启动 goroutine
启动一个 goroutine 就像调用一个函数一样简单。你只需要在调用前加上go关键字。
列表 30.1 中的 goroutine 类似于工厂角落的睡眠刺猬。尽管如此,他在Sleep语句那里并没有做什么,他本可以做一些严肃的思考(计算)。当main函数返回时,程序中的所有 goroutine 都会立即停止,因此我们需要等待足够长的时间来看到睡眠刺猬打印他的“...打呼噜...”信息。我们将等待比必要的时间更长一点,只是为了确保。

列表 30.1. 睡眠的刺猬:sleepygopher.go
package main
import (
"fmt"
"time"
)
func main() {
go sleepyGopher() *1*
time.Sleep(4 * time.Second) *2*
} *3*
func sleepyGopher() {
time.Sleep(3 * time.Second) *4*
fmt.Println("... snore ...")
}
-
1 goroutine 启动了。
-
2 等待刺猬打呼噜
-
3 当我们到达这里时,所有的 goroutine 都停止了。
-
4 刺猬在睡觉。
快速检查 30.1
1
如果你想在 Go 中同时做更多的事情,你会使用什么?
2
用于启动一个独立运行任务的关键字是什么?
| |
快速检查 30.1 答案
1
一个 goroutine。
2
go。
30.2. 多个 goroutine
每次我们使用go关键字时,就会启动一个新的 goroutine。所有 goroutine 看起来都是同时运行的。尽管如此,它们可能并不技术上同时运行,因为计算机只有有限数量的处理单元。
事实上,这些处理器通常会在处理另一个 goroutine 之前,花费一些时间在一个 goroutine 上,使用一种称为时间共享的技术。具体是如何发生的,这是一个只有 Go 运行时、你使用的操作系统和处理器才知道的黑暗秘密。最好总是假设不同 goroutine 中的操作可能以任何顺序运行。
列表 30.2 中的main函数启动了五个sleepyGopher goroutine。它们都睡眠三秒钟,然后打印相同的内容。
列表 30.2. 五个睡眠的刺猬:sleepygophers.go
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 5; i++ {
go sleepyGopher()
}
time.Sleep(4 * time.Second)
}
func sleepyGopher() {
time.Sleep(3 * time.Second)
fmt.Println("... snore ...")
}
我们可以通过向每个 goroutine 传递一个参数来找出哪些先完成。向 goroutine 传递参数就像向任何函数传递参数一样:值被复制并作为参数传递。
当你运行下一个列表时,你应该看到尽管我们按顺序从零到九启动了所有的 goroutine,但它们都在不同的时间完成。如果你在 Go 沙盒外运行这个程序,你每次都会看到不同的顺序。
列表 30.3. 确认的刺猬:identifiedgophers.go
func main() {
for i := 0; i < 5; i++ {
go sleepyGopher(i)
}
time.Sleep(4 * time.Second)
}
func sleepyGopher(id int) {
time.Sleep(3 * time.Second)
fmt.Println("... ", id, " snore ...")
}
这段代码有一个问题。它等待了四秒钟,而它只需要等待超过三秒钟。更重要的是,如果 goroutine 执行的任务不仅仅是睡眠,我们不知道它们需要多长时间来完成工作。我们需要一种方法让代码知道所有 goroutine 何时完成。幸运的是,Go 为我们提供了我们需要的:通道。
快速检查 30.2
问题 1:
不同的 goroutine 以什么顺序运行?
| |
快速检查 30.2 答案
1:
任何订单。
30.3. 通道
通道可以用来安全地从一个 goroutine 向另一个 goroutine 发送值。将通道想象成那些旧办公室中传递邮件的气动管道系统之一。如果你把它放进去,它就会飞到管道的另一端,然后可以被其他人取出。
与任何其他 Go 类型一样,通道可以用作变量,传递给函数,存储在结构体中,并且可以执行几乎任何其他您希望它们执行的操作。
要创建一个通道,使用 make,这是与创建映射和切片相同的内置函数。通道的类型在创建时指定。以下通道只能发送和接收整数值:
c := make(chan int)
一旦你有了通道,你就可以向它发送值并接收发送到它的值。你使用 左箭头 操作符 (<-) 在通道上发送或接收值。
要发送一个值,将箭头指向通道表达式,就像箭头在告诉右侧的值流入通道一样。发送操作将等待直到有东西(在另一个 goroutine 中)尝试在同一个通道上接收。在等待期间,发送者不能做任何事情,尽管所有其他 goroutine 仍然可以自由运行(假设它们没有在通道操作上等待)。以下发送了值 99:
c <- 99
要从通道接收值,箭头指向通道(它在通道的左侧)。在下面的代码中,我们从通道 c 接收一个值并将其赋给变量 r。与在通道上发送类似,接收者将等待另一个 goroutine 尝试在同一个通道上发送:
r := <-c
注意
虽然通常将通道接收操作单独放在一行上,但这不是必需的。通道接收操作可以在任何可以使用其他表达式的位置使用。
列表 30.4 中的代码创建了一个通道并将其传递给五个困倦的地鼠 goroutine。然后它等待接收五个消息,每个消息对应于已启动的每个 goroutine。每个 goroutine 睡觉并发送一个标识自己的值。当执行到达 main 函数的末尾时,我们可以确信所有的地鼠都已经完成了睡眠,并且它可以返回而不会打扰到任何地鼠的睡眠。例如,假设我们有一个程序将一些数值计算的输出保存到在线存储中。它可能同时保存几个东西,我们不希望在所有结果都成功保存之前退出。
列表 30.4. 通过通道休眠的地鼠:simplechan.go
func main() {
c := make(chan int) *1*
for i := 0; i < 5; i++ {
go sleepyGopher(i, c)
}
for i := 0; i < 5; i++ {
gopherID := <-c *2*
fmt.Println("gopher ", gopherID, " has finished sleeping")
}
}
func sleepyGopher(id int, c chan int) { *3*
time.Sleep(3 * time.Second)
fmt.Println("... ", id, " snore ...")
c <- id *4*
}
-
1 创建通道以进行通信
-
2 从通道接收值
-
3 将通道作为参数声明
-
4 将值发送回主函数
图 30.1 中的方框代表 goroutine,圆圈代表通道。从 goroutine 到通道的链接用指向通道的变量的名称标记;箭头方向表示 goroutine 使用通道的方式。当箭头指向 goroutine 时,goroutine 正在从通道中读取。
图 30.1. gopher 们一起的样子

快速检查 30.3
1
你会使用什么语句在名为
c的通道上发送字符串"hello world"?2
你会如何接收那个值并将其分配给一个变量?
| |
QC 30.3 答案
1
c <- "hello world"2
v = <-c
30.4. 使用 select 进行通道冲浪
在前面的例子中,我们使用单个通道等待多个 goroutine。当所有 goroutine 都产生相同类型的值时,这很好,但并不总是这样。我们通常会想要等待两种或更多不同类型的值。
这的一个例子是我们正在等待通过通道的一些值,但我们不想等待太久。也许我们对我们的沉睡 gopher 有点不耐烦,经过一段时间后我们的耐心耗尽。或者我们可能想在几秒钟后超时网络请求,而不是几分钟。
幸运的是,Go 标准库提供了一个很好的函数,time.After,来帮助。它返回一个通道,在经过一段时间后接收一个值(发送值的 goroutine 是 Go 运行时的一部分)。
我们希望继续从沉睡的 gopher 那里接收值,直到他们全部完成睡眠或者我们的耐心耗尽。这意味着我们需要同时等待计时器通道和另一个通道。select语句允许我们这样做。
select语句看起来像在第 3 课中介绍的switch语句。select语句内部的每个case都包含一个通道接收或发送。select等待直到一个案例准备好,然后运行它及其相关的 case 语句。就像select同时查看两个通道,并在它们中的任何一个发生动作时采取行动一样。
以下列表使用time.After创建一个超时通道,然后使用select等待沉睡的 gopher 的通道和超时通道。
列表 30.5. 急切等待沉睡的 gopher:select1.go
timeout := time.After(2 * time.Second)
for i := 0; i < 5; i++ {
select { *1*
case gopherID := <-c: *2*
fmt.Println("gopher ", gopherID, " has finished sleeping")
case <-timeout: *3*
fmt.Println("my patience ran out")
return *4*
}
}
-
1 select 语句
-
2 等待 gopher 醒来
-
3 等待时间耗尽
-
4 放弃并返回
提示
当select语句中没有案例时,它将永远等待。这可能在你启动了一些你想无限期运行的 goroutine 时停止main函数返回时很有用。
当所有的 gopher 都恰好睡眠三秒钟时,这并不很有趣,因为我们的耐心总是在任何 gopher 醒来之前耗尽。在下一条列表中的 gopher 睡眠的时间是随机的。当你运行这个程序时,你会发现一些 gopher 按时醒来,但有些却没有。
列表 30.6. 随机睡眠的 gopher:select2.go
func sleepyGopher(id int, c chan int) {
time.Sleep(time.Duration(rand.Intn(4000)) * time.Millisecond)
c <- id
}
提示
这种模式在你想要限制做某事所花费的时间时非常有用。通过将操作放在 goroutine 中,并在完成时通过 channel 发送信号,Go 中的任何操作都可以设置超时。
| |
注意
尽管我们已经停止等待 goroutines,但如果我们没有从main函数返回,它们仍然会占用内存。如果可能的话,告诉它们完成是一个好的做法。
| |
nil channel 无作用
因为你需要使用make显式地创建 channel,你可能会想知道如果你使用了尚未“创建”的 channel 值会发生什么。与 map、slice 和 pointer 一样,channel 可以是 nil。事实上,nil是它们的默认零值。
如果你尝试使用 nil channel,它不会引发 panic——相反,操作(发送或接收)将永远阻塞,就像一个从未接收过或发送过任何内容的 channel。这个例外是close(在后面的课程中会介绍)。如果你尝试关闭一个 nil channel,它将引发 panic。
初看起来,这可能似乎并不很有用,但它可以非常有帮助。考虑一个包含select语句的循环。我们可能不希望在循环的每次迭代中都等待select中提到的所有 channel。例如,我们可能只在准备好发送值时尝试在 channel 上发送。我们可以通过使用一个只在想要发送值时才非 nil 的 channel 变量来实现这一点。
到目前为止,一切都很顺利。当我们的main函数在 channel 上接收时,它发现有一个 gopher 在 channel 上发送了一个值。但如果我们不小心尝试在没有任何 goroutine 可以发送时读取,或者如果我们尝试在 channel 上发送而不是接收会发生什么呢?
快速检查 30.4
1
time.After返回什么类型的值?2
如果你在一个 nil channel 上发送或接收会发生什么?
3
select语句中的每个情况包含什么?
| |
QC 30.4 答案
1
一个 channel。
2
它将永远阻塞。
3
一个 channel 操作。
30.5. 阻塞和死锁
当一个 goroutine 在等待发送或接收通道上的数据时,我们称它为阻塞。这听起来可能和我们编写了一个永远旋转什么也不做的循环代码一样,表面上它们看起来确实一样。但如果你在你的笔记本电脑上运行一个无限循环,你可能会发现风扇开始嗡嗡作响,电脑变热,因为它做了很多工作。相比之下,一个阻塞的 goroutine 不占用任何资源(除了 goroutine 本身使用的一小部分内存)。它安静地停在那里,等待阻止它的因素停止阻止。
当一个或多个 goroutine 最终因为某些永远不会发生的事情而阻塞时,我们称之为死锁,你的程序通常会崩溃或挂起。死锁可能由像这样简单的事情引起:
func main() {
c := make(chan int)
<-c
}
在大型程序中,死锁可能涉及 goroutine 之间复杂的一系列依赖关系。
虽然理论上难以防范,但在实践中,通过坚持一些简单的指南(很快就会介绍),制作无死锁的程序并不困难。当你确实发现死锁时,Go 可以显示所有 goroutine 的状态,因此通常很容易找出发生了什么。
快速检查 30.5
Q1:
一个阻塞的 goroutine 会做什么?
| |
QC 30.5 答案
1:
它什么也不做。
30.6. 一个地鼠装配线
到目前为止,我们的地鼠们相当懒惰。它们只是睡一会儿,然后醒来,通过它们的通道发送一个值。但这个工厂里并非所有地鼠都这样。有些地鼠在装配线上勤奋工作,从生产线上的前一个地鼠那里接收一个物品,对它进行一些处理,然后将其发送给生产线上的下一个地鼠。尽管每个地鼠完成的工作很简单,但装配线可以产生令人惊讶的复杂结果。
这种技术被称为流水线,它对于在不使用大量内存的情况下处理大量数据流非常有用。尽管每个 goroutine 一次可能只持有单个值,但它们可能在一段时间内处理数百万个值。流水线也很有用,因为你可以将其用作“思维工具”,帮助更容易地解决某些类型的问题。
我们已经拥有了组装 goroutine 成流水线的所有工具。Go 值沿着流水线流动,从一个 goroutine 传递到下一个。流水线中的工作者反复从其上游邻居那里接收一个值,对其进行一些处理,然后将结果发送到下游。
让我们构建一个由工人组成的装配线,这些工人处理字符串值。装配线起点的 gopher 如列表 30.7 所示——流的来源。这个 gopher 不读取值,只发送它们。在另一个程序中,这可能涉及从文件、数据库或网络中读取数据,但在这里我们只发送一些任意值。为了告诉下游的 gopher 没有更多的值,源发送一个哨兵值,即空字符串,以表示何时完成。
列表 30.7. 源 gopher:pipeline1.go
func sourceGopher(downstream chan string) {
for _, v := range []string{"hello world", "a bad apple", "goodbye all"}
{
downstream <- v
}
downstream <- ""
}
列表 30.8 中的 gopher 从装配线中过滤掉任何不良内容。它从其上游通道读取一个项目,并且只有当值中没有字符串"bad"时,才将其发送到下游通道。当它看到最后的空字符串时,过滤 gopher 停止,并确保将空字符串发送到下一个 gopher。
列表 30.8. 过滤 gopher:pipeline1.go
func filterGopher(upstream, downstream chan string) {
for {
item := <-upstream
if item == "" {
downstream <- ""
return
}
if !strings.Contains(item, "bad") {
downstream <- item
}
}
}
装配线末尾的 gopher——打印 gopher,如列表 30.9 所示。这个 gopher 没有下游。在另一个程序中,它可能将结果保存到文件或数据库中,或者打印它看到的值的摘要。这里的打印 gopher 打印它看到的所有值。
列表 30.9. 打印 gopher:pipeline1.go
func printGopher(upstream chan string) {
for {
v := <-upstream
if v == "" {
return
}
fmt.Println(v)
}
}
让我们把我们的 gopher 工人组合起来。我们在管道中有三个阶段(源、过滤、打印),但只有两个通道。我们不需要为最后一个 gopher 启动一个新的 goroutine,因为我们想在退出整个程序之前等待它完成。当printGopher函数返回时,我们知道其他两个 goroutine 已经完成了它们的工作,然后我们可以从main返回,完成整个程序,如以下列表和图 30.2 所示。
列表 30.10. 装配:pipeline1.go
func main() {
c0 := make(chan string)
c1 := make(chan string)
go sourceGopher(c0)
go filterGopher(c0, c1)
printGopher(c1)
}
图 30.2. Gopher 管道

我们目前拥有的管道代码存在问题。我们使用空字符串来表示没有更多的值需要处理,但如果我们想将空字符串处理成其他任何值一样呢?我们可以发送一个包含我们想要的字符串和一个表示是否为最后一个值的布尔字段的 struct 值。
但有更好的方法。Go 允许我们关闭一个通道来表示不再发送任何值,如下所示:
close(c)
当一个通道被关闭时,你不能向其中写入任何更多的值(如果你尝试这样做,你会得到一个 panic),任何读取都会立即返回类型的零值(在这种情况下是空字符串)。
注意
小心!如果你在一个循环中从关闭的通道读取,而没有检查它是否已关闭,循环将永远旋转,消耗大量的 CPU 时间。确保你知道哪些通道可能已关闭,并相应地进行检查。
我们如何判断通道是否已关闭?如下所示:
v, ok := <-c
当我们将结果分配给两个变量时,第二个变量会告诉我们是否已成功从通道读取。如果通道已关闭,则为 false。
使用这些新工具,我们可以轻松关闭整个管道。下一列表显示了管道头部的源 goroutine。
列表 30.11. 汇编:pipeline2.go
func sourceGopher(downstream chan string) {
for _, v := range []string{"hello world", "a bad apple", "goodbye all"}
{
downstream <- v
}
close(downstream)
}
下一列表显示了过滤器 goroutine 现在的样子。
列表 30.12. 汇编:pipeline2.go
func filterGopher(upstream, downstream chan string) {
for {
item, ok := <-upstream
if !ok {
close(downstream)
return
}
if !strings.Contains(item, "bad") {
downstream <- item
}
}
}
从通道读取直到其关闭的模式足够常见,以至于 Go 提供了一个快捷方式。如果我们在一个 range 语句中使用通道,它将读取通道中的值直到通道关闭。
这意味着我们的代码可以用 range 循环更简单地重写。以下列表实现了与之前相同的功能。
列表 30.13. 汇编:pipeline2.go
func filterGopher(upstream, downstream chan string) {
for item := range upstream {
if !strings.Contains(item, "bad") {
downstream <- item
}
}
close(downstream)
}
汇编线上的最后一个 gopher 读取所有消息,并依次打印,如下一列表所示。
列表 30.14. 汇编:pipeline2.go
func printGopher(upstream chan string) {
for v := range upstream {
fmt.Println(v)
}
}
快速检查 30.6
1
当你从关闭的通道读取时,你看到了什么值?
2
如何检查通道是否已关闭?
| |
QC 30.6 答案
1
通道类型的零值。
2
使用双值赋值语句:
v, ok := <-c
摘要
-
go语句启动一个新的 goroutine,并与之并发运行。 -
通道用于在 goroutine 之间发送值。
-
使用
make(chan string)创建一个通道。 -
<-操作符从通道接收值(当在通道值之前使用时)。 -
<-操作符将值发送到通道(当放置在通道值和要发送的值之间时)。 -
close函数关闭一个通道。 -
range语句读取通道中的所有值,直到其关闭。
让我们看看你是否掌握了这些...
实验:remove-identical.go
看到相同的行重复出现是令人厌烦的。编写一个管道元素(一个 goroutine),它记住前一个值,并且只有当它与之前的不同时,才将值发送到管道的下一阶段。为了使事情更简单一些,你可以假设第一个值永远不会是空字符串。
实验:split-words.go
有时候操作单词比操作句子更容易。编写一个管道元素,它接受字符串,将它们拆分成单词(你可以使用 strings 包中的 Fields 函数),并将所有单词逐个发送到下一个管道阶段。
第 31 课. 并发状态
在阅读完第 31 课后,你将能够
-
保持状态安全
-
使用互斥锁和回复通道
-
使用服务循环
这里我们又回到了 gopher 工厂。忙碌的 gopher 们仍在建造东西,但有几条生产线库存不足,因此他们需要订购更多。
不幸的是,这是一个过时的工厂,只有一条通往外界的单一共享电话线。不过,所有生产线都有自己的听筒。一只地鼠拿起电话下单,但当她开始说话时,另一只地鼠拿起另一个听筒开始拨号,干扰了第一只地鼠。然后另一只也这样做,他们都非常困惑,没有人能成功下单。如果他们能同意一次只使用电话就好了!
在 Go 程序中的共享值有点像这个共享电话。如果有两个或更多的 goroutine 试图同时使用一个共享值,事情可能会出错。可能会没事。也许没有两只地鼠会同时尝试使用电话。但事情可能会以各种方式出错。
可能两只地鼠同时说话会混淆电话另一端的卖家,他们最终订购了错误的东西,或者订购的数量错误,或者订单的其他方面出了问题。无法知道——所有赌注都取消了。
这就是共享 Go 值的问题。除非我们明确知道可以使用特定类型的值进行并发,否则我们必须假设这是不允许的。这种情况被称为竞态条件,因为 goroutines 就像是在争夺使用这个值。
备注
Go 编译器包括尝试在您的代码中查找竞态条件的功能。这非常值得使用,如果它报告了竞态,修复代码总是值得的。请参阅golang.org/doc/articles/race_detector.html。
备注
如果两个 goroutine 同时从同一事物中读取,这是可以的,但如果你在另一个写入的同时读取或写入,你会得到未定义的行为。
考虑这一点
假设我们有一群 goroutine 在工作,爬取网页和抓取网页。我们可能想要跟踪哪些网页已经被访问过。让我们说我们想要跟踪每个网页的链接数量(谷歌在搜索结果中对网页进行排名时做了一些类似的事情)。
看起来我们可以在 goroutine 之间共享一个映射,它包含每个网页的链接计数。当一个 goroutine 处理一个网页时,它会增加该页面的映射条目。
然而,这样做是错误的,因为所有的 goroutine 都在同时更新映射,这会产生竞态条件。我们需要找到一种绕过它的方法。进入互斥锁。
31.1. 互斥锁
Back in the gopher factory, one clever gopher has a bright idea. She puts a glass jar in the middle of the factory floor that holds a single metal token. When a gopher needs to use the phone, they take the token out of the jar and keep it until the phone call has finished. Then they return the token to the jar. If there’s no token in the jar when a gopher wants to make a call, they have to wait until the token is returned.
Note that there’s nothing that physically stops a gopher from using the phone without taking the token. But if they do, there may be unintended consequences from two gophers talking over one another on the phone. Also, consider what happens if the gopher with the token forgets to return it: no other gopher will be able to use the phone until they remember to return it.

In a Go program, the equivalent of that glass jar is called a mutex. The word mutex is short for mutual exclusion. Goroutines can use a mutex to exclude each other from doing something at the same time. The something in question is up to the programmer to decide. Like the jar in the factory, the only “mutual exclusion” properties of a mutex come from the fact that we’re careful to use it whenever we access the thing we’re guarding with it.
Mutexes have two methods: Lock and Unlock. Calling Lock is like taking the token from the jar. We put the token back in the jar by calling Unlock. If any goroutine calls Lock while the mutex is locked, it’ll wait until it’s unlocked before locking it again.
To use the mutex properly, we need to make sure that any code accessing the shared values locks the mutex first, does whatever it needs to, then unlocks the mutex. If any code doesn’t follow this pattern, we can end up with a race condition. Because of this, mutexes are almost always kept internal to a package. The package knows what things the mutex guards, but the Lock and Unlock calls are nicely hidden behind methods or functions.
Unlike channels, Go mutexes aren’t built into the language itself. Rather, they’re available in the sync package. Listing 31.1 is a complete program that locks and unlocks a global mutex value. We don’t need to initialize the mutex before using it—the zero value is an unlocked mutex.
The defer keyword introduced in lesson 28 can help with mutexes too. Even if there are many lines of code in a function, the Unlock call stays next to the Lock call.
列表 31.1. 锁定和解锁互斥锁:mutex.go
package main
import "sync" *1*
var mu sync.Mutex *2*
func main() {
mu.Lock() *3*
defer mu.Unlock() *4*
// The lock is held until we return from the function.
}
-
1 导入 sync 包
-
2 声明互斥锁
-
3 锁定互斥锁
-
4 在返回前解锁互斥锁
Note
The defer statement is particularly useful when there are multiple return statements. Without defer, we’d need a call to Unlock just before every return statement, and it would be very easy to forget one of those.
让我们实现一个类型,网络爬虫可以使用它来跟踪已访问网页的链接数量。我们将存储一个包含网页 URL 的映射,并用互斥锁保护它。列表 31.2 中的sync.Mutex是struct类型的一个成员,这是一个非常常见的模式。
提示
将互斥锁的定义放在它所保护的变量上方,并包含注释,以便关联清晰,是一种良好的实践。
列表 31.2. 页面引用映射:scrape.go
// Visited tracks whether web pages have been visited.
// Its methods may be used concurrently from multiple goroutines.
type Visited struct {
// mu guards the visited map.
mu sync.Mutex *1*
visited map[string]int *2*
}
-
1 声明一个互斥锁
-
2 声明一个从 URL(字符串)键到整数值的映射
注意
在 Go 中,你应该假设除非明确记录,否则没有方法可以在并发中使用,就像我们在这里所做的那样。
下一个列表中的代码定义了一个VisitLink方法,当遇到链接时调用;它返回该链接之前遇到的次数。
列表 31.3. 访问链接:scrape.go
// VisitLink tracks that the page with the given URL has
// been visited, and returns the updated link count.
func (v *Visited) VisitLink(url string) int {
v.mu.Lock() *1*
defer v.mu.Unlock() *2*
count := v.visited[url]
count++
v.visited[url] = count *3*
return count
}
-
1 锁定互斥锁
-
2 确保互斥锁已解锁
-
3 更新映射
Go playground 不是实验竞态条件的好地方,因为它被故意保持确定性且无竞态。但你可以通过在语句之间插入time.Sleep调用来进行实验。
尝试修改列表 31.3,使用在第 30 课开头介绍的技术来启动几个 goroutine,它们都调用带有不同值的VisitLink,并尝试在不同位置插入Sleep语句进行实验。还尝试删除Lock和Unlock调用,看看会发生什么。
当有一个小而定义良好的状态要保护时,互斥锁的使用相当简单,并且当编写希望从多个 goroutine 同时使用的方法时,它是必不可少的工具。
快速检查 31.1
1
如果两个 goroutine 同时尝试更改相同的值,可能会发生什么?
2
如果你在解锁之前再次尝试锁定互斥锁,会发生什么?
3
如果你没有加锁就解锁会发生什么?
4
同时从不同的 goroutine 调用同一类型的函数是否安全?
| |
QC 31.1 答案
1
它是未定义的。程序可能会崩溃或发生其他任何事情。
2
它将永远阻塞。
3
它会恐慌:未加锁的互斥锁解锁。
4
不,除非明确记录为这样。
31.1.1. 互斥锁陷阱
在列表 31.2 中,当互斥锁被锁定时,我们只做一件非常简单的事情:我们更新一个映射。在锁定期间我们做的越多,我们就越需要小心。如果我们锁定互斥锁等待某事,我们可能会长时间地阻止其他人。更糟糕的是,如果我们试图锁定相同的互斥锁,我们将会死锁——Lock调用将永远阻塞,因为我们永远不会在等待获取锁的同时放弃锁!
为了安全起见,请遵循以下指南:
-
尽量保持 mutex 中的代码简单。
-
对于给定的共享状态,只有一个 mutex。
Mutex 对于简单的共享状态来说很好用,但有时我们可能需要更多。在第 30 课的 gopher 工厂中,我们可能希望 gophers 能够独立行动,响应其他 gophers 的请求,但同时也随着时间的推移做自己的事情。与装配线上的 gophers 不同,这样的 gophers 不会完全响应其他 gophers 的消息,但可以决定为自己做事情。
快速检查 31.2
Q1:
锁定 mutex 可能存在两个潜在的问题是什么?
QC 31.2 答案
1:
它可能会阻塞其他也试图锁定 mutex 的 goroutines;这可能导致死锁。
31.2. 长期工作者
考虑在火星表面驾驶探测车的任务。好奇号火星探测车上的软件被构建为一系列独立的模块,通过传递消息相互通信(见mng.bz/Z7Xa),这与 Go 的 goroutines 非常相似。
探测车的模块负责探测车行为的各个方面。让我们尝试编写一些 Go 代码,在虚拟火星上驾驶一个(高度简化的)探测车。因为我们没有真正的引擎来驾驶,我们将通过更新一个包含探测车坐标的变量来凑合。我们希望探测车可以从地球控制,因此它需要对外部命令做出响应。
注意
我们在这里构建的代码结构可以用于任何类型的长期任务,这些任务可以独立执行,例如网站轮询器或硬件设备控制器。
要驾驶探测车,我们将启动一个 goroutine,该 goroutine 将负责控制其位置。当探测车软件启动时启动 goroutine,并在关闭前保持运行。因为它持续运行并独立操作,我们将称这个 goroutine 为工作者。
工作者通常被编写为一个包含select语句的for循环。循环在工作者存活期间运行;select等待发生感兴趣的事情。在这种情况下,“感兴趣的事情”可能是一个来自外部的命令。记住,尽管工作者是独立操作的,但我们仍然希望能够控制它。或者,它可能是一个定时器事件,告诉工作者是时候移动探测车了。
这里有一个什么也不做的骨架工作者函数:
func worker() {
for {
select {
// Wait for channels here.
}
}
}
我们可以像在之前的例子中启动 goroutines 一样启动这样的工作者:
go worker()
事件循环和 goroutines
一些其他编程语言使用事件循环——一个等待事件并当事件发生时调用注册函数的中心循环。通过提供 goroutines 作为核心概念,Go 避免了需要中心事件循环的需求。任何工作者 goroutine 都可以被视为它自己的事件循环。
我们希望我们的火星漫游车定期更新其位置。为此,我们希望驱动它的工作 goroutine 每隔一段时间醒来进行更新。我们可以使用time.After来实现这一点(在第 30 课中讨论过),它提供了一个在给定持续时间后接收值的通道。
列表 31.4 中的工作进程每秒打印一个值。目前,我们只是增加一个数字而不是更新位置。当我们收到计时器事件时,我们再次调用After,这样在下一次循环中,我们将等待一个新的计时器通道。
列表 31.4. 数字打印工作进程:printworker.go
func worker() {
n := 0
next := time.After(time.Second) *1*
for {
select {
case <-next: *2*
n++
fmt.Println(n) *3*
next = time.After(time.Second) *4*
}
}
}
-
1 创建初始计时器通道
-
2 等待计时器触发
-
3 打印数字
-
4 为另一个事件创建另一个计时器通道
注意
在这个例子中,我们不需要使用select语句。只有一个情况的select与直接使用通道操作相同。但在这里我们使用select,因为在接下来的课程中,我们将更改代码以等待不仅仅是计时器。否则,我们可以完全避免After调用并使用time.Sleep。
现在我们已经有一个可以独立行动的工作进程,让我们通过更新位置而不是数字使其更像漫游车。方便的是,Go 的image包提供了一个Point类型,我们可以用它来表示漫游车的当前位置和方向。Point是一个包含 X 和 Y 坐标以及相应方法的结构。例如,Add方法将一个点添加到另一个点。
让我们用 X 轴表示东西方向,用 Y 轴表示南北方向。要使用Point,我们必须首先导入image包:
import "image"
每次我们在计时器通道上接收到一个值时,我们将表示当前方向的点添加到当前位置,如下一列表所示。目前,漫游车将始终从同一位置 [10, 10] 开始并向东行进,但我们将很快解决这个问题。
列表 31.5. 位置更新工作进程:positionworker.go
func worker() {
pos := image.Point{X: 10, Y: 10} *1*
direction := image.Point{X: 1, Y: 0} *2*
next := time.After(time.Second)
for {
select {
case <-next:
pos = pos.Add(direction)
fmt.Println("current position is ", pos) *3*
next = time.After(time.Second)
}
}
}
-
1 当前位置(初始为 [10, 10])
-
2 当前方向(初始为 [1, 0],向东行进)
-
3 打印当前位置
如果一个火星漫游车只能直线行进,那就没什么用了。我们希望能够控制漫游车使其向不同方向行进,或停止它,或使其行进得更快。我们需要另一个可以用来向工作进程发送命令的通道。当工作进程在命令通道上接收到一个值时,它可以执行该命令。在 Go 中,通常将这样的通道隐藏在方法后面,因为通道被认为是实现细节。
下面的列表中的RoverDriver类型包含我们将用于向工作进程发送命令的通道。我们将使用一个command类型来保存发送的命令。
列表 31.6. RoverDriver 类型:rover.go
// RoverDriver drives a rover around the surface of Mars.
type RoverDriver struct {
commandc chan command
}
我们可以将创建通道并启动工作者的逻辑封装在NewRoverDriver函数中,如下一列表所示。我们将定义一个drive方法来实现我们的工作者逻辑。虽然它是一个方法,但它将像本章前面提到的worker函数一样工作。作为一个方法,它能够访问RoverDriver结构中的任何值。
列表 31.7. 创建:rover.go
func NewRoverDriver() *RoverDriver {
r := &RoverDriver{
commandc: make(chan command),
}
go r.drive()
return r
}
现在我们需要决定我们想要能够发送给探测车的命令。为了保持简单,让我们只允许两个命令:“向左转 90°”和“向右转 90°”,如以下列表所示。
列表 31.8. 命令类型:rover.go
type command int
const (
right = command(0)
left = command(1)
)
注意
通道可以是任何 Go 类型;命令类型可以是包含任意复杂命令的结构体类型。
现在我们已经定义了RoverDriver类型和一个创建其实例的函数,我们需要drive方法(将控制探测车的工作者),如列表 31.9 提供。它与之前看到的定位更新工作者几乎相同,只是它也等待在命令通道上。当它收到命令时,它会根据命令值来决定要做什么。为了了解发生了什么,我们记录了变化。
列表 31.9. RoverDriver工作者:rover.go
// drive is responsible for driving the rover. It
// is expected to be started in a goroutine.
func (r *RoverDriver) drive() {
pos := image.Point{X: 0, Y: 0}
direction := image.Point{X: 1, Y: 0}
updateInterval := 250 * time.Millisecond
nextMove := time.After(updateInterval)
for {
select {
case c := <-r.commandc: *1*
switch c {
case right: *2*
direction = image.Point{
X: -direction.Y,
Y: direction.X,
}
case left: *3*
direction = image.Point{
X: direction.Y,
Y: -direction.X,
}
}
log.Printf("new direction %v", direction)
case <-nextMove:
pos = pos.Add(direction)
log.Printf("moved to %v", pos)
nextMove = time.After(updateInterval)
}
}
}
-
1 在命令通道上等待命令
-
2 向右转
-
3 向左转
现在我们可以通过添加控制探测车的方法来完成RoverDriver类型的定义,如列表 31.10 所示。我们将声明两个方法,每个命令一个。每个方法都会在commandc通道上发送正确的命令。例如,如果我们调用Left方法,它将发送一个left命令值,工作者将接收这个值并改变工作者的方向。
注意
虽然这些方法正在控制探测车的方向,但它们没有直接访问方向值,因此不存在它们可以并发更改它并导致竞争条件的风险。这意味着我们不需要互斥锁,因为通道允许与探测车的 goroutine 通信,而不直接更改其任何值。
列表 31.10. RoverDriver方法:rover.go
// Left turns the rover left (90° counterclockwise).
func (r *RoverDriver) Left() {
r.commandc <- left
}
// Right turns the rover right (90° clockwise).
func (r *RoverDriver) Right() {
r.commandc <- right
}
现在我们有了完全功能的RoverDriver类型,列表 31.11 创建了一个探测车并发送了一些命令。现在它可以自由地移动了!
列表 31.11. 放手吧!rover.go
func main() {
r := NewRoverDriver()
time.Sleep(3 * time.Second)
r.Left()
time.Sleep(3 * time.Second)
r.Right()
time.Sleep(3 * time.Second)
}
尝试通过使用不同的定时和发送不同的命令来实验RoverDriver类型。
虽然我们在这里关注了一个具体的例子,但这种工作者模式可以在许多不同的情况下很有用,在这些情况下,你需要有一些长期运行的 goroutine 来控制某些东西,同时保持对外部控制的响应。
快速检查 31.3
1
在 Go 中,我们通常使用什么代替事件循环?
2
提供了
Point数据类型的 Go 标准库包是什么?3
你可能会使用哪些 Go 语句来实现一个长期运行的 worker goroutine?
4
通道使用的内部细节是如何隐藏的?
5
可以通过通道发送哪些 Go 值?
QC 31.3 答案
1
goroutine 中的循环。
2
image包。3
for语句和select语句。4
在方法调用之后。
5
任何值都可以通过通道发送。
摘要
-
除非明确标记为可以这样做,否则不要同时从多个 goroutine 访问状态。
-
使用互斥锁确保一次只有一个 goroutine 访问某个东西。
-
只用一个互斥锁来保护一块状态。
-
尽量减少在互斥锁保持期间的操作。
-
你可以将一个长生命周期的 goroutine 写作一个带有
select循环的工作者。 -
在方法后面隐藏工作者的细节。
看看你是否掌握了这些...
实验:positionworker.go
以 列表 31.5 为起点,修改代码,使每次移动的延迟时间增加半秒。
实验:rover.go
以 RoverDriver 类型为起点,定义 Start 和 Stop 方法以及相关的命令,并使漫游者遵守这些命令。
第 32 课。火星生活
32.1. 可驾驶的网格
通过实现 MarsGrid 类型来创建一个漫游者可以驾驶的网格。你需要使用互斥锁来确保它可以同时被多个 goroutine 安全使用。它看起来可能如下所示:
// MarsGrid represents a grid of some of the surface
// of Mars. It may be used concurrently by different
// goroutines.
type MarsGrid struct {
// To be done.
}
// Occupy occupies a cell at the given point in the grid. It
// returns nil if the point is already occupied or the point is
// outside the grid. Otherwise it returns a value that can be
// used to move to different places on the grid.
func (g *MarsGrid) Occupy(p image.Point) *Occupier
// Occupier represents an occupied cell in the grid.
// It may be used concurrently by different goroutines.
type Occupier struct {
// To be done.
}
// Move moves the occupier to a different cell in the grid.
// It reports whether the move was successful
// It might fail because it was trying to move outside
// the grid or because the cell it's trying to move into
// is occupied. If it fails, the occupier remains in the same place.
func (g *Occupier) Move(p image.Point) bool
现在将第 31 课的漫游者示例(kindle_split_046.html#ch31)修改一下,使其不再只是本地更新坐标,而是使用传递给 NewRoverDriver 函数的 MarsGrid 对象。如果它触碰到网格的边缘或障碍物,它应该转向并朝另一个随机方向前进。

现在你可以通过调用 NewRoverDriver 来启动几个漫游者,并看到它们在网格上一起驾驶。
32.2. 报告发现
我们想在火星上找到生命,所以我们会派几个漫游者下去寻找,但我们需要知道何时找到生命。在网格的每个单元格中,分配一些生命可能性的概率,一个介于 0 和 1000 之间的随机数。如果一个漫游者找到一个生命值超过 900 的单元格,它可能已经找到了生命,并且必须发送一个无线电消息回地球。
不幸的是,并不总是能够立即发送消息,因为中继卫星并不总是位于地平线上。实现一个缓冲 goroutine,它接收从漫游者发送的消息,并将它们缓冲到一个切片中,直到它们可以发送回地球。
实现地球作为一个只偶尔接收消息的 goroutine(在现实中,每天可能只有几个小时,但你可能希望将间隔缩短一些)。每个消息应包含可能发现生命的单元格的坐标以及生命值本身。
你也许还想给你的每个漫游者起一个名字,并在消息中包含这个名字,这样你就能知道哪个漫游者发送了它。在漫游者打印的日志消息中包含名字也很有用,这样你可以跟踪每个漫游者的进度。
释放你的漫游者去搜索,看看它们能找到什么!
结论。从这里走向何方
这标志着《用 Go 编程》的结束,但你的旅程并未结束。我们希望你的脑海中充满了想法,并渴望继续学习和构建。感谢您加入我们。
在雷达之下
Go 是一种相对较小的语言,你已经学到了大部分。在本版《用 Go 编程》中,还有一些边缘内容没有涵盖:
-
它没有涵盖使用方便的
iota标识符声明顺序常量。 -
它没有提及位移动操作(
<<和>>)和位运算符(&和|)。 -
第 3 课涵盖了循环,但跳过了
continue关键字,并跳过了goto关键字和标签。 -
第 4 课涵盖了作用域,但没有涵盖阴影变量——那些阴影角色。
-
第 6 课到第 8 课处理了浮点数、整数和大数,但没有处理复数或虚数。
-
第 12 课展示了
return关键字,但没有展示裸返回——谦逊是一种美德。 -
第 12 课提到了空接口
interface{},但只是简要提及。 -
第 13 课介绍了方法,但没有介绍方法值。
-
第 28 课提到了类型断言,但没有提到类型选择器。
-
第 30 课没有提到方向通道。
-
它没有解释使用
init进行初始化的特殊函数,就像main一样。 -
它没有详细说明每个内置函数,例如用于指针的
new和用于切片的copy(见golang.org/pkg/builtin/)。 -
它没有演示如何编写新包来组织代码或与他人共享。
超出游乐场
如果你刚开始学习计算机编程,你可能已经欣赏了基于网络的 Go 游乐场,但游乐场有一些限制。
要摆脱游乐场的限制并构建下一个酷炫的东西,你需要在你的电脑上安装 Go(见golang.org/dl/)。启动终端或命令提示符有点像跳进时光机。学会在电脑上导航并运行程序,就像 1995 年一样!
你还需要一个文本编辑器。本书的作者使用 Sublime Text 和 Acme,但有许多编辑器对 Go 有很好的支持(见golang.org/doc/editors.html)。迟早,你将需要一个版本控制工具,如git——它确实是一个时光机,但仅限于代码和其他文件。
还有更多
Go 不仅仅是一种编程语言。有一个丰富的工具和库生态系统等待你去发现。
自动化测试、调试、基准测试以及更多所需的一切都是可用的。标准库有许多更多的包可以探索,如果你用完了,gophers 社区一直在忙于为任何需求制作大量第三方包(见godoc.org)。
有许多在线资源(见golang.org/wiki)可以帮助你继续你的学习之旅,还有数十本适合 Go 语言爱好者的书籍,包括《Go 语言实战》、《Go Web 编程》和《Go 语言实战》(见golang.org/wiki/Books)。
总有更多东西可以学习,所以加入这个乐趣吧!Go 社区欢迎你的加入。
附录. 解答
本附录提供了课程结束练习和核心项目的解答。请注意,任何问题都有不止一个解决方案。
备注
您可以从 Manning 网站 www.manning.com/books/get-programming-with-go 下载这些解答和其余的源代码,或者在网上浏览 github.com/nathany/get-programming-with-go 的源代码。
单元 0
第 1 课
实验: playground.go
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello, Nathan")
fmt.Println(" hola")
}
单元 1
第 2 课
实验: malacandra.go
package main
import "fmt"
func main() {
const hoursPerDay = 24
var days = 28
var distance = 56000000 // km
fmt.Println(distance/(days*hoursPerDay), "km/h")
}
第 3 课
实验: guess.go
package main
import (
"fmt"
"math/rand"
)
func main() {
var number = 42
for {
var n = rand.Intn(100) + 1
if n < number {
fmt.Printf("%v is too small.\n", n)
} else if n > number {
fmt.Printf("%v is too big.\n", n)
} else {
fmt.Printf("You got it! %v\n", n)
break
}
}
}
第 4 课
实验: random-dates.go
package main
import (
"fmt"
"math/rand"
)
var era = "AD"
func main() {
for count := 0; count < 10; count++ {
year := 2018 + rand.Intn(10)
leap := year%400 == 0 || (year%4 == 0 && year%100 != 0)
month := rand.Intn(12) + 1
daysInMonth := 31
switch month {
case 2:
daysInMonth = 28
if leap {
daysInMonth = 29
}
case 4, 6, 9, 11:
daysInMonth = 30
}
day := rand.Intn(daysInMonth) + 1
fmt.Println(era, year, month, day)
}
}
核心项目 5
实验: tickets.go
package main
import (
"fmt"
"math/rand"
)
const secondsPerDay = 86400
func main() {
distance := 62100000
company := ""
trip := ""
fmt.Println("Spaceline Days Trip type Price")
fmt.Println("======================================")
for count := 0; count < 10; count++ {
switch rand.Intn(3) {
case 0:
company = "Space Adventures"
case 1:
company = "SpaceX"
case 2:
company = "Virgin Galactic"
}
speed := rand.Intn(15) + 16 // 16-30 km/s
duration := distance / speed / secondsPerDay // days
price := 20.0 + speed // millions
if rand.Intn(2) == 1 {
trip = "Round-trip"
price = price * 2
} else {
trip = "One-way"
}
fmt.Printf("%-16v %4v %-10v $%4v\n", company, duration, trip, price)
}
}
单元 2
第 6 课
实验: piggy.go
package main
import (
"fmt"
"math/rand"
)
func main() {
piggyBank := 0.0
for piggyBank < 20.00 {
switch rand.Intn(3) {
case 0:
piggyBank += 0.05
case 1:
piggyBank += 0.10
case 2:
piggyBank += 0.25
}
fmt.Printf("$%5.2f\n", piggyBank)
}
}
第 7 课
实验: piggy.go
package main
import (
"fmt"
"math/rand"
)
func main() {
piggyBank := 0
for piggyBank < 2000 {
switch rand.Intn(3) {
case 0:
piggyBank += 5
case 1:
piggyBank += 10
case 2:
piggyBank += 25
}
dollars := piggyBank / 100
cents := piggyBank % 100
fmt.Printf("$%d.%02d\n", dollars, cents)
}
}
第 8 课
实验: canis.go
package main
import (
"fmt"
)
func main() {
const distance = 236000000000000000
const lightSpeed = 299792
const secondsPerDay = 86400
const daysPerYear = 365
const years = distance / lightSpeed / secondsPerDay / daysPerYear
fmt.Println("Canis Major Dwarf Galaxy is", years, "light years away.") *1*
}
- 1 打印 Canis Major Dwarf Galaxy is 24962 light years away.
第 9 课
实验: caesar.go
message := "L fdph, L vdz, L frqtxhuhg."
for i := 0; i < len(message); i++ {
c := message[i]
if c >= 'a' && c <= 'z' {
c -= 3
if c < 'a' {
c += 26
}
} else if c >= 'A' && c <= 'Z' {
c -= 3
if c < 'A' {
c += 26
}
}
fmt.Printf("%c", c)
}
实验: international.go
message := "Hola Estación Espacial Internacional"
for _, c := range message {
if c >= 'a' && c <= 'z' {
c = c + 13
if c > 'z' {
c = c - 26
}
} else if c >= 'A' && c <= 'Z' {
c = c + 13
if c > 'Z' {
c = c - 26
}
}
fmt.Printf("%c", c)
}
第 10 课
实验: input.go
yesNo := "1"
var launch bool
switch yesNo {
case "true", "yes", "1":
launch = true
case "false", "no", "0":
launch = false
default:
fmt.Println(yesNo, "is not valid")
}
fmt.Println("Ready for launch:", launch) *1*
- 1 打印 Ready for launch: true
核心项目 11
实验: decipher.go
cipherText := "CSOITEUIWUIZNSROCNKFD"
keyword := "GOLANG"
message := ""
keyIndex := 0
for i := 0; i < len(cipherText); i++ {
// A=0, B=1, ... Z=25
c := cipherText[i] - 'A'
k := keyword[keyIndex] - 'A'
// cipher letter - key letter
c = (c-k+26)%26 + 'A'
message += string(c)
// increment keyIndex
keyIndex++
keyIndex %= len(keyword)
}
fmt.Println(message)
实验: cipher.go
message := "your message goes here"
keyword := "golang"
keyIndex := 0
cipherText := ""
message = strings.ToUpper(strings.Replace(message, " ", "", -1))
keyword = strings.ToUpper(strings.Replace(keyword, " ", "", -1))
for i := 0; i < len(message); i++ {
c := message[i]
if c >= 'A' && c <= 'Z' {
// A=0, B=1, ... Z=25
c -= 'A'
k := keyword[keyIndex] - 'A'
// cipher letter + key letter
c = (c+k)%26 + 'A'
// increment keyIndex
keyIndex++
keyIndex %= len(keyword)
}
cipherText += string(c)
}
fmt.Println(cipherText)
单元 3
第 12 课
实验: functions.go
package main
import "fmt"
func kelvinToCelsius(k float64) float64 {
return k - 273.15
}
func celsiusToFahrenheit(c float64) float64 {
return (c * 9.0 / 5.0) + 32.0
}
func kelvinToFahrenheit(k float64) float64 {
return celsiusToFahrenheit(kelvinToCelsius(k))
}
func main() {
fmt.Printf("233° K is %.2f° C\n", kelvinToCelsius(233))
fmt.Printf("0° K is %.2f° F\n", kelvinToFahrenheit(0))
}
第 13 课
实验: methods.go
package main
import "fmt"
type celsius float64
func (c celsius) fahrenheit() fahrenheit {
return fahrenheit((c * 9.0 / 5.0) + 32.0)
}
func (c celsius) kelvin() kelvin {
return kelvin(c + 273.15)
}
type fahrenheit float64
func (f fahrenheit) celsius() celsius {
return celsius((f - 32.0) * 5.0 / 9.0)
}
func (f fahrenheit) kelvin() kelvin {
return f.celsius().kelvin()
}
type kelvin float64
func (k kelvin) celsius() celsius {
return celsius(k - 273.15)
}
func (k kelvin) fahrenheit() fahrenheit {
return k.celsius().fahrenheit()
}
func main() {
var k kelvin = 294.0
c := k.celsius()
fmt.Print(k, "° K is ", c, "° C")
}
第 14 课
实验: calibrate.go
package main
import (
"fmt"
"math/rand"
)
type kelvin float64
type sensor func() kelvin
func fakeSensor() kelvin {
return kelvin(rand.Intn(151) + 150)
}
func calibrate(s sensor, offset kelvin) sensor {
return func() kelvin {
return s() + offset
}
}
func main() {
var offset kelvin = 5
sensor := calibrate(fakeSensor, offset)
for count := 0; count < 10; count++ {
fmt.Println(sensor())
}
}
核心项目 15
实验: tables.go
package main
import (
"fmt"
)
type celsius float64
func (c celsius) fahrenheit() fahrenheit {
return fahrenheit((c * 9.0 / 5.0) + 32.0)
}
type fahrenheit float64
func (f fahrenheit) celsius() celsius {
return celsius((f - 32.0) * 5.0 / 9.0)
}
const (
line = "======================="
rowFormat = "| %8s | %8s |\n"
numberFormat = "%.1f"
)
type getRowFn func(row int) (string, string)
// drawTable draws a two column table.
func drawTable(hdr1, hdr2 string, rows int, getRow getRowFn) {
fmt.Println(line)
fmt.Printf(rowFormat, hdr1, hdr2)
fmt.Println(line)
for row := 0; row < rows; row++ {
cell1, cell2 := getRow(row)
fmt.Printf(rowFormat, cell1, cell2)
}
fmt.Println(line)
}
func ctof(row int) (string, string) {
c := celsius(row*5 - 40)
f := c.fahrenheit()
cell1 := fmt.Sprintf(numberFormat, c)
cell2 := fmt.Sprintf(numberFormat, f)
return cell1, cell2
}
func ftoc(row int) (string, string) {
f := fahrenheit(row*5 - 40)
c := f.celsius()
cell1 := fmt.Sprintf(numberFormat, f)
cell2 := fmt.Sprintf(numberFormat, c)
return cell1, cell2
}
func main() {
drawTable("°C", "°F", 29, ctof)
fmt.Println()
drawTable("°F", "°C", 29, ftoc)
}
单元 4
第 16 课
实验: chess.go
package main
import "fmt"
func display(board [8][8]rune) {
for _, row := range board {
for _, column := range row {
if column == 0 {
fmt.Print(" ")
} else {
fmt.Printf("%c ", column)
}
}
fmt.Println()
}
}
func main() {
var board [8][8]rune
// black pieces
board[0][0] = 'r'
board[0][1] = 'n'
board[0][2] = 'b'
board[0][3] = 'q'
board[0][4] = 'k'
board[0][5] = 'b'
board[0][6] = 'n'
board[0][7] = 'r'
// pawns
for column := range board[1] {
board[1][column] = 'p'
board[6][column] = 'P'
}
// white pieces
board[7][0] = 'R'
board[7][1] = 'N'
board[7][2] = 'B'
board[7][3] = 'Q'
board[7][4] = 'K'
board[7][5] = 'B'
board[7][6] = 'N'
board[7][7] = 'R'
display(board)
}
第 17 课
实验: terraform.go
package main
import "fmt"
// Planets attaches methods to []string.
type Planets []string
func (planets Planets) terraform() {
for i := range planets {
planets[i] = "New " + planets[i]
}
}
func main() {
planets := []string{
"Mercury", "Venus", "Earth", "Mars",
"Jupiter", "Saturn", "Uranus", "Neptune",
}
Planets(planets[3:4]).terraform()
Planets(planets[6:]).terraform()
fmt.Println(planets) *1*
}
- 1 打印 [Mercury Venus Earth New Mars Jupiter Saturn New Uranus New Neptune]
第 18 课
实验: capacity.go
s := []string{}
lastCap := cap(s)
for i := 0; i < 10000; i++ {
s = append(s, "An element")
if cap(s) != lastCap {
fmt.Println(cap(s))
lastCap = cap(s)
}
}
第 19 课
实验: words.go
package main
import (
"fmt"
"strings"
)
func countWords(text string) map[string]int {
words := strings.Fields(strings.ToLower(text))
frequency := make(map[string]int, len(words))
for _, word := range words {
word = strings.Trim(word, `.,"-`)
frequency[word]++
}
return frequency
}
func main() {
text := `As far as eye could reach he saw nothing but the stems of the
great plants about him receding in the violet shade, and far overhead
the multiple transparency of huge leaves filtering the sunshine to the
solemn splendour of twilight in which he walked. Whenever he felt able
he ran again; the ground continued soft and springy, covered with the
same resilient weed which was the first thing his hands had touched in
Malacandra. Once or twice a small red creature scuttled across his
path, but otherwise there seemed to be no life stirring in the wood;
nothing to fear -- except the fact of wandering unprovisioned and alone
in a forest of unknown vegetation thousands or millions of miles
beyond the reach or knowledge of man.`
frequency := countWords(text)
for word, count := range frequency {
if count > 1 {
fmt.Printf("%d %v\n", count, word)
}
}
}
核心项目 20
实验: life.go
package main
import (
"fmt"
"math/rand"
"time"
)
const (
width = 80
height = 15
)
// Universe is a two-dimensional field of cells.
type Universe [][]bool
// NewUniverse returns an empty universe.
func NewUniverse() Universe {
u := make(Universe, height)
for i := range u {
u[i] = make([]bool, width)
}
return u
}
// Seed random live cells into the universe.
func (u Universe) Seed() {
for i := 0; i < (width * height / 4); i++ {
u.Set(rand.Intn(width), rand.Intn(height), true)
}
}
// Set the state of the specified cell.
func (u Universe) Set(x, y int, b bool) {
u[y][x] = b
}
// Alive reports whether the specified cell is alive.
// If the coordinates are outside of the universe, they wrap around.
func (u Universe) Alive(x, y int) bool {
x = (x + width) % width
y = (y + height) % height
return u[y][x]
}
// Neighbors counts the adjacent cells that are alive.
func (u Universe) Neighbors(x, y int) int {
n := 0
for v := -1; v <= 1; v++ {
for h := -1; h <= 1; h++ {
if !(v == 0 && h == 0) && u.Alive(x+h, y+v) {
n++
}
}
}
return n
}
// Next returns the state of the specified cell at the next step.
func (u Universe) Next(x, y int) bool {
n := u.Neighbors(x, y)
return n == 3 || n == 2 && u.Alive(x, y)
}
// String returns the universe as a string.
func (u Universe) String() string {
var b byte
buf := make([]byte, 0, (width+1)*height)
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
b = ' '
if u[y][x] {
b = '*'
}
buf = append(buf, b)
}
buf = append(buf, '\n')
}
return string(buf)
}
// Show clears the screen and displays the universe.
func (u Universe) Show() {
fmt.Print("\x0c", u.String())
}
// Step updates the state of the next universe (b) from
// the current universe (a).
func Step(a, b Universe) {
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
b.Set(x, y, a.Next(x, y))
}
}
}
func main() {
a, b := NewUniverse(), NewUniverse()
a.Seed()
for i := 0; i < 300; i++ {
Step(a, b)
a.Show()
time.Sleep(time.Second / 30)
a, b = b, a // Swap universes
}
}
单元 5
第 21 课
实验: landing.go
type location struct {
Name string `json:"name"`
Lat float64 `json:"latitude"`
Long float64 `json:"longitude"`
}
locations := []location{
{Name: "Bradbury Landing", Lat: -4.5895, Long: 137.4417},
{Name: "Columbia Memorial Station", Lat: -14.5684, Long: 175.472636},
{Name: "Challenger Memorial Station", Lat: -1.9462, Long: 354.4734},
}
bytes, err := json.MarshalIndent(locations, "", " ")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println(string(bytes))
第 22 课
实验: landing.go
package main
import "fmt"
// location with a latitude, longitude.
type location struct {
lat, long float64
}
// coordinate in degrees, minutes, seconds in a N/S/E/W hemisphere.
type coordinate struct {
d, m, s float64
h rune
}
// newLocation from latitude, longitude d/m/s coordinates.
func newLocation(lat, long coordinate) location {
return location{lat.decimal(), long.decimal()}
}
// decimal converts a d/m/s coordinate to decimal degrees.
func (c coordinate) decimal() float64 {
sign := 1.0
switch c.h {
case 'S', 'W', 's', 'w':
sign = -1
}
return sign * (c.d + c.m/60 + c.s/3600)
}
func main() {
spirit := newLocation(coordinate{14, 34, 6.2, 'S'}, coordinate{175, 28,
21.5, 'E'})
opportunity := newLocation(coordinate{1, 56, 46.3, 'S'}, coordinate{354,
28, 24.2, 'E'})
curiosity := newLocation(coordinate{4, 35, 22.2, 'S'}, coordinate{137,
26, 30.12, 'E'})
insight := newLocation(coordinate{4, 30, 0.0, 'N'}, coordinate{135, 54,
0, 'E'})
fmt.Println("Spirit", spirit)
fmt.Println("Opportunity", opportunity)
fmt.Println("Curiosity", curiosity)
fmt.Println("InSight", insight)
}
实验: distance.go
package main
import (
"fmt"
"math"
)
// location with a latitude, longitude.
type location struct {
lat, long float64
}
// coordinate in degrees, minutes, seconds in a N/S/E/W hemisphere.
type coordinate struct {
d, m, s float64
h rune
}
// newLocation from latitude, longitude d/m/s coordinates.
func newLocation(lat, long coordinate) location {
return location{lat.decimal(), long.decimal()}
}
// decimal converts a d/m/s coordinate to decimal degrees.
func (c coordinate) decimal() float64 {
sign := 1.0
switch c.h {
case 'S', 'W', 's', 'w':
sign = -1
}
return sign * (c.d + c.m/60 + c.s/3600)
}
// world with a volumetric mean radius in kilometers
type world struct {
radius float64
}
// distance calculation using the Spherical Law of Cosines.
func (w world) distance(p1, p2 location) float64 {
s1, c1 := math.Sincos(rad(p1.lat))
s2, c2 := math.Sincos(rad(p2.lat))
clong := math.Cos(rad(p1.long - p2.long))
return w.radius * math.Acos(s1*s2+c1*c2*clong)
}
// rad converts degrees to radians.
func rad(deg float64) float64 {
return deg * math.Pi / 180
}
var (
mars = world{radius: 3389.5}
earth = world{radius: 6371}
)
func main() {
spirit := newLocation(coordinate{14, 34, 6.2, 'S'}, coordinate{175, 28,
21.5, 'E'})
opportunity := newLocation(coordinate{1, 56, 46.3, 'S'}, coordinate{354,
28, 24.2, 'E'})
curiosity := newLocation(coordinate{4, 35, 22.2, 'S'}, coordinate{137,
26, 30.12, 'E'})
insight := newLocation(coordinate{4, 30, 0.0, 'N'}, coordinate{135, 54,
0.0, 'E'})
fmt.Printf("Spirit to Opportunity %.2f km\n", mars.distance(spirit,
opportunity))
fmt.Printf("Spirit to Curiosity %.2f km\n", mars.distance(spirit,
curiosity))
fmt.Printf("Spirit to InSight %.2f km\n", mars.distance(spirit, insight))
fmt.Printf("Opportunity to Curiosity %.2f km\n", mars.distance(opportunity,
curiosity))
fmt.Printf("Opportunity to InSight %.2f km\n", mars.distance(opportunity,
insight))
fmt.Printf("Curiosity to InSight %.2f km\n", mars.distance(curiosity,
insight))
london := newLocation(coordinate{51, 30, 0, 'N'}, coordinate{0, 8, 0, 'W'})
paris := newLocation(coordinate{48, 51, 0, 'N'}, coordinate{2, 21, 0, 'E'})
fmt.Printf("London to Paris %.2f km\n", earth.distance(london, paris))
edmonton := newLocation(coordinate{53, 32, 0, 'N'}, coordinate{113, 30, 0,
'W'})
ottawa := newLocation(coordinate{45, 25, 0, 'N'}, coordinate{75, 41, 0,
'W'})
fmt.Printf("Hometown to Capital %.2f km\n", earth.distance(edmonton,
ottawa))
mountSharp := newLocation(coordinate{5, 4, 48, 'S'}, coordinate{137, 51, 0,
'E'})
olympusMons := newLocation(coordinate{18, 39, 0, 'N'}, coordinate{226, 12,
0, 'E'})
fmt.Printf("Mount Sharp to Olympus Mons %.2f km\n",
mars.distance(mountSharp, olympusMons))
}
第 23 课
实验: gps.go
package main
import (
"fmt"
"math"
)
type world struct {
radius float64
}
type location struct {
name string
lat, long float64
}
func (l location) description() string {
return fmt.Sprintf("%v (%.1f°, %.1f°)", l.name, l.lat, l.long)
}
type gps struct {
world world
current location
destination location
}
func (g gps) distance() float64 {
return g.world.distance(g.current, g.destination)
}
func (g gps) message() string {
return fmt.Sprintf("%.1f km to %v", g.distance(),
g.destination.description())
}
func (w world) distance(p1, p2 location) float64 {
s1, c1 := math.Sincos(rad(p1.lat))
s2, c2 := math.Sincos(rad(p2.lat))
clong := math.Cos(rad(p1.long - p2.long))
return w.radius * math.Acos(s1*s2+c1*c2*clong)
}
func rad(deg float64) float64 {
return deg * math.Pi / 180
}
type rover struct {
gps
}
func main() {
mars := world{radius: 3389.5}
bradbury := location{"Bradbury Landing", -4.5895, 137.4417}
elysium := location{"Elysium Planitia", 4.5, 135.9}
gps := gps{
world: mars,
current: bradbury,
destination: elysium,
}
curiosity := rover{
gps: gps,
}
fmt.Println(curiosity.message()) *1*
}
- 1 打印 545.4 km to Elysium Planitia (4.5°, 135.9°)
第 24 课
实验: marshal.go
package main
import (
"encoding/json"
"fmt"
"os"
)
// coordinate in degrees, minutes, seconds in a N/S/E/W hemisphere.
type coordinate struct {
d, m, s float64
h rune
}
// String formats a DMS coordinate.
func (c coordinate) String() string {
return fmt.Sprintf("%v°%v'%.1f\" %c", c.d, c.m, c.s, c.h)
}
// decimal converts a d/m/s coordinate to decimal degrees.
func (c coordinate) decimal() float64 {
sign := 1.0
switch c.h {
case 'S', 'W', 's', 'w':
sign = -1
}
return sign * (c.d + c.m/60 + c.s/3600)
}
func (c coordinate) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
DD float64 `json:"decimal"`
DMS string `json:"dms"`
D float64 `json:"degrees"`
M float64 `json:"minutes"`
S float64 `json:"seconds"`
H string `json:"hemisphere"`
}{
DD: c.decimal(),
DMS: c.String(),
D: c.d,
M: c.m,
S: c.s,
H: string(c.h),
})
}
// location with a latitude, longitude in decimal degrees.
type location struct {
Name string `json:"name"`
Lat coordinate `json:"latitude"`
Long coordinate `json:"longitude"`
}
func main() {
elysium := location{
Name: "Elysium Planitia",
Lat: coordinate{4, 30, 0.0, 'N'},
Long: coordinate{135, 54, 0.0, 'E'},
}
bytes, err := json.MarshalIndent(elysium, "", " ")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println(string(bytes))
}
核心项目 25
实验: animals.go
package main
import (
"fmt"
"math/rand"
"time"
)
type honeyBee struct {
name string
}
func (hb honeyBee) String() string {
return hb.name
}
func (hb honeyBee) move() string {
switch rand.Intn(2) {
case 0:
return "buzzes about"
default:
return "flies to infinity and beyond"
}
}
func (hb honeyBee) eat() string {
switch rand.Intn(2) {
case 0:
return "pollen"
default:
return "nectar"
}
}
type gopher struct {
name string
}
func (g gopher) String() string {
return g.name
}
func (g gopher) move() string {
switch rand.Intn(2) {
case 0:
return "scurries along the ground"
default:
return "burrows in the sand"
}
}
func (g gopher) eat() string {
switch rand.Intn(5) {
case 0:
return "carrot"
case 1:
return "lettuce"
case 2:
return "radish"
case 3:
return "corn"
default:
return "root"
}
}
type animal interface {
move() string
eat() string
}
func step(a animal) {
switch rand.Intn(2) {
case 0:
fmt.Printf("%v %v.\n", a, a.move())
default:
fmt.Printf("%v eats the %v.\n", a, a.eat())
}
}
const sunrise, sunset = 8, 18
func main() {
rand.Seed(time.Now().UnixNano())
animals := []animal{
honeyBee{name: "Bzzz Lightyear"},
gopher{name: "Go gopher"},
}
var sol, hour int
for {
fmt.Printf("%2d:00 ", hour)
if hour < sunrise || hour >= sunset {
fmt.Println("The animals are sleeping.")
} else {
i := rand.Intn(len(animals))
step(animals[i])
}
time.Sleep(500 * time.Millisecond)
hour++
if hour >= 24 {
hour = 0
sol++
if sol >= 3 {
break
}
}
}
}
单元 6
第 26 课
实验: turtle.go
package main
import "fmt"
type turtle struct {
x, y int
}
func (t *turtle) up() {
t.y--
}
func (t *turtle) down() {
t.y++
}
func (t *turtle) left() {
t.x--
}
func (t *turtle) right() {
t.x++
}
func main() {
var t turtle
t.up()
t.up()
t.left()
t.left()
fmt.Println(t) *1*
t.down()
t.down()
t.right()
t.right()
fmt.Println(t) *2*
}
-
1 打印 {-2 -2}
-
2 打印 {0 0}
第 27 课
实验: knights.go
package main
import (
"fmt"
)
type item struct {
name string
}
type character struct {
name string
leftHand *item
}
func (c *character) pickup(i *item) {
if c == nil || i == nil {
return
}
fmt.Printf("%v picks up a %v\n", c.name, i.name)
c.leftHand = i
}
func (c *character) give(to *character) {
if c == nil || to == nil {
return
}
if c.leftHand == nil {
fmt.Printf("%v has nothing to give\n", c.name)
return
}
if to.leftHand != nil {
fmt.Printf("%v's hands are full\n", to.name)
return
}
to.leftHand = c.leftHand
c.leftHand = nil
fmt.Printf("%v gives %v a %v\n", c.name, to.name, to.leftHand.name)
}
func (c character) String() string {
if c.leftHand == nil {
return fmt.Sprintf("%v is carrying nothing", c.name)
}
return fmt.Sprintf("%v is carrying a %v", c.name, c.leftHand.name)
}
func main() {
arthur := &character{name: "Arthur"}
shrubbery := &item{name: "shrubbery"}
arthur.pickup(shrubbery) *1*
knight := &character{name: "Knight"}
arthur.give(knight) *2*
fmt.Println(arthur) *3*
fmt.Println(knight) *4*
}
-
1 打印 Arthur picks up a shrubbery
-
2 打印 Arthur gives Knight a shrubbery
-
3 打印 Arthur is carrying nothing
-
4 打印 Knight is carrying a shrubbery
第 28 课
实验: url.go
u, err := url.Parse("https://a b.com/")
if err != nil {
fmt.Println(err) *1*
fmt.Printf("%#v\n", err) *2*
if e, ok := err.(*url.Error); ok {
fmt.Println("Op:", e.Op) *3*
fmt.Println("URL:", e.URL) *4*
fmt.Println("Err:", e.Err) *5*
}
os.Exit(1)
}
fmt.Println(u)
-
1 打印 parse https://a b.com/: 主机名中存在无效字符 “ ”
-
2 打印 &url.Error{Op:“parse”, URL:“https://a b.com/”, Err:” “}
-
3 打印 Op: parse
-
4 打印 URL: https://a b.com/
-
5 打印 Err: invalid character “ ” in host name
核心项目 29
实验: sudoku.go
package main
import (
"errors"
"fmt"
"os"
)
const (
rows, columns = 9, 9
empty = 0
)
// Cell is a square on the Sudoku grid.
type Cell struct {
digit int8
fixed bool
}
// Grid is a Sudoku grid.
type Grid [rows][columns]Cell
// Errors that could occur.
var (
ErrBounds = errors.New("out of bounds")
ErrDigit = errors.New("invalid digit")
ErrInRow = errors.New("digit already present in this row")
ErrInColumn = errors.New("digit already present in this column")
ErrInRegion = errors.New("digit already present in this region")
ErrFixedDigit = errors.New("initial digits cannot be overwritten")
)
// NewSudoku makes a new Sudoku grid.
func NewSudoku(digits [rows][columns]int8) *Grid {
var grid Grid
for r := 0; r < rows; r++ {
for c := 0; c < columns; c++ {
d := digits[r][c]
if d != empty {
grid[r][c].digit = d
grid[r][c].fixed = true
}
}
}
return &grid
}
// Set a digit on a Sudoku grid.
func (g *Grid) Set(row, column int, digit int8) error {
switch {
case !inBounds(row, column):
return ErrBounds
case !validDigit(digit):
return ErrDigit
case g.isFixed(row, column):
return ErrFixedDigit
case g.inRow(row, digit):
return ErrInRow
case g.inColumn(column, digit):
return ErrInColumn
case g.inRegion(row, column, digit):
return ErrInRegion
}
g[row][column].digit = digit
return nil
}
// Clear a cell from the Sudoku grid.
func (g *Grid) Clear(row, column int) error {
switch {
case !inBounds(row, column):
return ErrBounds
case g.isFixed(row, column):
return ErrFixedDigit
}
g[row][column].digit = empty
return nil
}
func inBounds(row, column int) bool {
if row < 0 || row >= rows || column < 0 || column >= columns {
return false
}
return true
}
func validDigit(digit int8) bool {
return digit >= 1 && digit <= 9
}
func (g *Grid) inRow(row int, digit int8) bool {
for c := 0; c < columns; c++ {
if g[row][c].digit == digit {
return true
}
}
return false
}
func (g *Grid) inColumn(column int, digit int8) bool {
for r := 0; r < rows; r++ {
if g[r][column].digit == digit {
return true
}
}
return false
}
func (g *Grid) inRegion(row, column int, digit int8) bool {
startRow, startColumn := row/3*3, column/3*3
for r := startRow; r < startRow+3; r++ {
for c := startColumn; c < startColumn+3; c++ {
if g[r][c].digit == digit {
return true
}
}
}
return false
}
func (g *Grid) isFixed(row, column int) bool {
return g[row][column].fixed
}
func main() {
s := NewSudoku([rows][columns]int8{
{5, 3, 0, 0, 7, 0, 0, 0, 0},
{6, 0, 0, 1, 9, 5, 0, 0, 0},
{0, 9, 8, 0, 0, 0, 0, 6, 0},
{8, 0, 0, 0, 6, 0, 0, 0, 3},
{4, 0, 0, 8, 0, 3, 0, 0, 1},
{7, 0, 0, 0, 2, 0, 0, 0, 6},
{0, 6, 0, 0, 0, 0, 2, 8, 0},
{0, 0, 0, 4, 1, 9, 0, 0, 5},
{0, 0, 0, 0, 8, 0, 0, 7, 9},
})
err := s.Set(1, 1, 4)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
for _, row := range s {
fmt.Println(row)
}
}
单元 7
第 30 课
实验: remove-identical.go
package main
import (
"fmt"
)
func main() {
c0 := make(chan string)
c1 := make(chan string)
go sourceGopher(c0)
go removeDuplicates(c0, c1)
printGopher(c1)
}
func sourceGopher(downstream chan string) {
for _, v := range []string{"a", "b", "b", "c", "d", "d", "d", "e"} {
downstream <- v
}
close(downstream)
}
func removeDuplicates(upstream, downstream chan string) {
prev := ""
for v := range upstream {
if v != prev {
downstream <- v
prev = v
}
}
close(downstream)
}
func printGopher(upstream chan string) {
for v := range upstream {
fmt.Println(v)
}
}
实验: split-words.go
package main
import (
"fmt"
"strings"
)
func main() {
c0 := make(chan string)
c1 := make(chan string)
go sourceGopher(c0)
go splitWords(c0, c1)
printGopher(c1)
}
func sourceGopher(downstream chan string) {
for _, v := range []string{"hello world", "a bad apple", "goodbye all"}
{
downstream <- v
}
close(downstream)
}
func splitWords(upstream, downstream chan string) {
for v := range upstream {
for _, word := range strings.Fields(v) {
downstream <- word
}
}
close(downstream)
}
func printGopher(upstream chan string) {
for v := range upstream {
fmt.Println(v)
}
}
第 31 课
实验: positionworker.go
package main
import (
"fmt"
"image"
"time"
)
func main() {
go worker()
time.Sleep(5 * time.Second)
}
func worker() {
pos := image.Point{X: 10, Y: 10}
direction := image.Point{X: 1, Y: 0}
delay := time.Second
next := time.After(delay)
for {
select {
case <-next:
pos = pos.Add(direction)
fmt.Println("current position is ", pos)
delay += time.Second / 2
next = time.After(delay)
}
}
}
实验: rover.go
package main
import (
"image"
"log"
"time"
)
func main() {
r := NewRoverDriver()
time.Sleep(3 * time.Second)
r.Left()
time.Sleep(3 * time.Second)
r.Right()
time.Sleep(3 * time.Second)
r.Stop()
time.Sleep(3 * time.Second)
r.Start()
time.Sleep(3 * time.Second)
}
// RoverDriver drives a rover around the surface of Mars.
type RoverDriver struct {
commandc chan command
}
// NewRoverDriver starts a new RoverDriver and returns it.
func NewRoverDriver() *RoverDriver {
r := &RoverDriver{
commandc: make(chan command),
}
go r.drive()
return r
}
type command int
const (
right = command(0)
left = command(1)
start = command(2)
stop = command(3)
)
// drive is responsible for driving the rover. It
// is expected to be started in a goroutine.
func (r *RoverDriver) drive() {
pos := image.Point{X: 0, Y: 0}
direction := image.Point{X: 1, Y: 0}
updateInterval := 250 * time.Millisecond
nextMove := time.After(updateInterval)
speed := 1
for {
select {
case c := <-r.commandc:
switch c {
case right:
direction = image.Point{
X: -direction.Y,
Y: direction.X,
}
case left:
direction = image.Point{
X: direction.Y,
Y: -direction.X,
}
case stop:
speed = 0
case start:
speed = 1
}
log.Printf("new direction %v; speed %d", direction, speed)
case <-nextMove:
pos = pos.Add(direction.Mul(speed))
log.Printf("moved to %v", pos)
nextMove = time.After(updateInterval)
}
}
}
// Left turns the rover left (90° counterclockwise).
func (r *RoverDriver) Left() {
r.commandc <- left
}
// Right turns the rover right (90° clockwise).
func (r *RoverDriver) Right() {
r.commandc <- right
}
// Stop halts the rover.
func (r *RoverDriver) Stop() {
r.commandc <- stop
}
// Start gets the rover moving.
func (r *RoverDriver) Start() {
r.commandc <- start
}
核心项目 32
实验: lifeonmars.go
package main
import (
"fmt"
"image"
"log"
"math/rand"
"sync"
"time"
)
func main() {
marsToEarth := make(chan []Message)
go earthReceiver(marsToEarth)
gridSize := image.Point{X: 20, Y: 10}
grid := NewMarsGrid(gridSize)
rover := make([]*RoverDriver, 5)
for i := range rover {
rover[i] = startDriver(fmt.Sprint("rover", i), grid, marsToEarth)
}
time.Sleep(60 * time.Second)
}
// Message holds a message as sent from Mars to Earth.
type Message struct {
Pos image.Point
LifeSigns int
Rover string
}
const (
// The length of a Mars day.
dayLength = 24 * time.Second
// The length of time per day during which
// messages can be transmitted from a rover to Earth.
receiveTimePerDay = 2 * time.Second
)
// earthReceiver receives messages sent from Mars.
// As connectivity is limited, it only receives messages
// for some time every Mars day.
func earthReceiver(msgc chan []Message) {
for {
time.Sleep(dayLength - receiveTimePerDay)
receiveMarsMessages(msgc)
}
}
// receiveMarsMessages receives messages sent from Mars
// for the given duration.
func receiveMarsMessages(msgc chan []Message) {
finished := time.After(receiveTimePerDay)
for {
select {
case <-finished:
return
case ms := <-msgc:
for _, m := range ms {
log.Printf("earth received report of life sign level %d from
%s at %v", m.LifeSigns, m.Rover, m.Pos)
}
}
}
}
func startDriver(name string, grid *MarsGrid, marsToEarth chan []Message)
*RoverDriver {
var o *Occupier
// Try a random point; continue until we've found one that's
// not currently occupied.
for o == nil {
startPoint := image.Point{X: rand.Intn(grid.Size().X), Y: rand.Intn(
grid.Size().Y)}
o = grid.Occupy(startPoint)
}
return NewRoverDriver(name, o, marsToEarth)
}
// Radio represents a radio transmitter that can send
// message to Earth.
type Radio struct {
fromRover chan Message
}
// SendToEarth sends a message to Earth. It always
// succeeds immediately - the actual message
// may be buffered and actually transmitted later.
func (r *Radio) SendToEarth(m Message) {
r.fromRover <- m
}
// NewRadio returns a new Radio instance that sends
// messages on the toEarth channel.
func NewRadio(toEarth chan []Message) *Radio {
r := &Radio{
fromRover: make(chan Message),
}
go r.run(toEarth)
return r
}
// run buffers messages sent by a rover until they
// can be sent to Earth.
func (r *Radio) run(toEarth chan []Message) {
var buffered []Message
for {
toEarth1 := toEarth
if len(buffered) == 0 {
toEarth1 = nil
}
select {
case m := <-r.fromRover:
buffered = append(buffered, m)
case toEarth1 <- buffered:
buffered = nil
}
}
}
// RoverDriver drives a rover around the surface of Mars.
type RoverDriver struct {
commandc chan command
occupier *Occupier
name string
radio *Radio
}
// NewRoverDriver starts a new RoverDriver and returns it.
func NewRoverDriver(
name string,
occupier *Occupier,
marsToEarth chan []Message,
) *RoverDriver {
r := &RoverDriver{
commandc: make(chan command),
occupier: occupier,
name: name,
radio: NewRadio(marsToEarth),
}
go r.drive()
return r
}
type command int
const (
right command = 0
left command = 1
)
// drive is responsible for driving the rover. It
// is expected to be started in a goroutine.
func (r *RoverDriver) drive() {
log.Printf("%s initial position %v", r.name, r.occupier.Pos())
direction := image.Point{X: 1, Y: 0}
updateInterval := 250 * time.Millisecond
nextMove := time.After(updateInterval)
for {
select {
case c := <-r.commandc:
switch c {
case right:
direction = image.Point{
X: -direction.Y,
Y: direction.X,
}
case left:
direction = image.Point{
X: direction.Y,
Y: -direction.X,
}
}
log.Printf("%s new direction %v", r.name, direction)
case <-nextMove:
nextMove = time.After(updateInterval)
newPos := r.occupier.Pos().Add(direction)
if r.occupier.MoveTo(newPos) {
log.Printf("%s moved to %v", r.name, newPos)
r.checkForLife()
break
}
log.Printf("%s blocked trying to move from %v to %v", r.name,
r.occupier.Pos(), newPos)
// Pick one of the other directions randomly.
// Next time round, we'll try to move in the new
// direction.
dir := rand.Intn(3) + 1
for i := 0; i < dir; i++ {
direction = image.Point{
X: -direction.Y,
Y: direction.X,
}
}
log.Printf("%s new random direction %v", r.name, direction)
}
}
}
func (r *RoverDriver) checkForLife() {
// Successfully moved to new position.
sensorData := r.occupier.Sense()
if sensorData.LifeSigns < 900 {
return
}
r.radio.SendToEarth(Message{
Pos: r.occupier.Pos(),
LifeSigns: sensorData.LifeSigns,
Rover: r.name,
})
}
// Left turns the rover left (90° counterclockwise).
func (r *RoverDriver) Left() {
r.commandc <- left
}
// Right turns the rover right (90° clockwise).
func (r *RoverDriver) Right() {
r.commandc <- right
}
// MarsGrid represents a grid of some of the surface
// of Mars. It may be used concurrently by different
// goroutines.
type MarsGrid struct {
bounds image.Rectangle
mu sync.Mutex
cells [][]cell
}
// SensorData holds information about what's in
// a point in the grid.
type SensorData struct {
LifeSigns int
}
type cell struct {
groundData SensorData
occupier *Occupier
}
// NewMarsGrid returns a new MarsGrid of the
// given size.
func NewMarsGrid(size image.Point) *MarsGrid {
grid := &MarsGrid{
bounds: image.Rectangle{
Max: size,
},
cells: make([][]cell, size.Y),
}
for y := range grid.cells {
grid.cells[y] = make([]cell, size.X)
for x := range grid.cells[y] {
cell := &grid.cells[y][x]
cell.groundData.LifeSigns = rand.Intn(1000)
}
}
return grid
}
// Size returns a Point representing the size of the grid.
func (g *MarsGrid) Size() image.Point {
return g.bounds.Max
}
// Occupy occupies a cell at the given point in the grid. It
// returns nil if the point is already occupied or the point is outside
// the grid. Otherwise it returns a value that can be used
// to move to different places on the grid.
func (g *MarsGrid) Occupy(p image.Point) *Occupier {
g.mu.Lock()
defer g.mu.Unlock()
cell := g.cell(p)
if cell == nil || cell.occupier != nil {
return nil
}
cell.occupier = &Occupier{
grid: g,
pos: p,
}
return cell.occupier
}
func (g *MarsGrid) cell(p image.Point) *cell {
if !p.In(g.bounds) {
return nil
}
return &g.cells[p.Y][p.X]
}
// Occupier represents an occupied cell in the grid.
type Occupier struct {
grid *MarsGrid
pos image.Point
}
// MoveTo moves the occupier to a different cell in the grid.
// It reports whether the move was successful
// It might fail because it was trying to move outside
// the grid or because the cell it's trying to move into
// is occupied. If it fails, the occupier remains in the same place.
func (o *Occupier) MoveTo(p image.Point) bool {
o.grid.mu.Lock()
defer o.grid.mu.Unlock()
newCell := o.grid.cell(p)
if newCell == nil || newCell.occupier != nil {
return false
}
o.grid.cell(o.pos).occupier = nil
newCell.occupier = o
o.pos = p
return true
}
// Sense returns sensory data from the current cell.
func (o *Occupier) Sense() SensorData {
o.grid.mu.Lock()
defer o.grid.mu.Unlock()
return o.grid.cell(o.pos).groundData
}
// Pos returns the current grid position of the occupier.
func (o *Occupier) Pos() image.Point {
return o.pos
}



浙公网安备 33010602011771号