Go是一个开源项目,它由Google一个名为“Go团队”的分布式团队维护。该项目由编程语言规范、编译器、工具、文档和标准库组成。
让我们通过一些事实和最佳实践来了解Go的基础知识及其在快进模式下的特征。虽然一些建议可能会让人觉得武断,但都是基于我自2014年以来使用Go的经验。这些经验充满了小插曲、错误和我们吸取的惨痛教训。笔者在这里分享它们,以为前车之鉴。
Go项目的核心部分是同名的通用语言,主要用于系统编程。正如你在示例代码2-1中注意到的,Go是一种命令式语言,我们可以对事情的执行方式进行(某种)控制。此外,它是静态类型和可编译的,这意味着编译器可以在程序运行之前进行许多优化和检查。仅凭这些特性就足以使Go适用于可靠和高效的程序。
代码示例2-1:输出“Hello World”并退出程序的例子
项目和语言都称为“Go”,但有时你也可以将它们称为“Golang”。
Go与Golang
根据经验,我们可以在任何地方使用“Go”这个名称,除非它与英文单词go或一种名为“Go”的古老游戏冲突。“Golang”来自域选择( https://golang.org ),因为“go”对其作者不可用。因此,在网络上搜索有关该编程语言的资源时,请使用“Golang”。
Go也有它的吉祥物,称为“Go gopher”( https://oreil.ly/SbxVX )。我们经常在各种形式、情况和组合中看到这只可爱的地鼠,例如,会议演讲、博客文章或项目徽标里。有时Go开发人员也被称为“gophers”!
这一切都始于2007年左右,来自谷歌的3位经验丰富的程序员勾勒出Go语言的构想:
Rob Pike
UTF-8和Plan 9操作系统的联合创始人。在Go之前是许多编程语言的合著者,例如用于编写分布式系统的Limbo和用于在图形用户界面中编写并发应用程序的Newsqueak。两者都受到Hoare的通信顺序过程(Communicating Sequential Processes,C S P)的启发
。
Robert Griesemer
在其他工作中,Griesemer开发了Sawzall语言( https://oreil.ly/gYKMj )并与Niklaus Wirth一起获得了博士学位。同样,Niklaus撰写的“A Plea for Lean Software”在1.2.3节中被引用。
Ken Thompson
首代Unix系统的原作者之一。grep命令行实用程序的唯一创建者。Ken与Rob Pike共同创建了UTF-8和Plan 9。他也写过几种语言,例如Bon和B语言。
他们三位旨在创建一种新的编程语言,旨在改进当时以C++、Java和Python为首的主流编程。一年后,随着Ian Taylor和Russ Cox于2008年的加入,它变成了一个专项项目,该团队后来被称为Go团队( https://oreil.ly/Nnj6N )。Go团队于2009年宣布公开Go项目,并于2012年3月发布了1.0版本。
在Go的设计中提到的与C++相关的主要劣势 [4] 有如下几点:
· 复杂性,做同一件事的方法很多,特性太多。
· 编译时间超长,尤其是对于较大的代码库。
· 大型项目中的更新和重构成本。
· 不易使用且内存模型容易出错。
这些因素则是Go诞生的原因,它源于对现有解决方案的不满,以及通过少做多得来实现更多目标的雄心。这些指导原则是创造一种语言,它不会因减少重复而牺牲安全性,且能接受更简单的代码。它不会为了更快的编译或解释而牺牲执行效率,同时确保构建时间足够快。Go会尽量加快编译速度,例如通过显式导入( https://oreil.ly/qxuUS )。特别是在默认启用缓存的情况下,它仅编译更改的代码,因此构建时间很少超过一分钟。
你可以将Go代码视为脚本!
虽然从技术上讲Go是一种编译语言,但你可以像运行JavaScript、Shell或Python一样运行它。它就像调用go run <executable package> <flags>一样简单。由于它编译速度超快,因此可以出色地工作。你可以将其视为一种脚本语言,且同时保持编译的优势。
在语法方面,Go应该是简单、关键字高亮和令人熟知的。语法基于C语言,具有类型推导(自动类型检测,如C++中的auto),并且没有前向声明和头文件。概念保持正交,这使得其更容易组合和推理。元素的正交性意味着我们可以向任何类型或数据定义添加方法(添加方法与创建类型是分开的)。接口与类型同样也是正交的。
自从宣布Go以来,所有的开发都是在开源环境中完成的( https://oreil.ly/ZeKm6 ),并有公共邮件列表和bug跟踪器。更改涉及公共、权威源代码,采用BSD样式许可证( https://oreil.ly/XBDEK ),并由Go团队审查所有贡献。无论更改或想法是否来自谷歌,这个过程都是一样的。项目路线图和提案也是公开制定的。
不幸的是,虽然有很多开源项目,但有些项目的开放程度不如其他项目。谷歌仍然是唯一一家管理Go的公司,并对它拥有最终的决定性控制权。即使任何人都可以修改、使用和贡献,由单个供应商协调的项目也存在做出自私和破坏性决定的风险,如重新许可或阻止某些功能。虽然在一些有争议的情况下,Go团队的决定让社区感到惊讶 [5] ,但总体而言,该项目的管理是相当合理的。很多变化来自谷歌之外,Go2.0草案提案过程得到了很好的尊重和社区的推动。最后,我相信Go团队的一致决策和管理也会带来很多好处。不同的观点和冲突是不可避免的,只要大家有一致的基本共识,即使不完美,也比没有决定或用多种方法做同一件事更好。
到目前为止,这个项目设置已被证明在采用和语言稳定性方面运行良好。对于软件的效率目标而言,这样的结合再好不过了。我们建立并投资了一家大公司,以确保每次发行新版本都不会带来任何性能下降。一些谷歌内部软件依赖于Go,例如谷歌云平台( https://oreil.ly/vjyOc )。许多人依赖谷歌云平台的可靠性。另一方面,我们有一个庞大的Go社区,可以提供反馈、发现错误并提供想法和优化。如果这还不够,我们还有开源代码,以允许我们深入研究实际的Go库、运行环境(见2.2.3节)等,以了解特定的性能特征代码。
Robert Griesemer在GopherCon 2015( https://oreil.ly/s3ZZ5 )中提到,当他们第一次开始构建Go时,他们知道哪些事情不能做。主要指导原则是简单、安全和易读。换句话说,Go遵循“少即是多”的模式。这是一个跨越许多领域的至理名言。在Go中,只有一种惯用的编码风格 [6] ,而一个名为gofmt的工具可以确保其中的大部分风格一致。特别是代码格式(仅次于命名)是程序员很少解决的一个问题。我们花时间争论它,并根据我们的特定需求和信仰对其进行调整。由于通过工具实现了单一风格,我们节省了大量时间。正如一条Go谚语所说( https://oreil.ly/ua2G8 ),“gofmt的风格无人喜欢,但gofmt是每个人的最爱。”Go的作者们计划将语言设计最小化,以便基本上只用一种方法来编写特定的结构。当你编写程序时,这会减少很多决策。比如用一种方法处理错误,一种方法编写对象,一种方法并发运行,等等。
Go可能“缺少”大量功能,但可以说它比C或C++更具表现力( https://oreil.ly/CPkvV )。这种极简主义风格允许Go保持代码的简单性和可读性,从而提高软件的可靠性、安全性,并且提升应用程序目标落地的整体速度。
我的代码地道吗?
“地道”一词在Go社区中被过度使用了。通常,它意味着“经常”使用的Go模式。由于Go的采用率已经大幅提高,人们将最初的“地道”风格做了许多创造性的改进。如今,并不能说清楚什么才是“地道”的风格。
这就像曼达洛系列中的“This is the way”。当我们说“这段代码是地道的”时,它能让我们更加自信。所以要谨慎使用这个词,或者说避免使用它,除非你能详细说明为什么你的方法更好( https://oreil.ly/dAAKz )。
有趣的是,“少即是多”这个成语可以帮助我们提高效率。正如我们在第1章中所了解到的,如果在运行环境中做少量的工作,这通常意味着程序拥有更快的执行速度、更精简和更低复杂度的代码。在本书中,我们将尝试在提高代码性能的同时保持这些方面。
Go源代码以包或模块的形式被安置在目录中。在同一目录中,包是源文件的集合(源文件带有 .go 后缀)。包名在每个源文件的顶部用package的语法指定,如示例2-1所示。同一目录中的所有文件必须具有相同的包名 [7] (包名可以与目录名不同)。多个包可以是单个Go模块的一部分。模块是一个包含 go.mod 文件的目录,该文件声明所有依赖模块及其构建Go应用程序所需的版本。这个文件后续会被依赖管理工具Go Modules( https://oreil.ly/z5GqG )使用。模块中的每个源文件都可以从其所在的模块或外部模块导入包。一些包也可以是“可执行的”。例如,如果一个包名为main并且在某个文件中有func main(),我们就可以执行它。为了方便找到这种包,有时他们会放在cmd目录中。请注意,你不能导入可执行包。你只能构建或运行它。
在包内,你可以决定将哪些函数、类型、接口和方法暴露给包的用户,哪些只能在包范围内访问。这很重要,因为暴露出尽可能少的API以获得更好的可读性、可重用性和可靠性。Go对此没有任何private或public关键字。相反,它采用了一种比较新颖的方法。如代码示例2-2所示,如果函数、类型、接口、变量的名称以大写字母开头,则包外的任何代码都可以使用。如果名称以小写字母开头,则是私有的。
代码示例2-2:使用命名大小写实现构造可访问性控制
❶ 细心的读者可能会注意到代码中的奇怪之处,即在私有类型或接口中对外暴露了类型或方法。如果结构或接口是私有的,包外的人可以使用它们吗?答案是肯定的,你可以在公共函数中返回私有接口或类型,但是这种用法极少,例如,func New()privateStruct { return privateStruct{}}。尽管privateStruct是私有的,但包的用户可以访问其所有公共字段和方法。
内部包
你可以根据需要命名和构建代码目录以形成包,但目录名称是为特殊含义保留的。如果你确保只有指定的包才能导入其他包,可以创建一个名为internal的包子目录。internal目录下的任何包都不能被祖先以外的任何包导入(包括internal中的其他包)。
根据笔者的经验,导入预编译库(例如C++、C#或Java)并使用某些头文件中定义的导出函数和类是很常见的。不管怎样,导入编译后的代码是有一些好处:
· 它使工程师不必费力编译特定代码,即可查找和下载依赖项的对应版本、特殊编译工具或额外的资源。
· 在暴露开源代码和担心客户复制能提供商业价值的代码的情况下,销售这样一个预构建的库可能会更容易
。
原则上,这意味着它可以很好地工作。库的开发人员维护特定的编程协议(API),并且此类库的用户不需要担心其实现复杂性。
不幸的是,在现实中这很难完美地实现。落地过程中可能会失败或效率低,接口可能会产生误导,文档也可能会丢失。在这种情况下,访问源代码是非常重要的,它使我们能够更深入地了解实现。我们可以根据具体的源代码来发现问题,而不是靠猜。我们甚至可以修复工具库或分包并立即使用它还可以提取所需的部分并使用它们来构建其他工具库。
Go通过使用名为“导入路径”的包URI来显式导入所需要库的各个部分(在Go中称为“模块的包”)来承担这种可能存在的瑕疵。此类导入也会受到严格控制,即未使用的导入或循环依赖会导致编译错误。让我们看看在代码示例2-3中声明的不同的导入方法。
代码示例2-3:main.go文件中来自 github.com/prometheus/prometheus 模块的部分导入语句
❶ 如果导入声明中没有带路径结构的域,则表示导入了“standard” [8] 库中的包。这个特殊的导入方式允许我们使用$(go env GOROOT)/src/context/目录中的代码,这些代码通过context引用,例如context.Background()。
❷ 可以显式导入包,而不需要任何标识符。我们不想引用这个包中的任何构造,但我们希望初始化一些全局变量。在这种情况下,pprof包将调试端点添加到全局HTTP服务路由器中。虽然这种方式可行,但在实践中我们应该避免重复使用全局的、可修改的变量。
❸ 非标准包可以使用互联网域名形式的导入路径和特定模块中包的可选路径导入。例如,Go工具与https://github.com能很好地集成。因此如果你将Go代码托管在Git存储库中,它会找到指定的包。在本例中,github.com/oklog/run模块中有run包,它则是https://github.com/oklog/run Git存储库。
❹ 如果包取自当前模块(在本例中,我们的模块是github.com/prometheus/prometheus),包将从你的本地目录解析。在我们的示例中,是导入<module root>/config模块。
这种设计侧重于开放和明确定义的依赖关系。它与开源分发模型配合得非常好,社区可以在公共Git存储库中协作开发强大的包。当然,也可以使用标准版本控制身份验证协议来隐藏模块或包。此外,官方工具不支持以二进制形式分发包( https://oreil.ly/EnkBT ),因此强烈建议在编译时提供依赖源。
软件的依赖性的问题并不容易解决。Go从C++和其他语言的错误中吸取了教训,并采取了谨慎的方法来避免漫长的编译时间,以及通常被称为“依赖地狱”的影响。
通过标准库的设计,我们在控制依赖关系方面做出了巨大努力。为了使用一个函数,复制一点代码比引入一个大库更好。(系统构建中的测试会抱怨新核心依赖项的出现)依赖的清爽胜过代码重用。这在实践中的一个例子是(低级)网络包有它自己的整数到十进制的例行转换程序,以避免依赖更大和依赖性强的格式化I/O包。另一个是字符串转换包strconv有一个“可打印”字符定义的私有实现,而不是引入大型Unicode字符类表;strconv遵循的Unicode标准已通过包的测试,从而得到了验证。
——Rob Pike,“Go at Google:Language Design in the Service of Software Engineering”( https://oreil.ly/wqKGT )
再次强调,在追求效率的大方向下,依赖性和透明度的潜在极简主义带来了巨大的价值。更少的未知因素意味着我们可以快速发现主要瓶颈,并首先关注最重要的价值优化。如果我们注意到依赖项中存在潜在的优化空间,我们就不需要围绕它进行工作。相反,我们欢迎直接向上游贡献优化改进,这对双方都有帮助!
从一开始,Go就拥有一套强大而一致的工具,其作为其命令行界面工具的一部分,称为go。列举一些实用程序:
· go bug打开一个新的浏览器标签,其中包含可以提交正式bug报告的正确位置(GitHub上的Go存储库)。
· go build -o <output path> <packages>命令,构建给定的Go包。
· go env显示当前终端会话中设置的与Go相关的所有环境变量。
· go fmt <file,packages or directories>会将给定的代码格式化为所需的样式,比如清除空白、修复错误的缩进等。源代码甚至不需要是有效的和可编译的Go代码。你还可以安装一个扩展的官方格式化工具goimports( https://oreil.ly/6fDcy ),它可以额外清理和格式化你的导入语句。
为了获得最佳的体验,请将你的IDE设置为在每个文件上运行goimports-w $FILE,这样就不用再担心手动缩进了!
· go get <package@version>允许你安装所要求依赖项的预期版本。使用@latest后缀获取@none的最新版本以卸载依赖项。
· go help <command/topic>打印有关命令或给定主题的文档。例如,go help environment告诉你所有关于Go可能使用的环境变量。
· go install <package>类似于go get,如果给定的包是“可执行的”,则安装相应的二进制文件。
· go list列出Go的包和模块。它允许使用Go模板(稍后解释)进行灵活的输出格式化,例如,go list-mod=readonly-m-f'{{ if and(not.Indirect)(not.Main)}}{{.Path}}{{end}}' all列出所有直接非可执行依赖模块。
· go mod允许管理依赖模块。
· go test允许运行单元测试、模糊测试和基准测试。我们会在第8章详细讨论后者。
· go tool拥有十几个更高级的CLI工具。我们将在9.2.1节中特别关注go tool pprof,以进行性能优化。
· go vet运行基本的静态分析检查。
在大多数情况下,Go CLI就是你进行高效Go编程时所需的全部了 [9] 。
错误是每个运行的软件中不可避免的一部分。尤其是在分布式系统,它们一般具有处理不同类型故障的高级研究和算法 [10] 。尽管错误提示是必要的,但大多数编程语言并不推荐或强制使用特定的故障处理方式。例如,在C++中,你会看到程序员使用多种方法从函数返回错误:
· 异常(Exceptions)。
· 整型返回码(如果返回值非零,则表示错误)。
· 隐式状态码 [11] 。
· 其他标记值(如果返回值为空,则为错误)。
· 通过参数返回潜在错误。
· 自定义错误类。
· 单子(Monads)
。
每种选择都有其利弊,但事实上,用如此多的方法处理错误可能会导致严重的问题。它可能会隐藏一些语句并导致返回错误,从而引起意外,引入复杂性,使我们的软件不可靠。
当然,提供多选项的用意是好的。它为开发人员提供了更多选择。也许你创建的软件不是很关键,或者是第一次迭代,所以你想让“快乐路径”十分清楚。在这种情况下,掩盖一些“坏路径”似乎是一个不错的短期想法,对吧?不幸的是,与许多捷径一样,它会带来许多风险。软件的复杂性和对功能的需求导致代码永远不会超出“第一次迭代”,而非关键代码很快就会成为关键代码的依赖项。这是导致软件不可靠或难以调试的罪魁祸首之一。
Go采取了一种独特的方法,即将错误视为一种首要的语言特性。它假设我们想要编写可靠的软件,使错误处理显式、简单和统一地在库与接口之间进行。让我们看一下代码示例2-4中的一些例子。
代码示例2-4:具有不同返回参数的多个函数签名
❶ 这里的关键点是函数和方法将错误流定义为它们签名的一部分。在这种情况下,noErrCanHappen函数声明在其调用期间不会发生任何错误。
❷ 通过查看doOrErr函数签名,我们知道可能会发生一些错误。我们还不知道是什么类型的错误,只知道它正在实现一个内置的错误接口。我们也知道如果错误为nil,则表示没有错误。
❸ 在“快乐路径”中计算某些结果时,可以利用Go函数能返回多个参数的能力。如果可能发生错误,它应该总是最后一个返回参数。从调用方来看,如果错误为零,我们应该只接触结果。
值得注意的是,Go有一个名为panic的异常机制,可以使用内置函数recover()进行恢复。它虽然在某些情况下有用或者是必要的(例如初始化),但在生产代码实践中,你不应该使用panic机制来处理常规错误。它们效率较低,隐藏了错误,总的来说会带来许多意外。将错误作为调用的一部分,使得编译器和程序员能在正常执行路径中为应对错误情况做好准备。代码示例2-5展示了如果错误发生在我们的函数执行路径中,我们可以如何处理错误。
代码示例2-5:检查和处理错误
❶ 请注意,我们没有导入内置的errors包,而是使用了开源的直接替代品github.com/efficientgo/core/errors的core模块。这是我推荐的替代品,可用来替换errors包和当前正流行的github.com/pkg/errors包。
❷ 要判断是否发生了错误,我们需要检查err变量是否为nil。如果发生错误,我们可以进行错误处理。通常,这意味着记录它、退出程序、增加指标,甚至明确忽略它。
❸ 有时,将错误处理委托给调用者是合适的。例如,如果函数因许多错误而失败,请考虑使用errors.Wrap函数对其进行包装,以添加错误的短上下文。例如,使用github.com/efficientgo/core/errors,我们将拥有上下文和堆栈跟踪信息,如果后面使用%+v,这些内容将被呈现出来。
如何包装错误
请注意,我建议使用errors.Wrap(或errors.Wrapf),而不是内置的包装错误的方法。Go为了允许传递错误的fmt.Errors类型函数定义了%w标识符。目前,我不建议使用%w,因为它不是类型安全的,也不像Wrap那样明确,过去曾引起过一些不明显的bug。
只通过这种方式定义错误和处理错误是Go最好的特性之一。有趣的是,由于过于冗长和某些复杂难懂的样板文件,这也成为它在语言上的一个缺点。它有时可能会让人觉得重复,但一些工具可以帮你减少样板文件。
一些Go IDE定义了代码模板。例如,在JetBrain的GoLand产品中,键入err并按Tab键,将生成if err!=nil语句。你还可以通过折叠或展开错误处理块来提高可读性。
另一个常见的抱怨是,编写Go让人感到非常“悲观”,因为它把那些可能永远不会发生的错误也显现出来了。程序员必须在每一步都决定如何处理它们,这需要耗费脑力和时间。然而,根据笔者的经验,这样做是值得的,并且可以使程序变得更加可预见且更容易调试。
永远不要忽略错误!
由于错误处理的冗长,很容易发生跳过err!=nil检查的情况。除非你知道一个函数永远不会返回错误(未来的版本也一样!),否则不要考虑这样做。如果你不知道如何处理错误,请考虑默认将其传递给调用者。如果你必须忽略错误,就考虑明确地执行“_=”语法。此外,始终使用linters,它会警告你某些未检查的错误。
错误处理对一般Go代码的运行时效率是否有影响?是的!不幸的是,这种影响比开发人员通常预期的还要显著得多。根据笔者的经验,错误路径通常比正常路径慢一个数量级,并且执行成本更高。其中一个原因是,在监控或基准测试步骤中,我们往往不会忽略错误流(见3.3节)。
另一个常见的原因是,构建错误通常涉及大量的字符串操作,以创建人类可读的消息。因此,它可能代价高昂,特别是对于冗长的调试标签,这将在本书的后面部分提到。理解这些含义并确保一致性和有效的错误处理,对于任何软件来说都是必不可少的,我们将在接下来的章节中详细介绍。
对于Go这样一个“年轻”的语言来说,它的一个普遍的优点是其生态系统非常成熟。虽然本节中列出的项目对于扎实的编程语言来说不是强制性的,但它们改善了整个开发体验。这也是Go社区如此庞大且仍在增长的原因。
首先,Go允许程序员专注于业务逻辑,而不必为YAML解码或加密哈希算法等基本功能重新实现或导入第三方库。Go标准库质量高、健壮、超向后兼容且功能丰富。它们具有良好的基准测试、可靠的API和良好的文档。因此,你可以在不导入外部包的情况下实现大部分目标。例如,运行HTTP服务非常简单,如代码示例2-6所示。
代码示例2-6:服务HTTP请求的最少代码
在大多数情况下,标准库的效率足够好,甚至优于第三方替代方案。例如,尤其对于包的低级元素,比如HTTP客户端和服务器代码的net/http,或者是crypto、math和sort等元素(以及更多元素),有大量的优化来服务于大多数用例。这使得开发人员可以在不担心排序性能等基本问题的情况下构建更复杂的代码。然而情况并非总是如此。有些库是为特定用途设计的,滥用它们可能会导致严重的资源浪费。我们将在第11章中介绍你需要注意的所有事项。
成熟生态系统的另一个亮点是,在浏览器中内置一个基础的官方的Go编辑器,名为Go Playground( https://oreil.ly/9Os3y )。如果你想快速测试某些东西或分享交互式代码示例,这是一个很棒的工具。它也很容易扩展,因此社区经常发布Go Playground的变体,以尝试和分享以前的实验性的语言特性,如泛型( https://oreil.ly/f0qpm )(现在是主要语言的一部分,我们会在2.2.5节中进行讲解)。
最后但同样重要的是,Go项目定义了它的模板语言,称为Go模板( https://oreil.ly/FdEZ8 )。在某种程度上,它类似于Python的Jinja2语言( https://oreil.ly/U6Em1 )。虽然这听起来像是Go的一个附带功能,但它对任何动态文本或HTML的生成都是有利的。它也经常用于像Helm( https://helm.sh )或Hugo( https://gohugo.io )这类流行的工具中。
如果你在Go中定义了一个变量,但从未读取任何值或不将其传递给另一个函数,编译将会失败。同样,如果你在import语句中添加了一个包,但在文件中没有使用该包,它也会失败。
笔者看到Go开发人员已经习惯并且喜欢上了这个特性,但对于新手来说,这很令人诧异。如果你想快速地使用这门语言,那么在未被使用的构造上受阻可能会带来些许沮丧,例如,创建一些变量,却从不以调试目的使用他们。
但是,有一些方法可以显式处理这些情况!你可以在代码示例2-7中看到一些处理这些用法检查的示例。
代码示例2-7:未使用和已使用变量的示例
❶ 变量a、b、d没有被使用,所以会导致编译错误。
❷ 变量e被使用。
❸ 变量f在技术上用于明确无标识符(_)。如果你明确想要告诉读者(和编译器)你想要忽略该值,那么这种方法很有用。
同样,未被使用的导入将使编译过程失败,因此像goimports这样的工具(在2.1.7节中提到)会自动删除未使用的导入。对未使用的变量和导入进行编译失败提醒,可以有效地确保代码保持清晰度和相关性。请注意,它只检查内部函数变量。未使用的结构字段、方法或类型等元素不会被检查。
测试是每个应用程序强制实施的部分,无论大小。在Go中,测试是开发过程中自然而然的一部分,它易于编写,注重简洁性和可读性。如果我们想要高效的代码,则需要进行可靠的测试,以便让我们不必在迭代程序时担心回归。添加一个带有 _test.go 后缀的文件,从而在包中为你的代码引入单元测试。你可以在该文件中编写任何代码,生产代码无法访问到它们。你可以添加四种类型的函数,这些函数将针对不同的测试部分进行调用。某些签名可以区分这些类型,尤其是以前缀为Test、fuzz、Example或Benchmark命名的函数,以及特定的参数。
让我们来看看代码示例2-8中的单元测试类型。这是一个有趣的表测试。Example和Benchmark在2.2.1节和8.1.1节中进行了解释。
代码示例2-8:示例单元表测试
❶ 如果 _test.go 文件中的函数以关键字Test命名,并且只接受t*testing.T,则认为它是“单元测试”。你可以通过go test命令运行它们。
❷ 通常,我们希望使用多个测试用例(通常是边缘情况)来测试一个特定的函数,这些测试用例定义了不同的输入和预期输出。在这里,我建议使用表测试。定义你的输入和输出,然后在易于阅读的循环中运行相同的函数。
❸ 你可以选择调用t.Run,它允许你指定子测试。在动态测试用例(如表测试)上定义这些子测试是一种很好的做法,它使你能够快速导航到失败的用例。
❹ Go testing.T类型提供了有用的方法,如Fail或Fatal,来中止单元测试,或使用Error接口来继续运行并检查其他潜在错误。在我们的例子中,笔者建议使用一个简单的辅助工具,即来自我们的开源核心库( https://oreil.ly/yAit9 )的testutil.Equals [12] 。
笔者建议经常编写测试。提前为关键部分编写单元测试将帮助你更快地实现所需的功能。这也是笔者建议遵循合理的测试驱动开发形式的原因,详见3.6节。
在讨论更高级的功能之前,上述信息应该能使你形成对语言目标、优势和功能的大致认识。