标签 RobPike 下的文章

Go未来演进:基于共同目标和数据驱动的决策

本文永久链接 – https://tonybai.com/2023/12/10/go-changes

自从Go语言之父Rob Pike从Google退休并隐居澳洲后,Russ Cox便成为了Go语言团队的“带头大哥”,虽然其资历还无法与依旧奋战在一线的另外一位Go语言之父Robert Griesemer相比。如今,Russ Cox对Go语言未来的演化发展是很有“发言权”的,Go module的引入便是Russ Cox的重要决策之一。从Go社区来看,这些年来,以Russ Cox为首的Go团队对Go演进决策总体上是良性的、受欢迎的,比如Go module、Go泛型、Go对wasm的支持等,当然也有一些变化是受到质疑的,比如:Go 1.22版本很可能从试验特性到正式特性的loopvar等

注:我的极客时间《Go语言第一课》专栏中有对Go module和Go泛型的详细讲解,欢迎感兴趣的童鞋订阅阅读。

想必很多Gopher也和我一样,对Go团队就某一proposal的决策方式和依据很好奇 –到底他们是如何决定是否accept这个proposal的?Go语言后续该如何演化?向哪个方向发展演化?

今年9月份举办的GopherCon 2023上,Russ Cox代表Go团队做了名为“Go Changes”的主题演讲

在这个talk中,我们能找到一些答案。近期他重新录制了该演讲视频,并在其个人博客中放出。

本文就是基于这个视频内容进行整理加工后的文字稿,供国内广大gopher参考。


这是我在2023年GopherCon上做的一次演讲的重新录制视频。在这次演讲中,我和大家分享了三部分内容:为什么Go需要随着时间的推移而改变,我们如何应对Go的变化过程,以及为什么选择性遥测(opt-in telemetry)是这个过程中的一个重要且适当的部分。不过,这个演讲不是关于某个特定的Go特性变化,而是关于Go整体的变化过程,特别是我们是如何决定做出哪些改变的。

首先一个明显的问题是,为什么Go需要改变? 为什么我们不能对Go感到满意,然后将其束之高阁呢? 一个显而易见的答案是我们不可能一次就把事情做对,你对比一下上面图片中展示的第一版毛绒Go吉祥物和我们在GopherCon上发放的最终版本,你就能明白我的意思了。

但这里还有一个更深层次的答案:

我的一位前同事在他使用了多年的邮件签名中引用了生物学家兼科幻小说作家杰克·科恩(Jack Cohen)的一句名言。在这句名言中,科恩说:“我们生物学家使用的一个描述‘稳定(stable)’的专业词汇就是“死(dead)”。

所有的生命都在变化,适应新的环境,修复损伤等。编程环境也需要改变。除非我们想要Go死掉,否则它需要适应新的环境,比如新的协议、操作系统和重要用例。我们也需要发现并修复bug — 语言、库和生态系统的问题,这些问题只有随着时间的推移或Go发展到一定阶段和规模才会暴露出来。

Go必须改变,并与时俱进。这次演讲就是关于我们如何决定做出哪些改变

这次演讲分三个部分:

  • 第一部分是关于我们对Go的愿景和期望。
  • 第二部分是关于我们如何利用数据来决定做出哪些改变。
  • 第三部分是关于我们在Go工具链中增加选择性遥测的计划,以便更好地理解Go的使用情况和出现问题的地方。

到演讲结束时,你将了解我们考量和决定Go变化的过程,并了解数据在做出这些决定中的重要性,我希望你能理解为什么选择性遥测是一个很好的额外数据来源,甚至可能愿意在系统推出时就选择加入。

让我们从这个开始:我们希望Go发生什么样的变化?如果我们在这个基本问题上意见不一致,我们也就无法就具体的变化达成共识。

例如,我们是否应该在Go中添加一个Perl语句,让我们可以用Perl编写函数?

我认为我们不应该,但假设你有不同意见。为了解决这个问题,我们需要理解为什么我们持不同意见。

约翰·奥斯特豪特(John Ousterhout)写了一份名为“开放决策制定(Open Decision Making)”的好文档,内容虽然来自他在创业公司的经验,但它几乎完全适用于开源项目。

在这份文档中,他提出的最重要的观点之一是:如果一群聪明人面对同一个问题,并具有相同的信息,如果他们有相同的目标,那么他们很可能得出相同的结论

如果你和我在Go中是否要嵌入Perl这个问题上存在分歧,根本原因肯定是我们对Go目标有不同的理解,所以我们必须建立明确Go的目标。

Go的目标是更好的软件工程,特别是大规模软件工程。Go的独特设计决策几乎全部针对这个目标。我们已经多次阐述过这一点,包括在上述截图中的这两篇文章中。再说一次,Go的目标是更好的软件工程

现在我们来说说Perl。20年前,当我很年轻、甚至有些天真、Go还不存在的时候,我编写并部署了一个完全用Perl编写的大型分布式系统。我热爱Perl所擅长的东西,但它并不是以更好的软件工程为目标。如果我们在这一点上有分歧,那么我可能应该定义一下我所说的软件工程是什么意思。

注:如果要理解Go以更好软件工程为目标,或是Google的软件工程理念,可以阅读一下《Software Engineering at Google》这本佳作。

我喜欢说,当你给编程加入时间和其他程序员时,软件工程就出现了。编程意味着让一个程序工作。你有一个要解决的问题,你编写一些代码,运行它,调试它,得到答案,完成。这就是编程,这已经够难的了。

但是当那段代码不得不日复一日地继续工作时会发生什么,甚至和其他人一起对它进行维护?那么你需要添加测试,以确保你修正后的bug不会在6个月后由你自己或是一个不熟悉这段代码的新团队成员重新引入。这就是为什么Go从第一天开始就内置了对测试的支持,并建立了一种文化,那就是对任何bug的修复或新增代码都要添加测试。

那么随着时间的流逝,当代码必须在Go本身发生改变的情况下继续工作时会发生什么?那么我们需要强调兼容性,这是Go1版本以来一直在做的。事实上,Go 1.21版本发布了许多兼容性改进,我在2022年的GopherCon上对此有过介绍。

随着代码量的增长,如果需要某种全局清理时该怎么办?你需要工具,而不可避免的第一个绊脚石是那些工具需要模仿代码的格式化风格来编辑,以避免出现无关的差异。gofmt的存在是为了支持goimports、gorename、go fix和gopls等工具,以及你自己可能使用我们提供的包编写的自定义工具。

既然提到了软件包,当你使用其他人提供的软件包时,不可避免的第一个绊脚石是多个人会用相同的名字(比如sqlite或yaml)编写软件包。那么我们如何在一个给定的程序中识别究竟使用哪个了呢?为了在一个去中心化的方式无歧义地回答这个问题,Go使用URL作为包导入路径。

随着时间的推移,下一个问题是挑选使用特定软件包的哪个版本,并决定该版本是否与所有其他依赖项兼容。这就是为什么Go提供了modules、workspaces、Go modules mirror镜像和Go module校验和数据库。

接下来的问题是每个人的代码都有bug,包括安全bug。你需要了解关于最重要bug的信息,这样你就知道需要更新到已修复的版本。这就是为什么我们添加了Go漏洞数据库和govulncheck,Julie也在GopherCon上谈到了这一点,当有视频链接时我会在下面添加。

以上是较大的例子,但也有小的例子,比如添加新的协议如HTTP/3,移除对过时平台的支持,以及修复或废弃容易出错的API,以避免大型代码库中的常见错误。

这把我们带到了Go提案过程(Proposal Process),这是我们对是否接受(accept)和拒绝(decline)哪些变更做出决定的方式:

当我们考虑这些决定时,使用数据非常重要,这可以帮助我们达成共识。

简单地说,任何人都可以在Go的GitHub问题跟踪器上提出Go更改提案(Change Proposal)。然后,在该问题上进行讨论,我们试图在参与者之间就是否接受或拒绝该建议达成共识,或者该建议需要做出什么修改才能被接受。

随着时间的推移,我们越来越欣赏约翰·奥斯特豪特在他的观察中提出的第二句话的重要性:如果面对问题的人不仅共同的目标,还有共同的信息,他们很可能会达成共识。

在Go的早期,只有我们几个人做决定。我们根据技术判断和直觉做出决定,这些判断和直觉是基于我们过去的经验。那些经验就是我们使用的信息。由于我们的过去经验有足够的重叠,我们大多数时候能达成共识。大多数小项目都是这种工作方式。

随着决策涉及的人数大大增加,共享经验就会减少。我们需要一个新的共享信息来源。我们发现的最好信息来源是收集实际数据,然后将这些数据作为共享信息来做决策。但是我们从哪里获得这些数据呢?对Go来说,我们有许多潜在的来源,每一个都适合具体的决策类型。在这里,我将向你展示其中的一些。

一个数据来源是与Go用户交谈。我们以各种方式做到这一点:

首先是Go用户调查,我们从2016年开始每年做一次,最近开始一年做两次。调查非常适合了解Go最流行的用途以及人们面临的最常见问题。多年来,最常见的问题是缺乏依赖管理和泛型。我们使用这些信息将开发Go模块和泛型作为优先事项。

另一个数据来源是我们可以在VSCode中使用VSCode Go插件运行的调查。这些调查可以帮助我们了解VSCode Go体验的实效性。

来自用户的最后一个直接数据来源是我们全年进行的研究访谈和用户体验研究。这些研究允许我们从小规模的用户群体中识别模式或获取更多关于特定主题的信息。

调查和访谈通过与用户交谈来收集数据。另一个数据来源是阅读代码:我们可以分析已发布的开源Go module代码。

例如,在添加新的“go vet”检查之前,我们会在开源代码库的一个子集上运行它,然后读取一些随机样本的结果,看检查是否指出了真实的问题,以及它是否有太多的假阳性。

在Go 1.22版本,我们计划添加一个go vet检查,检查对append的调用是否没有append任何内容。这里有检查器标记的两段代码:

顶部的一段代码表明开发人员可能认为append总是复制其输入slice。底部的一段代码可能是正确的,但难于措辞来描述。

这里还有另外两段代码:

在顶部的一段中,或者for循环从未运行,或者它永远不会完成,因为e.Sigs的长度永远不会改变。底部的代码也似乎是一个清晰的bug:代码正在仔细决定将消息追加到哪个列表中,然后它没有将其追加到任何一个列表中。

由于我们对样本代码段进行的所有采样都是可疑的或完全错误的,我们决定添加该检查。在这里,数据比直觉更好。

所有这些方法都是在少量样本上工作。对于典型的代码分析,我喜欢手动检查100个样本,与世界上所有Go代码的量相比,这只是一个微小的比例。最后一份Go开发者调查有不到6000名受访者,而全世界可能有300万Go开发者,样本比例不到1%。

一个很好的问题是为什么这些极小的样本能告诉我们有关更大人群的信息?答案是抽样精度只依赖于样本数量,而不依赖于总体规模。

这乍一看似乎反直觉,但假设我有一个装有100万只Go吉祥物的大箱子,我随机拿出两个。首先我拿到一个蓝色的,然后我拿到一个粉红色的。根据这两个样本,我估计箱子中的吉祥物大约一半是蓝色的,一半是粉红色的。但如果我告诉你箱子里有粉红色、蓝色和灰色的吉祥物,你是否会感到十分惊讶? 不会非常惊讶!如果箱子正好分三分之一粉红色、蓝色和灰色,那么这9对颜色组合中的每一对都同样可能:

得到一个非灰色吉祥物的机会是2/3,得到两个的机会就是2/3的平方,即4/9。没看到灰色的情况出现概率将近一半。这就是为什么我们不会非常惊讶的原因。

现在假设我取出100只,有48只蓝色和52只粉红色。我再次估计箱子大约一半是蓝色,一半是粉红色。现在如果我告诉你箱子里有粉红色、蓝色和灰色的吉祥物,你会有多惊讶?你应该会非常惊讶。

事实上,你完全不应该相信我。如果那是真的,得到100只连续的非灰色吉祥物的机会是2/3的100次方,约等于10的负48次方:

随机出现这种情况的可能性为零。要么我在说谎,要么我没有随机抽取。可能所有的灰色吉祥物都在箱子底部,我没有抽取到足够深的地方。

请注意:这都不依赖于箱子中有多少只Go吉祥物,它只取决于我们取出了多少只。用于特定预测精度的数学更复杂,但具有相同的效果:只有样本数量重要,箱子中的吉祥物数目不重要

一般来说,手工计算这些数学太困难了,所以这里有一个表格,你可以在我的博客上找到:

它说明,如果你提取100个样本并根据这些样本估计百分比,那么90%的时间你的估计将在真实百分比的正负8%之内。99%的时间它们将在13%之内。如果像Go调查中那样有5000个样本,那么90%的时间估计误差在正负1%之内,99%的时间在正负2%之内。超过这个数量,我们实际上不需要更多样本。

有一个注意事项是样本需要是随机的, 或者至少与你正在估计的内容不相关。你不能只从箱子的顶部抽取吉祥物,然后对整个箱子做出断言。

如果你避免了这个错误, 那么当你试图估计一个新的API是否有用或者某个特定的vet check是否值得的时候, 花一个小时左右手动检查100个样本是合理的。如果是一个坏主意, 那将很快显现出来。而如果看起来是一个好主意, 再花几个小时检查更多的样本, 无论是手动检查还是用程序检查,都会大大提高你的估计准确性。与做出错误决策的代价相比,这是一个非常小的成本。

简而言之,采样的魔力在于将许多一次性估计转变为可以手动或用少量数据完成的工作。这就是为什么我们已经看到的所有数据来源都能够相当好地代表整个Go开发者群体的原因。

现在进入演讲的第三部分:Go工具链中的遥测(Telemetry):

遥测也将是Go开发者使用的一个小样本,但它应该是一个有代表性的样本,并且回答不同的问题,而不是调查和代码分析所做的问题。

遥测始终是一个有争议的话题,特别是对于开源项目来说,所以让我从最重要的细节开始说起:上传遥测报告是完全自愿和选择加入的:

除非你运行一个显式命令选择加入数据收集,否则不会上传任何数据。而且,这不是那种上传你的全部活动的详细跟踪的遥测系统。这种遥测也只适用于我们作为Go发行版的一部分分发的命令,比如gopls、go命令和编译器(compiler),它不会涉及你构建的任何程序

在我更详细地描述完这个系统之后,我希望你会发现你会愿意选择加入这个遥测系统。实际上,我们给自己设定的主要设计限制是,即使由其他人运行,我们也愿意选择加入该系统。

在我以2023年11月的录制这个内容时,该系统刚刚开始运行,只有少数人被要求在VSCode Go中选择加入gopls遥测。所以总体来说,你现在还不能选择加入。但希望很快你就可以了。

在我们深入了解细节之前,遥测的动机是它提供了与调查和代码分析不同的信息。它主要提供的两个类别是使用信息(Usage Information)和故障信息(Breakage Information)。调查让我们能够询问关于Go使用的广泛问题,但对于详细的使用信息来说并不好。那将是太多问题,对于调查对象来说,90%的问题要回答”no”是一种浪费时间。

这个幻灯片显示了我们在之前的版本中警告过即将删除的Go功能列表。列表中的最后一项,buildmode=shared,是我们试图移除的功能,但在事先警告后,至少有一个用户提出了异议,我们将其保留了下来。即便如此,buildmode=shared与Go module基本不兼容,所以它的使用可能非常有限。但我们没有数据,所以它仍然存在于代码库中。遥测可以为我们提供基本的使用信息,以便我们可以基于数据而不是猜测做出这些决策。

另一个重要的类别是故障信息:

如果Go工具链明显有问题,我们希望在GitHub上收到错误报告。但是Go工具链也可能以用户注意不到的微妙方式出现问题。一个例子是,在macOS上的Go 1.14到Go 1.19的版本中,标准库包的二进制文件在预先构建时使用了非默认的编译标志,这是一个意外,这使得它们看起来像是过时了,Go命令在运行时会重新编译它们,这意味着如果你的程序导入了net包,你需要安装Xcode中的C编译器来构建程序。我们希望Go能够自行构建纯Go程序,而无需其他工具链。因此,要求安装Xcode是一个bug。但是我们没有注意到这个问题,也没有用户在GitHub上报告它。遇到这个问题的人似乎只是安装了Xcode并继续进行了工作。遥测可以提供基本的性能指标,比如标准库缓存命中率,这样Go工具链的开发人员即使用户没有意识到这个问题,也能注意到这个问题。

另一个例子是编译器的内部崩溃:

Go编译器在程序的第一个错误处不会停止。它会继续进行,尽可能多地查找和报告不同的错误。但是有时,继续分析已知错误的程序会导致意外的panic。我们不希望向用户显示这样的崩溃。相反,编译器会从panic中恢复,并且仅报告已经发现的错误。这样,Go用户可以纠正这些错误,这也可能纠正隐藏的panic。用户的工作不会因为看到编译器崩溃而中断。这对用户来说是好的,但是Go工具链的开发人员仍然希望了解这个崩溃并修复这个错误。遥测可以确保即使用户不知道这个错误,但我们还能了解到这个错误。

为了收集使用情况和故障信息,Go遥测设计记录“计数器和崩溃”:

像go命令、Go编译器或gopls这样的Go工具链程序可以定义命名事件计数器,并在事件发生时递增计数器。事件还可以按堆栈跟踪单独计数。这些计数器在本地的磁盘文件中维护,每次保留一周的时间。在幻灯片上,gopls和其他工具正在将计数器写入每周的文件中。

每周一次,Go工具链中的上传程序(uploader)将从遥测服务器获取一个“上传配置”,其中列出了该周收集的特定事件名称。只有在遥测特定的提案审查过程达成共识后,才会更改该配置。该配置作为一个模块(module)提供,以保护下载的完整性,并保留过去配置的公共记录。然后,上传程序仅上传上传配置中列出的计数器。在幻灯片上,上传程序仅为gopls发送一份报告,仅包含少量计数器,即使磁盘上可能还有更多计数器。报告中包含关于使用gopls的编辑器的统计信息,以及关于完成请求的延迟的信息,还有一个发生了一次的gopls/bug事件,其中包含一个栈跟踪。

请注意,上传的数据中没有事件跟踪或任何用户数据,只有计数器、已在公共上传配置中列出的事件名称,以及Go工具链程序中的函数名称。还要注意,栈跟踪不包括任何函数的参数,只有函数名称,因此没有用户数据。

开源中的遥测可能会在拥有数据访问权限和没有数据访问权限的人之间产生信息失衡。我们希望避免这种情况。请记住奥斯特豪特规则:为了达成共识,我们需要每个人拥有相同的信息。由于Go的遥测上传不包含任何敏感数据,并且是在明确的选择同意的情况下收集的,我们可以完整地重新发布这些报告,以便任何人都可以进行任何数据分析。我们还将发布一些基本的图表,用于做出决策。我们唯一可能看到但没有重新发布的是报告来自哪些IP地址,我们的服务器会将这些信息与报告一起记录。

一个明显的问题是,是否有足够多的人选择启用遥测,以使数据足够准确以做出决策。幸运的是,采样的神奇之处在于可以帮助解决这个问题。

全球大约有300w Go开发者。当系统准备就绪并要求人们启用遥测时,即使只有千分之一的开发者选择参与,也会有3000名开发者,根据我们的图表显示,误差不到3%,置信度为99%。如果全球三分之二的Go开发者启用了遥测,那将是20000个样本,误差不到1%,置信度为99%。除此之外,我们实际上不需要更多的样本。如果我们持续获得更多的报告,我们可以调整上传配置,告诉系统在某个特定的周选择随机不上传任何东西。例如,如果有20万个系统选择了参与,我们可以告诉每个系统在任何给定的周上传的概率为10%。因此,即使我们预计选择参与率会很低,系统应该能够运行得很好,随着选择参与率的提高,Go遥测将从任何给定系统收集更少的数据。当然,这使得每个选择参与的人对我们来说更加重要。目前来说,Go遥测对于你们中的任何人来说都还没有准备好,但当准备好时,我希望你们会选择参与。

在结束之前,我希望你们从演讲中获得以下几点:

首先,Go需要不断变化,特别是随着计算世界的变化。

其次,任何改变的目标都是为了使Go在软件工程中变得更好,尤其是在规模化(scaling)方面。

第三,一旦我们确定了目标,达成共识的下一个最重要的部分是拥有共享数据来做出决策。

第四,Go工具链遥测是增补我们现有调查和代码分析数据的重要数据来源。

最后,在整个演讲中,虽然涉及到了数据和适当的统计,但我们评估的想法、假设和潜在的变化始终始于个人故事和对话。我们喜欢听到这些故事,并与你们所有人讨论如何使用Go,关于什么有效和什么无效。所以,请无论在什么情况下,无论是在会议上、邮件列表上还是在问题跟踪器上,请确保让我们知道Go对你们的工作情况以及存在的问题。我们总是很乐意听到这些。非常感谢。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

十分钟入门Go语言

本文永久链接 – https://tonybai.com/2023/02/23/learn-go-in-10-min

本文旨在带大家快速入门Go语言,期望小伙伴们在花费十分钟左右通读全文后能对Go语言有一个初步的认知,为后续进一步深入学习Go奠定基础。

本文假设你完全没有接触过Go,你可能是一名精通其他编程语言的程序员,也可能是毫无编程经验、刚刚想转行为码农的热血青年。

编程简介

编程就是生产可在计算机上执行的程序的过程(如下图)。在这个过程中,程序员是“劳动力”,编程语言是工具,可执行的程序是生产结果。而Go语言就是程序员在编程生产过程中使用的一种优秀生产工具。

作为“劳动力”的程序员在这个过程中要做的就是使用某种编程语言作为生产工具,将事先设计好的执行逻辑组织和表达出来,这与一个作家将其大脑中设计好的故事情节用人类语言组织和书写在纸上的过程颇为类似(如下图)。

通过这个类比来看,学习一门编程语言,就好比学习一门人类语言,其词汇和语法将是我们的主要学习内容,本文就将围绕Go语言的主要“词汇”和语法形式进行快速说明。

Go简介

Go语言是由Google公司的三位大神级程序员Robert Griesemer、Rob Pike和Ken Thompson在2007年共同开发的一种新的后端编程语言,2009年,Go语言宣布开源。

Go语言的特点是简单易学、静态类型、编译速度快,运行效率高,代码简洁,并且原生支持并发编程。它还支持自动内存管理,可以让开发者更加专注于编程本身,而不用担心内存泄漏的问题。此外,Go语言还支持多核处理器,可以更好地利用多核处理器的优势,提高程序的运行效率。

经过十多年的发展,Go语言现在已经成为一种流行的编程语言,它可以用于开发各种应用程序,包括Web应用、网络服务、系统管理工具、移动应用、游戏开发、数据库管理等。Go语言常用于构建大型分布式系统,以及构建高性能的服务器端应用程序。Go为当前的云原生计算时代开发了一批“杀手级”应用,包括Docker、Kubernetes、Prometheus、InfluxDB、Cilium等。

安装Go

Go是静态语言,需要先编译,再执行,因此在开发Go程序之前,我们首先需要安装Go编译器以及相关工具链。安装的步骤很简单:

  • Go官网下载最新版本的Go语言安装包 – https://go.dev/dl/
  • 解压安装包,并将其复制到您想要安装的位置,例如:/usr/local/go;如果是Windows、MacOS平台,也可以下载图形化安装的安装包;
  • 设置环境变量,将Go语言的安装路径添加到PATH变量中;
  • 打开终端,输入go version,检查Go语言是否安装成功。如输出类似下面的内容,则表明安装成功!
$go version
go version go1.20 darwin/amd64

注:位于中国大陆的开发者们还需要一个额外的设置:export GOPROXY=’https://goproxy.cn’或将这个设置置于shell配置文件(比如.bashrc)中并使之生效。

第一个Go程序:Hello World

建立一个新目录,并在其中创建新文件helloworld.go,用任意编辑器打开helloworld.go,输入下面Go源码:

//helloworld.go

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

Go支持直接运行某个源文件:

$go run helloworld.go
Hello, World!

但通常我们会先编译这个源文件(helloworld.go),生成可执行的二进制程序(./helloworld),然后再运行它:

$go build -o helloworld helloworld.go
$./helloworld
Hello, World!

Go包(package)

Go包是Go语言中的一种封装技术,它可以将一组Go语言源文件组织成一个可重用的单元,以便在其他Go程序中使用。同属于一个Go包的所有源文件放在一个目录下,并且按惯例该目录的名字与包名相同。以Go标准库的io包为例,其包内的源文件列表如下:

// $GOROOT/src/io目录下的文件列表:
io.go
multi.go
pipe.go

Go包也是Go编译的基本单元,Go编译器可以将包编译为可执行文件(如何该包为main包,且包含main函数实现),也可以编译为可重用的库文件(.a)。

包声明

Go包的声明通常是在每个Go源文件的开头,使用关键字package进行声明,例如:

// mypackage.go
package mypackage

... ...

package的名字按惯例通常为全小写的单个单词或缩略词,比如io、net、os、fmt、strconv、bytes等。

导入Go包

如果要复用已有的Go包,我们需要在源码中导入该包。要导入Go包,可以使用import关键字,例如:

import "fmt"                    // 导入标准库的fmt包

import "github.com/spf13/pflag" // 导入spf13开源的pflag包

import _ "net/http/pprof"       // 导入标准库net/http/pprof包,
                                // 但不显式使用该包中的类型、变量、函数等标识符

import myfmt "fmt"              // 将导入的包重命名为myfmt

Go模块

Go模块(module)是Go语言在1.11版本中引入的新特性,Go module是一组相关的Go package的集合,这个包集合被当做一个独立的单元进行统一版本管理。Go module这种新的依赖管理机制可以让开发者更轻松地管理Go语言项目的依赖关系,并且可以更好地支持多版本的依赖管理。在具有实用价值的Go项目中,我们都会使用Go module进行依赖管理。Go module有版本之分,Go module的版本依赖关系是建立在对语义版本(semver)严格遵守的前提下的。

Go使用go.mod文件来精确记录依赖关系要求,下面是go.mod中依赖关系的操作方法:

$go mod init demo // 创建一个module root为demo的go.mod
$go mod init github.com/bigwhite/mymodule // 创建一个module root为github.com/bigwhite/mymodule的go.mod

$go get github.com/bigwhite/foo@latest  // 向go.mod中添加一个依赖包github.com/bigwhite/foo的最新版本
$go get github.com/bigwhite/foo         // 与上面命令等价
$go get github.com/bigwhite/foo@v1.2.3  // 显式指定要获取v1.2.3版本

$go mod tidy   // 自动添加缺失的依赖包和清理不用的依赖包
$go mod verify // 确认所有依赖都有效

Go最小项目结构

Go官方并没有规定Go项目的标准结构布局,下面是Go核心团队技术负责人Russ Cox推荐的Go最小项目结构:

// 在Go项目仓库根路径下

- go.mod
- LICENSE
- README
- xx.go
- yy.go
... ...

// 在Go项目仓库根路径下

- go.mod
- LICENSE
- README
- package1/
    - package1.go
- package2/
    - package2.go
... ...

变量

Go语言有两种变量声明方式:

  • 使用var关键字

使用var关键字进行声明的方式适合所有场合。

var a int     // 声明一个int型变量a,初值为0
var b int = 5 // 声明一个int型变量b,初值为5
var c = 6     // Go会根据右值自动为变量c的赋予默认类型,默认的整型为int

var (         // 我们可以将变量声明统一放置在一个var块中,这与上面的声明方式等价
    a int
    b int = 5
    c = 6
)

注:Go变量声明采用变量在前,类型在后的方式,这与C、C++、Java等静态编程语言有较大不同。

  • 使用短声明方式声明变量
a := 5       // 声明一个变量a,Go会根据右值自动为变量a的赋予默认类型,默认的整型为int
s := "hello" // 声明一个变量s,Go会根据右值自动为变量s的赋予默认类型,默认的字符串类型为string

注:这种声明方式仅限于在函数或方法内使用,不能用于声明包级变量或全局变量。

常量

Go语言的常量使用const关键字进行声明:

const a int       // 声明一个int型常量a,其值为0
const b int = 5   // 声明一个int型常量b,其值为5
const c = 6       // 声明一个常量c,Go会根据右值自动为常量c的赋予默认类型,默认的整型为int
const s = "hello" // 声明一个常量s,Go会根据右值自动为常量s的赋予默认类型,默认的字符串类型为string

const (           // 我们可以将常量声明统一放置在一个const块中,这与上面的声明方式等价
    a int
    b int = 5
    c = 6
    s = "hello"
)

类型

Go原生内置了多种基本类型与复合类型。

基本类型

Go原生支持的基本类型包括布尔型、数值类型(整型、浮点型、复数类型)、字符串类型,下面是一些示例:

bool  // 布尔类型,默认值false

uint     // 架构相关的无符号整型,64位平台上其长度为8字节
int      // 架构相关的有符号整型,64位平台上其长度为8字节
uintptr  // 架构相关的用于表示指针值的类型,它是一个无符号的整数,大到足以存储一个任意类型的指针的值

uint8    // 架构无关的8位无符号整型
uint16   // 架构无关的16位无符号整型
uint32   // 架构无关的32位无符号整型
uint64   // 架构无关的64位无符号整型

int8     // 架构无关的8位有符号整型
int16    // 架构无关的16位有符号整型
int32    // 架构无关的32位有符号整型
int64    // 架构无关的64位有符号整型

byte     // uint8类型的别名
rune     // int32类型的别名,用于表示一个unicode字符(码点)

float32     // 单精度浮点类型,满足IEEE-754规范
float64     // 双精度浮点类型,满足IEEE-754规范

complex64   // 复数类型,其实部和虚部均为float32浮点类型
complex128  // 复数类型,其实部和虚部均为float64浮点类型

string      // 字符串类型,默认值为""

我们可以使用预定义函数complex来构造复数类型,比如:complex(1.0, -1.4)构造的复数为1 – 1.4i。

复合类型

Go原生支持的复合类型包括数组(array)、切片(slice)、结构体(struct)、指针(pointer)、函数(function)、接口(interface)、map、channel。

数组类型

数组类型是一组同构类型元素组成的连续体,它具有固定的长度(length),不能动态伸缩:

[8]int      // 一个元素类型为int、长度为16的数组类型
[32]byte    // 一个元素类型为byte、长度为32的数组类型
[2]string   // 一个元素类型为string、长度为2的数组类型
[N]T        // 一个元素类型为T、长度为N的数组类型

通过预定义函数len可以得到数组的长度:

var a = [8]int{11, 12, 13, 14, 15, 16, 17, 18}
println(len(a)) // 8

通过数组下标(从0开始)可以直接访问到数组中的任意元素:

println(a[0]) // 11
println(a[2]) // 13
println(a[7]) // 18

Go支持声明多维数组,即数组的元素类型依然为数组类型:

[2][3][5]float64  // 一个多维数组类型,等价于[2]([3]([5]float64))

切片类型

切片类型与数组类型类似,也是同构类型元素的连续体。不同的是切片类型的长度可变,我们在声明切片类型时无需传入长度属性:

[]int       // 一个元素类型为int的切片类型
[]string    // 一个元素类型为string的切片类型
[]T         // 一个元素类型为T的切片类型
[][][]float64 // 多维切片类型,等价于[]([]([]float64))

通过预定义函数len可以得到切片的当前长度:

var sl = []int{11, 12} // 一个元素类型为int的切片,其长度(len)为2, 其值为[11 12]
println(len(sl)) // 2

切片还有一个属性,那就是容量,通过预定义函数cap可以获得其容量值:

println(cap(sl)) // 2

和数组不同,切片可以动态伸缩,Go会根据元素的数量动态对切片容量进行扩展。我们可以通过append函数向切片追加元素:

sl = append(sl, 13)     // 向sl中追加新元素,操作后sl为[11 12 13]
sl = append(sl, 14)     // 向sl中追加新元素,操作后sl为[11 12 13 14]
sl = append(sl, 15)     // 向sl中追加新元素,操作后sl为[11 12 13 14 15]
println(len(sl), cap(sl)) // 5 8 追加后切片容量自动扩展为8

和数组一样,切片也是使用下标直接访问其中的元素:

println(sl[0]) // 11
println(sl[2]) // 13
println(sl[4]) // 15

结构体类型

Go的结构体类型是一种异构类型字段的聚合体,它提供了一种通用的、对实体对象进行聚合抽象的能力。下面是一个包含三个字段的结构体类型:

struct {
    name string
    age  int
    gender string
}

我们通常会给这样的一个结构体类型起一个名字,比如下面的Person:

type Person struct {
    name string
    age  int
    gender string
}

下面声明了一个Person类型的变量:

var p = Person {
    name: "tony bai",
    age: 20,
    gender: "male",
}

我们可以通过p.FieldName来访问结构体中的字段:

println(p.name) // tony bai
p.age = 21

结构体类型T的定义中可以包含类型为*T的字段成员,但不能递归包含T类型的字段成员:

type T struct {
    ... ...
    p *T    // ok
    t T     // 错误:递归定义
}

Go结构体亦可以在定义中嵌入其他类型:

type F struct {
    ... ...
}

type MyInt int

type T struct {
    MyInt
    F
    ... ...
}

嵌入类型的名字将作为字段名:

var t = T {
    MyInt: 5,
    F: F {
        ... ...
    },
}

println(t.MyInt) // 5

Go支持不包含任何字段的空结构体:

struct{}
type Empty struct{}        // 一个空结构体类型

空结构体类型的大小为0,这在很多场景下很有用(省去了内存分配的开销):

var t = Empty{}
println(unsafe.Sizeof(t)) // 0

指针类型

int类型对应的指针类型为*int,推而广之T类型对应的指针类型为*T。和非指针类型不同,指针类型变量存储的是内存单元的地址,*T指针类型变量的大小与T类型大小无关,而是和系统地址的表示长度有关。

*int     // 一个int指针类型
*[4]byte // 一个[4]byte数组指针类型

var a = 6
var p *T // 声明一个T类型指针变量p,默认值为nil
p = &a   // 用变量a的内存地址给指针变量p赋值
*p = 7   // 指针解引用,通过指针p将变量a的值由6改为7

n := new(int)  // 预定义函数返回一个*int类型指针
arr := new([4]int)  // 使用预定义函数new分配一个[4]int数组并返回一个*[4]int类型指针

map类型

map是Go语言提供的一种抽象数据类型,它表示一组无序的键值对,下面定义了一组map类型:

map[string]int                // 一个key类型为string,value类型为int的map类型
map[*T]struct{ x, y float64 } // 一个key类型为*T,value类型为struct{ x, y float64 }的map类型
map[string]interface{}        // 一个key类型为string,value类型为interface{}的map类型

我们可以用map字面量或make来创建一个map类型实例:

var m = map[string]int{}      // 声明一个map[string]int类型变量并初始化
var m1 = make(map[string]int) // 与上面的声明等价
var m2 = make(map[string]int, 100) // 声明一个map[string]int类型变量并初始化,其初始容量建议为100

操作map变量的方法也很简单:

m["key1"] = 5  // 添加/设置一个键值对
v, ok := m["key1"]  // 获取“key1”这个键的值,如果存在,则其值存储在v中,ok为true
delete(m, "key1") // 从m这个map中删除“key1”这个键以及其对应的值

其他类型

函数、接口、channel类型在后面有详细说明。

自定义类型

使用type关键字可以实现自定义类型:

type T1 int         // 定义一个新类型T1,其底层类型(underlying type)为int
type T2 string      // 定义一个新类型T2,其底层类型为string
type T3 struct{     // 定义一个新类型T3,其底层类型为一个结构体类型
    x, y int
    z string
}
type T4 []float64   // 定义一个新类型T4,其底层类型为[]float64切片类型
type T5 T4          // 定义一个新类型T5,其底层类型为[]float64切片类型

Go也支持为类型定义别名(alias),其形式如下;

type T1 = int       // 定义int的类型别名为T1,T1与int等价
type T2 = string    // 定义string的类型别名为T2,T2与string等价
type T3 = T2        // 定义T的类型别名为T3,T3与T2等价,也与string等价

类型转换

Go不支持隐式自动转型,如果要进行类型转换操作,我们必须显式进行,即便两个类型的底层类型相同也需如此:

type T1 int
type T2 int
var t1 T1
var n int = 5
t1 = T1(n)      // 显式将int类型变量转换为T1类型
var t2 T2
t2 = T2(t1)     // 显式将T1类型变量转换为T2类型

Go很多原生类型支持相互转换:

// 数值类型的相互转换

var a int16 = 16
b := int32(a)
c := uint16(a)
f := float64(a)

// 切片与数组的转换(Go 1.17版本及后续版本支持)

var a [3]int = [3]int([]int{1,2,3}) // 切片转换为数组
var pa *[3]int = (*[3]int)([]int{1,2,3}) // 切片转换为数组指针
sl := a[:] // 数组转换为切片

// 字符串与切片的相互转换

var sl = []byte{'h', 'e','l', 'l', 'o'}
var s = string(sl) // s为hello
var sl1 = []byte(s) // sl1为['h' 'e' 'l' 'l' 'o']
string([]rune{0x767d, 0x9d6c, 0x7fd4})  // []rune切片到string的转换

控制语句

Go提供了常见的控制语句,包括条件分支(if)、循环语句(for)和选择分支语句(switch)。

条件分支语句

// if ...

if a == 1 {
    ... ...
}

// if - else if - else

if a == 1 {

} else if b == 2 {

} else {

}

// 带有条件语句自用变量
if a := 1; a != 0 {

}

// if语句嵌套

if a == 1 {
    if b == 2 {

    } else if c == 3 {

    } else {

    }
}

循环语句

// 经典循环

for i := 0; i < 10; i++ {
    ...
}

// 模拟while ... do

for i < 10 {

}

// 无限循环

for {

}

// for range

var s = "hello"
for i, c := range s {

}

var sl = []int{... ...}
for i, v := range sl {

}

var m = map[string]int{}
for k, v := range m {

}

var c = make(chan int, 100)
for v := range c {

}

选择分支语句

var n = 5
switch n {
    case 0, 1, 2, 3:
        s1()
    case 4, 5, 6, 7:
        s2()
    default: // 默认分支
        s3()
}

switch n {
    case 0, 1:
        fallthrough  // 显式告知执行下面分支的动作
    case 2, 3:
        s1()
    case 4, 5, 6, 7:
        s2()
    default:
        s3()
}

switch x := f(); {
    case x < 0:
        return -x
    default:
        return x
}

switch {
    case x < y:
        f1()
    case x < z:
        f2()
    case x == 4:
        f3()
}

函数

Go使用func关键字来声明一个函数:

func greet(name string) string {
    return fmt.Sprintf("Hello %s", name)
}

函数由函数名、可选的参数列表和返回值列表组成。Go函数支持返回多个返回值,并且我们通常将表示错误值的返回类型放在返回值列表的最后面:

func Atoi(s string) (int, error) {
    ... ...
    return n, nil
}

在Go中函数是一等公民,因此函数自身也可以作为参数或返回值:

func MultiplyN(n int) func(x int) int {
  return func(x int) int {
    return x * n
  }
}

像上面MultiplyN函数中定义的匿名函数func(x int) int,它的实现中引用了它的外围函数MultiplyN的参数n,这样的匿名函数也被称为闭包(closure)

说到函数,我们就不能不提defer。在某函数F调用的前面加上defer,该函数F的执行将被“延后”至其调用者A结束之后:

func F() {
    fmt.Println("call F")
}

func A() {
    fmt.Println("call A")
    defer F()
    fmt.Println("exit A")
}

func main() {
    A()
}

上面示例输出:

call A
exit A
call F

在一个函数中可以多次使用defer:

func B() {
    defer F()
    defer G()
    defer H()
}

被defer修饰的函数将按照“先入后出”的顺序在B函数结束后被调用,上面B函数执行后将输出:

call H
call G
call F

方法

方法是带有receiver的函数。下面是Point类型的一个方法Length:

type Point struct {
    x, y float64
}

func (p Point) Length() float64 {
    return math.Sqrt(p.x * p.x + p.y * p.y)
}

而在func关键字与函数名之间的部分便是receiver。这个receiver也是Length方法与Point类型之间纽带。我们可以通过Point类型变量来调用Length方法:

var p = Point{3,4}
fmt.Println(p.Length())

亦可以将方法当作函数来用:

var p = Point{3,4}
fmt.Println(Point.Length(p)) // 这种用法也被称为方法表达式(method expression)

接口

接口是一组方法的集合,它代表一个“契约”,下面是一个由三个方法组成的方法集合的接口类型:

type MyInterface interface {
    M1(int) int
    M2(string) error
    M3()
}

Go推崇面向接口编程,因为通过接口我们可以很容易构建低耦合的应用。

Go还支持在接口类型(如I)中嵌套其他接口类型(如io.Writer、sync.Locker),其结果就是新接口类型I的方法集合为其方法集合与嵌入的接口类型Writer和Locker的方法集合的并集:

type I interface { // 一个嵌入了其他接口类型的接口类型
   io.Writer
   sync.Locker
}

接口实现

如果一个类型T实现了某个接口类型MyInterface方法集合中的所有方法,那么我们说该类型T实现了接口MyInterface,于是T类型的变量t可以赋值给接口类型MyInterface的变量i,此时变量i的动态类型为T:

var t T
var i MyInterface = t // ok

通过上述变量i可以调用T的方法:

i.M1(5)
i.M2("demo")
i.M3()

方法集合为空的接口类型interface{}被称为“空接口类型”,空白的“契约”意味着任何类型都实现了该空接口类型,即任何变量都可以赋值给interface{}类型的变量:

var i interface{} = 5 // ok
i = "demo"            // ok
i = T{}               // ok
i = &T{}              // ok
i = []T{}             // ok

注:Go 1.18中引入的新预定义标识符any与interface{}是等价类型。

接口的类型断言

Go支持通过类型断言从接口变量中提取其动态类型的值:

v, ok := i.(T) // 类型断言

如果接口变量i的动态类型确为T,那么v将被赋予该动态类型的值,ok为true;否则,v为T类型的零值,ok为false。

类型断言也支持下面这种语法形式:

v := i.(T)

但在这种形式下,一旦接口变量i之前被赋予的值不是T类型的值,那么这个语句将抛出panic。

接口类型的type switch

“type switch”这是一种特殊的switch语句用法,仅用于接口类型变量:

func main() {
    var x interface{} = 13
    switch x.(type) {
    case nil:
        println("x is nil")
    case int:
        println("the type of x is int") // 执行这一分支case
    case string:
        println("the type of x is string")
    case bool:
        println("the type of x is string")
    default:
        println("don't support the type")
    }
}

switch关键字后面跟着的表达式为x.(type),这种表达式形式是switch语句专有的,而且也只能在switch语句中使用。这个表达式中的x必须是一个接口类型变量,表达式的求值结果是这个接口类型变量对应的动态类型。

上述例子中switch后面的表达式也可由x.(type)换成了v := x.(type)。v中将存储变量x的动态类型对应的值信息:

var x interface{} = 13
switch x.(type) {
    case nil:
        println("v is nil")
    case int:
        println("the type of v is int, v =", v) // 执行这一分支case,v = 13
    ... ...
}

泛型

Go从1.18版本开始支持泛型。Go泛型的基本语法是类型参数(type parameter),Go泛型方案的实质是对类型参数的支持,包括:

  • 泛型函数(generic function):带有类型参数的函数;
  • 泛型类型(generic type):带有类型参数的自定义类型;
  • 泛型方法(generic method):泛型类型的方法。

泛型函数

下面是一个泛型函数max的定义:

type ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
        ~float32 | ~float64 |
        ~string
}

func max[T ordered](sl []T) T {
    ... ...
}

与普通Go函数相比,max函数在函数名称与函数参数列表之间多了一段由方括号括起的代码:[T ordered];max参数列表中的参数类型以及返回值列表中的返回值类型都是T,而不是某个具体的类型。

max函数中多出的[T ordered]就是Go泛型的类型参数列表(type parameters list),示例中这个列表中仅有一个类型参数T,ordered为类型参数的类型约束(type constraint)。

我们可以像普通函数一样调用泛型函数,我们可以显式指定类型实参:

var m int = max[int]([]int{1, 2, -4, -6, 7, 0})  // 显式指定类型实参为int
fmt.Println(m) // 输出:7

Go也支持自动推断出类型实参:

var m int = max([]int{1, 2, -4, -6, 7, 0}) // 自动推断T为int
fmt.Println(m) // 输出:7

泛型类型

所谓泛型类型,就是在类型声明中带有类型参数的Go类型:

type Set[T comparable] map[T]string

type element[T any] struct {
    next *element[T]
    val  T
}

type Map[K, V any] struct {
  root    *node[K, V]
  compare func(K, K) int
}

以泛型类型Set为例,其使用方法如下:

var s = Set[string]{}
s["key1"] = "value1"
println(s["key1"]) // value1

泛型方法

Go类型可以拥有自己的方法(method),泛型类型也不例外,为泛型类型定义的方法称为泛型方法(generic method)。

type Set[T comparable] map[T]string

func (s Set[T]) Insert(key T, val string) {
    s[key] = val
}

func (s Set[T]) Get(key T) (string, error) {
    val, ok := s[key]
    if !ok {
        return "", errors.New("not found")
    }
    return val, nil
}

func main() {
    var s = Set[string]{
        "key": "value1",
    }
    s.Insert("key2", "value2")
    v, err := s.Get("key2")
    fmt.Println(v, err) // value2 <nil>
}

类型约束

Go通过类型约束(constraint)对泛型函数的类型参数以及泛型函数中的实现代码设置限制。Go使用扩展语法后的interface类型来定义约束。

下面是使用常规接口类型作为约束的例子:

type Stringer interface {
    String() string
}

func Stringify[T fmt.Stringer](s []T) (ret []string) { // 通过Stringer约束了T的实参只能是实现了Stringer接口的类型
    for _, v := range s {
        ret = append(ret, v.String())
    }
    return ret
}

Go接口类型声明语法做了扩展,支持在接口类型中放入类型元素(type element)信息:

type ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
        ~float32 | ~float64 | ~string
}

func Less[T ordered](a, b T) bool {
    return a < b
}

type Person struct {
    name string
    age  int
}

func main() {
    println(Less(1, 2)) // true
    println(Less(Person{"tony", 11}, Person{"tom", 23})) // Person不满足ordered的约束,会导致编译错误
}

并发

Go语言原生支持并发,Go并没有使用操作系统线程作为并发的基本执行单元,而是实现了goroutine这一由Go运行时(runtime)负责调度的、轻量的用户级线程,为并发程序设计提供原生支持。

goroutine

通过go关键字+函数/方法的方式,我们便可以创建一个goroutine。创建后,新goroutine将拥有独立的代码执行流,并与创建它的goroutine一起被Go运行时调度。

go fmt.Println("I am a goroutine")

// $GOROOT/src/net/http/server.go
c := srv.newConn(rw)
go c.serve(connCtx)

goroutine的执行函数返回后,goroutine便退出。如果是主goroutine(执行main.main的goroutine)退出,那么整个Go应用进程将会退出,程序生命周期结束。

channel

Go提供了原生的用于goroutine之间通信的机制channel,channel的定义与操作方式如下:

// channel类型
chan T          // 一个元素类型为T的channel类型
chan<- float64  // 一个元素类型为float64的只发送channel类型
<-chan int      // 一个元素类型为int的只接收channel类型

var c chan int             // 声明一个元素类型为int的channel类型的变量,初值为nil
c1 := make(chan int)       // 声明一个元素类型为int的无缓冲的channel类型的变量
c2 := make(chan int, 100)  // 声明一个元素类型为int的带缓冲的channel类型的变量,缓冲大小为100
close(c)                   // 关闭一个channel

下面是两个goroutine基于channel通信的例子:

func main() {
    var c = make(chan int)
    go func(a, b int) {
        c <- a + b
    }(3,4)
    println(<-c) // 7
}

当涉及同时对多个channel进行操作时,Go提供了select机制。通过select,我们可以同时在多个channel上进行发送/接收操作:

select {
case x := <-ch1:     // 从channel ch1接收数据
  ... ...

case y, ok := <-ch2: // 从channel ch2接收数据,并根据ok值判断ch2是否已经关闭
  ... ...

case ch3 <- z:       // 将z值发送到channel ch3中:
  ... ...

default:             // 当上面case中的channel通信均无法实施时,执行该默认分支
}

错误处理

Go提供了简单的、基于错误值比较的错误处理机制,这种机制让每个开发人员必须显式地去关注和处理每个错误。

error类型

Go用error这个接口类型表示错误,并且按惯例,我们通常将error类型返回值放在返回值列表的末尾。

// $GOROOT/src/builtin/builtin.go
type error interface {
    Error() string
}

任何实现了error的Error方法的类型的实例,都可以作为错误值赋值给error接口变量。

Go提供了便捷的构造错误值的方法:

err := errors.New("your first demo error")
errWithCtx = fmt.Errorf("index %d is out of bounds", i)

错误处理形式

Go最常见的错误处理形式如下:

err := doSomething()
if err != nil {
    ... ...
    return err
}

通常我们会定义一些“哨兵”错误值来辅助错误处理方检视(inspect)错误值并做出错误处理分支的决策:

// $GOROOT/src/bufio/bufio.go
var (
    ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
    ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
    ErrBufferFull        = errors.New("bufio: buffer full")
    ErrNegativeCount     = errors.New("bufio: negative count")
)

func doSomething() {
    ... ...
    data, err := b.Peek(1)
    if err != nil {
        switch err {
        case bufio.ErrNegativeCount:
            // ... ...
            return
        case bufio.ErrBufferFull:
            // ... ...
            return
        case bufio.ErrInvalidUnreadByte:
            // ... ...
            return
        default:
            // ... ...
            return
        }
    }
    ... ...
}

Is和As

从Go 1.13版本开始,标准库errors包提供了Is函数用于错误处理方对错误值的检视。Is函数类似于把一个error类型变量与“哨兵”错误值进行比较:

// 类似 if err == ErrOutOfBounds{ … }
if errors.Is(err, ErrOutOfBounds) {
    // 越界的错误处理
}

不同的是,如果error类型变量的底层错误值是一个包装错误(Wrapped Error),errors.Is方法会沿着该包装错误所在错误链(Error Chain),与链上所有被包装的错误(Wrapped Error)进行比较,直至找到一个匹配的错误为止。

标准库errors包还提供了As函数给错误处理方检视错误值。As函数类似于通过类型断言判断一个error类型变量是否为特定的自定义错误类型:

// 类似 if e, ok := err.(*MyError); ok { … }
var e *MyError
if errors.As(err, &e) {
    // 如果err类型为*MyError,变量e将被设置为对应的错误值
}

如果error类型变量的动态错误值是一个包装错误,errors.As函数会沿着该包装错误所在错误链,与链上所有被包装的错误的类型进行比较,直至找到一个匹配的错误类型,就像errors.Is函数那样。

小结

读到这里,你已经对Go语言有了入门级的认知,但要想成为一名Gopher(对Go开发人员的称呼),还需要更进一步的学习与实践。我的极客时间专栏《Go语言第一课》是一个很好的起点,欢迎大家订阅学习^_^。

BTW,本文部分内容由ChatGPT生成!你能猜到是哪些部分吗^_^。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats