常见问题解答(FAQ)
起源
该项目的目的是什么?
2007年Go语言诞生之初,编程领域的格局与如今大不相同。生产环境的软件通常使用C++或Java编写,GitHub尚未出现,大多数计算机还未采用多处理器架构,除了Visual Studio和Eclipse,几乎没有可用的集成开发环境或其他高级工具,更不用说能在互联网上免费获取的了。
与此同时,我们对当时使用的语言及其构建系统在构建大型软件项目时所要求的过度复杂性感到沮丧。自C、C++和Java等语言首次开发以来,计算机的速度已大幅提升,但编程行为本身并未取得同等程度的进步。此外,多处理器架构显然正变得普遍,但大多数语言在高效且安全地对其进行编程方面提供的帮助甚少。
我们决定退后一步,思考随着技术发展,未来几年软件工程将面临哪些主要问题,以及一门新的语言如何能帮助解决它们。例如,多核CPU的兴起表明语言应为某种形式的并发或并行提供一等支持。而为了让大型并发程序中的资源管理变得可行,垃圾回收,或至少某种形式的安全自动内存管理是必需的。
这些考量促成了一系列讨论,Go语言由此诞生,最初是一系列想法和需求,然后发展成为一门语言。一个首要目标是,Go通过支持工具开发、自动化诸如代码格式化之类的日常任务,并消除处理大型代码库的障碍,从而更好地帮助一线程序员。
关于Go语言目标的更广泛描述,以及这些目标是如何实现或至少是如何被接近的,可以在这篇文章中找到:Go at Google: Language Design in the Service of Software Engineering。
该项目的历史是怎样的?
Robert Griesemer、Rob Pike和Ken Thompson于2007年9月21日开始在白板上勾勒一门新语言的目标。几天之内,目标就确定为一项要做的事情的计划,并且对其形态有了大致的构想。设计工作在与不相关工作并行的情况下兼职进行。到2008年1月,Ken已开始着手开发一个用于探索想法的编译器;它输出的是C代码。到年中,该语言已成为一个全职项目,并已足够成熟,可以尝试开发一个生产级编译器。2008年5月,Ian Taylor根据草案规范独立开始为Go开发GCC前端。Russ Cox于2008年底加入,并帮助将语言和库从原型转变为现实。
Go于2009年11月10日成为一个公开的开源项目。社区中无数的人贡献了想法、讨论和代码。
如今全球已有数百万Go程序员——被称为gophers——而且每天都在增加。Go的成功远远超出了我们的预期。
gopher吉祥物的起源是什么?
吉祥物和标志由Renée French设计,她也是Plan 9兔子Glenda的设计者。一篇关于gopher的博客文章解释了它如何衍生自她几年前为WFMUT恤设计使用的一个形象。该标志和吉祥物受Creative Commons Attribution 4.0许可协议保护。
该gopher有一份模型表,说明了它的特征以及如何正确地表现它们。该模型表最初在2016年Gophercon大会上由Renée在一次演讲中展示。它有独特的特征;它是Go gopher,不是随便一只gopher。
这门语言是叫Go还是Golang?
这门语言叫做Go。“golang"这个别名的出现是因为网站最初是golang.org。(那时还没有*.dev*域名。)不过很多人使用golang这个名称,并且它作为一个标签很方便。例如,这门语言在社交媒体上的标签是”#golang"。无论怎样,这门语言的名字就是Go。
附注:尽管官方标志有两个大写字母,但语言的名字写作Go,而不是GO。
你们为什么要创建一门新语言?
Go的诞生源于对我们在谷歌工作时所使用的现有语言和环境的沮丧。编程变得过于困难,而语言的选择是部分原因。人们不得不在高效编译、高效执行或易于编程之间做出选择;在同一种主流语言中无法同时拥有这三者。能够选择的程序员为了易于使用而牺牲了安全性和效率,转而使用Python和JavaScript等动态类型语言,而不是C++,或者程度较轻的Java。
我们并非唯一有此担忧的人。在编程语言领域相当平静多年之后,Go是第一批新语言之一——还有Rust、Elixir、Swift等等——它们使编程语言开发重新成为一个活跃的、几乎主流的领域。Go通过以下方式解决了这些问题:试图将解释型、动态类型语言的编程便捷性与静态类型、编译型语言的高效性和安全性相结合。它还旨在更好地适应当前硬件环境,支持网络化和多核计算。最后,使用Go的体验应该是快速的:在单台计算机上构建大型可执行文件最多只需几秒钟。
实现这些目标促使我们重新思考当前语言的一些编程方式,从而形成了: 组合式而非层次化的类型系统; 对并发和垃圾回收的支持; 严格的依赖规范; 等等。
这些特性无法很好地通过库或工具来实现;因此需要一种新的语言。
文章《谷歌中的Go》讨论了Go语言设计的背景和动机,并对本常见问题中提出的许多答案提供了更多细节。
Go的祖先是什么?
Go主要属于C语言家族(基本语法),并从Pascal/Modula/Oberon家族(声明、包)中汲取了重要灵感,同时也采纳了受Tony Hoare的CSP(通信顺序进程)启发的语言(如Newsqueak和Limbo)中的一些并发思想。然而,它从整体上是一门全新的语言。在每个方面,该语言的设计都基于对程序员工作方式的思考,以及如何使编程——至少是我们所做的那种编程——更有效率,这意味着更有趣。
设计中的指导原则是什么?
在Go被设计时,Java和C++是编写服务器最常用的语言,至少在谷歌是如此。我们认为这些语言需要过多的记账式工作和重复。一些程序员通过转向更动态、更灵活的语言(如Python)来应对,但这牺牲了效率和类型安全性。我们认为应该可以在一门语言中同时拥有效率、安全性和灵活性。
Go试图减少"打字"(键入代码)的量(也隐喻减少冗余)。在其整个设计过程中,我们努力减少杂乱和复杂性。没有前向声明和头文件;所有内容只声明一次。初始化具有表现力、自动化且易于使用。语法简洁且关键字负担轻。重复代码(如foo.Foo* myFoo = new(foo.Foo))通过使用:=声明并初始化构造进行简单类型推导来减少。也许最彻底的是,没有类型层次结构:类型只是存在,它们不必宣告其相互关系。这些简化使Go既富有表现力又易于理解,同时不牺牲生产力。
另一个重要原则是保持概念的正交性。方法可以为任何类型实现;结构体代表数据,接口代表抽象;等等。正交性使得理解事物组合时会发生什么变得更容易。
使用情况
谷歌内部是否使用Go?
是的。Go在谷歌的生产环境中被广泛使用。一个例子是谷歌的下载服务器dl.google.com,它用于分发Chrome二进制文件和其他大型可安装文件,如apt-get包。
Go并非谷歌使用的唯一语言——远非如此——但它是多个领域的关键语言,包括网站可靠性工程(SRE)和大规模数据处理。它也是运行谷歌云软件的关键组成部分。
还有哪些公司在使用Go?
Go的使用在全球范围内不断增长,尤其是在但不限于云计算领域。用Go编写的一些主要云基础设施项目是Docker和Kubernetes,但还有更多。
不过,不仅限于云领域,你可以在go.dev网站上的公司列表以及一些成功案例中看到。此外,Go Wiki包含一个页面,定期更新,列出了部分使用Go的公司。
Wiki上还有一个页面,链接了更多关于使用该语言的公司和项目的成功案例。
Go程序能否与C/C++程序链接?
在同一地址空间中同时使用C和Go是可能的,但这并非天然契合,可能需要特殊的接口软件。此外,将C与Go代码链接会放弃Go所提供的内存安全和栈管理特性。有时,使用C库来解决问题是绝对必要的,但这样做总会引入纯Go代码所没有的风险元素,因此请谨慎操作。
如果你确实需要将C与Go一起使用,具体操作取决于Go编译器的实现。由谷歌Go团队支持的Go工具链中的"标准"编译器称为gc。此外,还有基于GCC的编译器(gccgo)和基于LLVM的编译器(gollvm),以及为不同目的服务、有时实现语言子集的、日益增多的非主流编译器,例如TinyGo。
gc使用与C不同的调用约定和链接器,因此不能直接从C程序调用,反之亦然。cgo程序提供了一种"外部函数接口"机制,允许从Go代码中安全地调用C库。SWIG将此功能扩展到了C++库。
你也可以将cgo和SWIG与gccgo和gollvm一起使用。由于它们使用传统的ABI,在非常谨慎的情况下,也可以将这些编译器生成的代码直接与GCC/LLVM编译的C或C++程序链接。然而,安全地做到这一点需要理解所有相关语言的调用约定,并注意从Go调用C或C++时的栈限制。
Go支持哪些IDE? {#ide}Go 项目不包含定制的 IDE,但该语言及其库的设计使源代码分析变得简单。因此,大多数知名编辑器和 IDE 都能很好地支持 Go,无论是直接支持还是通过插件实现。
Go 团队还维护着一个支持 LSP 协议的 Go 语言服务器,名为 gopls。支持 LSP 的工具可以使用 gopls 来集成语言特定的支持。
提供良好 Go 支持的知名 IDE 和编辑器列表包括:Emacs、Vim、VSCode、Atom、Eclipse、Sublime、IntelliJ(通过名为 GoLand 的定制版本)等等。您常用的开发环境很可能也能高效地用于 Go 编程。
Go 支持 Google 的协议缓冲区吗?
一个单独的开源项目提供了必要的编译器插件和库,位于 github.com/golang/protobuf/。
设计
Go 有运行时系统吗?
Go 拥有一个广泛的运行时库,通常简称为 运行时,它是每个 Go 程序的一部分。此库实现了垃圾回收、并发、栈管理以及 Go 语言的其他关键特性。虽然它对于语言而言更为核心,但 Go 的运行时类似于 C 语言的 libc 库。
然而,重要的是要理解,Go 的运行时并不包含像 Java 运行时所提供的虚拟机。Go 程序会提前编译为本地机器码(或者对于某些变体实现,编译为 JavaScript 或 WebAssembly)。因此,尽管“运行时”一词常用于描述程序运行的虚拟环境,但在 Go 中,“运行时”只是提供关键语言服务的那个库的名称。
Unicode 标识符是怎么回事?
在设计 Go 时,我们希望确保它不会过度以 ASCII 为中心,这意味着要将标识符空间从 7 位 ASCII 的限制中扩展出来。Go 的规则——标识符字符必须是 Unicode 定义的字母或数字——简单易懂且易于实现,但存在一些限制。例如,组合字符被设计排除在外,这排除了某些语言(如天城文)。
这条规则还有另一个令人遗憾的后果。由于导出的标识符必须以大写字母开头,根据定义,由某些语言的字符创建的标识符无法被导出。目前唯一的解决方案是使用类似 X日本語 的形式,这显然不能令人满意。
自该语言最早的版本以来,关于如何最好地扩展标识符空间以适应使用其他母语的程序员,已有大量的思考。具体该怎么做仍是一个活跃的讨论话题,未来版本的语言可能会在标识符定义上更加灵活。例如,它可能会采纳 Unicode 组织关于标识符的 一些建议。无论发生什么,都必须在保持(或可能扩展)字母大小写决定标识符可见性的方式下兼容地进行,这仍然是我们最喜欢的 Go 特性之一。
目前,我们有一个简单的规则,可以在不破坏现有程序的前提下进行扩展,该规则避免了因允许歧义标识符而必然会出现的 bug。
为什么 Go 没有特性 X?
每种语言都包含一些新颖特性,同时会忽略某些人喜爱的特性。Go 的设计着眼于编程的便捷性、编译速度、概念的正交性以及支持并发和垃圾回收等特性的需要。您可能偏爱的特性缺失,可能是因为它不合适,因为它影响编译速度或设计的清晰度,或者因为它会使基础系统模型变得过于复杂。
如果 Go 缺少特性 X 令您困扰,请原谅我们,并探索 Go 所拥有的特性。您可能会发现它们以有趣的方式弥补了 X 的缺失。
Go 何时引入了泛型类型?
Go 1.18 版本为该语言添加了类型参数。这允许了一种多态或泛型编程的形式。详情请参阅语言规范和提案。
为什么 Go 最初发布时没有泛型类型?
Go 最初被设计为一种易于长期维护的服务器程序编写语言。(有关更多背景信息,请参阅此文)。其设计专注于可扩展性、可读性和并发性等方面。当时,多态编程似乎对语言目标并非必不可少,因此为了简化而最初未包含。
泛型很方便,但它们给类型系统和运行时带来了复杂性成本。我们花了一段时间才开发出一种我们认为其价值与复杂性相称的方案。
为什么 Go 没有异常?
我们认为,像 try-catch-finally 习惯用法那样将异常耦合到控制结构中,会导致代码混乱。这也往往鼓励程序员将许多普通错误(例如打开文件失败)标记为异常。
Go 采用了不同的方法。对于普通的错误处理,Go 的多值返回使得报告错误变得容易,而不会使返回值负担过重。一个规范的错误类型,结合 Go 的其他特性,使得错误处理既愉快又与其他语言大不相同。Go 还提供了一些内置函数,用于发出真正异常状况的信号并从中恢复。恢复机制仅在函数状态因错误而被拆解时执行,这足以应对灾难性情况,无需额外控制结构,若使用得当,还能产生清晰的错误处理代码。
详见延迟执行、恐慌与恢复一文。此外,错误即值这篇博文通过演示如何利用错误本身作为值这一特性,充分运用 Go 语言的能力进行错误处理,描述了一种在 Go 中干净处理错误的方法。
为什么 Go 没有断言?
Go 不提供断言功能。尽管断言无疑具有便利性,但我们的经验表明,程序员容易将其作为逃避深入思考适当错误处理与报告的“拐杖”。适当的错误处理意味着服务器能在非致命错误后继续运行而非崩溃。适当的错误报告意味着错误信息直接切中要点,让程序员无需解读冗长的崩溃追踪信息。对于不熟悉代码的程序员而言,精确的错误信息尤为重要。
我们理解这是一个有争议的点。Go 语言及其库中存在许多与现代实践不同的设计,纯粹是因为我们认为有时值得尝试不同的方法。
为什么基于 CSP 思想构建并发?
并发和多线程编程长期以难度著称。我们认为这既源于 pthreads 等复杂设计,也源于对互斥锁、条件变量、内存屏障等底层细节的过度强调。更高层次的接口能够实现更简洁的代码,即使底层仍存在互斥锁等机制。
为并发提供高级语言支持的最成功模型之一源自 Hoare 的通信顺序进程(CSP)。Occam 和 Erlang 是两种源于 CSP 的知名语言。Go 的并发原语源自该家族谱系的另一分支,其主要贡献是将 channel 作为一等公民的强大概念。多种早期语言的经验表明,CSP 模型能很好地融入过程式语言框架。
为什么使用 goroutine 而非线程?
Goroutine 是实现易用并发的一部分。其设计理念存在已久:将独立执行的函数(即协程)多路复用到一组线程上。当某个协程阻塞(例如调用阻塞系统调用)时,运行时会自动将同一线程上的其他协程迁移至其他可运行线程,避免它们被阻塞。程序员对此毫无感知,这正是关键所在。我们称这种机制为 goroutine,它可以非常轻量:除栈内存(仅几千字节)外几乎无额外开销。
为保持栈空间小巧,Go 的运行时使用可调整的有界栈。新创建的 goroutine 仅分配几千字节,这通常足够使用。当空间不足时,运行时会自动扩展(或收缩)存储栈的内存,使得大量 goroutine 能在有限内存中存在。每个函数调用平均仅增加约三条廉价指令的 CPU 开销。在同一地址空间中创建数十万个 goroutine 是可行的——若它们只是普通线程,系统资源在更小数量级时就会耗尽。
为什么未将 map 操作定义为原子操作?
经过长期讨论,我们决定:由于 map 的典型用法无需多 goroutine 安全访问,而在确需安全的场景中,map 往往已是更大同步数据结构或计算的一部分。因此要求所有 map 操作都获取互斥锁会拖慢大多数程序,却仅能为少数场景提升安全性。这并非易事,因为这意味着不受控的 map 访问可能导致程序崩溃。
该语言并未禁止原子 map 更新。在需要时(例如承载不可信程序),实现方案可对 map 访问进行互锁控制。
Map 访问仅在发生更新时才是不安全的。只要所有 goroutine 只进行读取操作(包括通过 for range 循环遍历)——不对元素赋值或执行删除操作——那么无需同步即可安全地并发访问 map。
为辅助正确使用 map,部分语言实现包含特殊检查机制,可在运行时自动报告并发修改 map 的不安全行为。此外,sync 库中提供 sync.Map 类型,虽然不适合作为内置 map 类型的通用替代品,但对静态缓存等特定使用模式能良好工作。
你们会接受我的语言修改建议吗?
人们经常提出语言改进建议——邮件列表中包含大量此类讨论历史——但被采纳的修改寥寥无几。尽管Go是一个开源项目,但其语言和库受到兼容性承诺的保护,该承诺防止破坏现有程序的更改——至少在源代码层面(程序可能偶尔需要重新编译以保持最新状态)。
如果您的提案违反了Go 1规范,那么无论其优点如何,我们都无法考虑。
Go未来的重大版本可能与Go 1不兼容,但相关讨论才刚刚开始,且有一点是确定的:在此过程中引入的不兼容性将会非常少。
此外,兼容性承诺促使我们在必要时为旧程序提供自动迁移路径。
即使您的提案与Go 1规范兼容,它也可能不符合Go的设计目标。
文章《Go在谷歌:服务于软件工程的语言设计》解释了Go的起源及其设计背后的动机。
类型
Go是面向对象的语言吗?
既是也不是。虽然Go拥有类型和方法,并允许面向对象的编程风格,但它没有类型层次结构。
Go中的“接口”概念提供了一种不同的方法,我们认为它更易于使用,并且在某些方面更为通用。
此外,Go支持将类型嵌入其他类型中,以提供类似于(但不完全相同)子类化的功能。
而且,Go中的方法比C++或Java中的更通用:它们可以为任何数据类型定义,甚至包括内置类型(如普通的、“未装箱”的整数)。
方法不仅限于结构体(类)。
同时,缺乏类型层次结构使得Go中的“对象”感觉比在C++或Java等语言中更加轻量级。
如何实现方法的动态分派?
实现动态分派方法的唯一途径是通过接口。结构体或任何其他具体类型上的方法总是静态解析的。
为什么没有类型继承?
面向对象编程(至少在知名语言中)涉及大量关于类型之间关系的讨论,而这些关系通常可以自动推导。Go采取了不同的方法。
Go不要求程序员预先声明两个类型相关,而是让类型自动满足任何指定其方法子集的接口。
除了减少记录工作,这种方法还具有实际优势:
- 类型可以同时满足多个接口,避免了传统多重继承的复杂性。
- 接口可以非常轻量级——一个接口甚至可以只有一个或零个方法,就能表达一个有用的概念。
- 如果有新想法出现或出于测试目的,可以在事后添加接口——而无需修改原始类型。
- 由于类型和接口之间没有显式关系,因此无需管理或讨论类型层次结构。
这些思想可用于构建类似于类型安全的Unix管道的结构。例如,可以查看fmt.Fprintf如何实现格式化输出到任意目标(而不仅是文件),或者bufio包如何完全独立于文件I/O,亦或image包如何生成压缩的图像文件。
所有这些想法都源于一个单一的接口(io.Writer),它代表一个单一的方法(Write)。而这仅仅是表面层次。Go的接口对程序结构有深远的影响。
需要一些时间来适应,但这种隐式的类型依赖风格是Go最具生产力的特性之一。
为什么len是函数而不是方法?
我们曾讨论过这个问题,但最终决定将len及其类似函数实现为函数在实践中是可行的,并且不会使基本类型的接口(在Go类型意义上)问题复杂化。
为什么Go不支持方法和运算符的重载?
如果方法分派不需要进行类型匹配,它将更加简化。
其他语言的经验告诉我们,拥有多种同名但不同签名的方法有时很有用,但在实践中也可能令人困惑且脆弱。
仅通过名称匹配并要求类型一致性是Go类型系统中的一个重大简化决策。
至于运算符重载,它似乎更多是一种便利而非绝对需求。同样,没有它会使事情更简单。
为什么Go没有implements声明?
Go类型通过实现接口的方法来实现该接口,仅此而已。
这一特性允许在不修改现有代码的情况下定义和使用接口。它实现了一种结构类型,促进了关注点分离,提高了代码重用性,并使得基于代码发展过程中涌现的模式进行构建变得更容易。
接口的语义是Go轻巧、灵活感的主要原因之一。
更多细节请参阅关于类型继承的问题。
如何保证我的类型满足某个接口?
您可以通过尝试使用T或指向T的指针的零值进行赋值,来让编译器检查类型T是否实现了接口I:go type T struct{} var _ I = T{} // 验证T实现了I。 var _ I = (*T)(nil) // 验证*T实现了I。 如果T(或相应地*T)没有实现接口I,这个错误将在编译时被捕获。
如果您希望接口的使用者显式声明他们实现了该接口,可以在接口的方法集中添加一个具有描述性名称的方法。例如:type Fooer interface { Foo() ImplementsFooer() }那么,类型必须实现 ImplementsFooer 方法才能成为 Fooer,从而清晰地记录这一事实,并在 go doc 的输出中声明该实现。```
type Bar struct{}
func (b Bar) ImplementsFooer() {}
func (b Bar) Foo() {}
### 为什么类型 T 不满足 Equal 接口? {#t_and_equal_interface}
考虑这个表示可以与另一个值进行比较的对象的简单接口:```
type Equaler interface {
Equal(Equaler) bool
}
```以及这个类型`T`:```go
type T int
func (t T) Equal(u T) bool { return t == u } // 不满足Equaler接口
```与某些多态类型系统中的类似情况不同,
`T` 并未实现 `Equaler` 接口。
`T.Equal` 的参数类型是 `T`,
而非接口要求的 `Equaler` 类型。
在Go语言中,类型系统不会自动提升 `Equal` 的参数类型;
这是程序员需要自行处理的工作,
正如实现了 `Equaler` 接口的类型 `T2` 所示:```go
type T2 int
func (t T2) Equal(u Equaler) bool { return t == u.(T2) } // 满足Equaler接口
```不过这仍与其他类型系统有所不同,因为在Go语言中,*任何*满足`Equaler`接口的类型都可以作为参数传递给`T2.Equal`,但运行时我们必须验证该参数是否为`T2`类型。有些语言会在编译期就确保这一点成立。
相关例子也有相反的情况:```
type Opener interface {
Open() Reader
}
func (t T3) Open() *os.File
```在Go语言中,`T3` 并不满足 `Opener` 接口,尽管在其他语言中可能满足。
虽然在这种情况下,Go的类型系统确实为程序员做的工作更少,但缺乏子类型化使得接口满足的规则非常容易陈述:函数的名称和签名是否与接口的完全一致?
Go的规则也易于高效实现。
我们认为这些好处弥补了缺乏自动类型提升的不足。
### 能否将 []T 转换为 []interface{}? {#convert_slice_of_interface}
不能直接转换。
语言规范禁止这样做,因为这两种类型在内存中的表示方式不同。
有必要将元素逐个复制到目标切片中。以下示例将一个 `int` 切片转换为 `interface{}` 切片:```
t := []int{1, 2, 3, 4}
s := make([]interface{}, len(t))
for i, v := range t {
s[i] = v
}
```### 如果 T1 和 T2 具有相同的底层类型,能否将 []T1 转换为 []T2? {#convert_slice_with_same_underlying_type}
这段代码示例的最后一行无法编译。```
type T1 int
type T2 int
var t1 T1
var x = T2(t1) // 可以
var st1 []T1
var sx = ([]T2)(st1) // 不行
```在Go语言中,类型与方法紧密相关,每个命名类型都有一个(可能为空的)方法集。
基本规则是,你可以更改被转换类型的名称(从而可能更改其方法集),但你不能更改复合类型元素的名称(及其方法集)。
Go要求你必须明确进行类型转换。
### 为什么我的nil error值不等于nil? {#nil_error}
在底层,接口由两个元素实现:一个类型 `T` 和一个值 `V`。
`V` 是一个具体值,例如一个 `int`、`struct` 或指针,它本身绝不是接口,并且具有类型 `T`。
例如,如果我们将 `int` 值 3 存储到一个接口中,那么得到的接口值在示意图形式上为 (`T=int`, `V=3`)。
值 `V` 也被称为接口的 *动态* 值,因为在程序执行过程中,一个给定的接口变量可能持有不同的值 `V`(以及对应的类型 `T`)。
一个接口值只有当 `V` 和 `T` 都未被设置时才是 `nil`(`T=nil`,`V` 未被设置)。
具体来说,一个 `nil` 接口总是持有一个 `nil` 类型。
如果我们将一个类型为 `*int` 的 `nil` 指针存储到一个接口值内部,其内部类型将是 `*int`,而不管指针的值如何:(`T=*int`, `V=nil`)。
因此,这样一个接口值将是 *非* `nil` 的, *即使其内部的指针值 `V` 是* `nil`。
这种情况可能会令人困惑,它出现在一个 `nil` 值被存储到一个接口值内部时,例如一个 `error` 返回值:```go
func returnsError() error {
var p *MyError = nil
if bad() {
p = ErrBad
}
return p // 总是返回一个非空错误。
}
```如果一切顺利,函数返回一个 `nil` 的 `p`,因此返回值是一个 `error` 接口值,包含 (`T=*MyError`, `V=nil`)。这意味着,如果调用者将返回的错误与 `nil` 进行比较,即使实际上没有发生任何错误,它也会看起来好像存在一个错误。为了向调用者返回一个正确的 `nil` `error`,函数必须显式地返回一个 `nil`:```
func returnsError() error {
if bad() {
return ErrBad
}
return nil
}
```对于返回错误的函数,始终在签名中使用 `error` 接口类型(如上例所示),而非具体类型(如 `*MyError`),有助于确保错误创建的正确性。例如,[`os.Open`](/pkg/os/#Open) 函数返回的是 `error` 类型,尽管当其非 `nil` 时,实际类型始终是具体类型 [`*os.PathError`](/pkg/os/#PathError)。
只要使用接口,就可能出现与本文描述类似的情况。请记住:如果接口中已存储任何具体值,则该接口就不会是 `nil`。更多信息请参见[《反射定律》](/doc/articles/laws_of_reflection.html)。
### 为何零大小类型的行为显得异常? {#zero_size_types}
Go 语言支持零大小类型,例如无字段的结构体(`struct{}`)或无元素的数组(`[0]byte`)。零大小类型无法存储任何值,但在不需要值的场景(如 `map[int]struct{}` 或仅包含方法而无需存储值的类型)中仍有用途。
不同的零大小类型变量可能被放置在内存的同一位置。由于这些变量无法存储值,因此这种处理方式是安全的。
此外,语言规范不保证指向两个不同零大小变量的指针是否相等。此类比较结果甚至可能在程序运行过程中发生变化:某次执行返回 `true`,另一次执行返回 `false`,具体取决于程序的编译和执行方式。
零大小类型的另一个相关问题是:指向零大小结构体字段的指针不得与内存中其他对象的指针重叠,否则可能导致垃圾回收器混淆。这意味着若结构体的最后一个字段为零大小类型,该结构体将被填充,以确保指向末尾字段的指针不会与结构体紧邻的内存重叠。因此,以下程序:
---
### 为什么 Go 没有未标记联合(如 C 语言)? {#unions}
未标记联合将违反 Go 的内存安全保证。
### 为什么 Go 没有变体类型? {#variant_types}
变体类型(亦称代数类型)提供了一种……```
func main() {
type S struct {
f1 byte
f2 struct{}
}
fmt.Println(unsafe.Sizeof(S{}))
}
```大多数 Go 实现中会打印 `2` 而非 `1`。
### 为什么 Go 没有未标记联合(如 C 语言)? {#unions}
未标记联合将违反 Go 的内存安全保证。
### 为什么 Go 没有变体类型? {#variant_types}
变体类型(亦称代数类型)提供了一种方式,用于指定某个值可能属于一组其他类型中的某一种,且仅限于这些类型。系统编程中一个常见的例子是:规定错误类型可能是网络错误、安全错误或应用程序错误,并允许调用方通过检查错误类型来区分问题来源。另一个例子是语法树,其中每个节点可以是不同类型:声明、语句、赋值等等。
我们曾考虑将变体类型加入 Go,但经过讨论后决定不纳入,因为它们会以令人困惑的方式与接口重叠。如果变体类型的元素本身是接口,会发生什么?
此外,变体类型所解决的部分问题,语言中已有其他方式覆盖。错误处理的例子很容易表达:使用接口值来保存错误,并用类型开关来区分情况。语法树的例子也可实现,尽管不那么优雅。
### 为什么 Go 没有协变结果类型? {#covariant_types}
协变结果类型将意味着像这样的接口……```
type Copyable interface {
Copy() interface{}
}
```将由该方法满足```
func (v Value) Copy() Value
```因为`Value`实现了空接口。
在Go中,方法类型必须精确匹配,所以`Value`并未实现`Copyable`。
Go将类型做什么(即其方法)与类型的实现分离开来。
如果两个方法返回不同的类型,它们就不是做同样的事情。
想要协变结果类型的程序员通常试图通过接口表达类型层次结构。
在Go中,更自然的方式是让接口和实现之间保持清晰的分离。
## 值 {#Values}
### 为什么Go不提供隐式数值转换? {#conversions}
C语言中数值类型之间自动转换带来的便利,被其引发的混淆所抵消。一个表达式何时是无符号的?值有多大?是否会发生溢出?结果是否可移植,独立于执行它的机器?
这也使编译器复杂化;C语言中“通常的算术转换”不易实现,且在不同架构间不一致。
出于可移植性的考虑,我们决定以代码中一些显式转换为代价,使事情清晰直白。
然而,Go中常量的定义——任意精度、无符号和大小标注的值——大大缓解了这个问题。
一个相关的细节是,与C语言不同,即使`int`是64位类型,`int`和`int64`也是不同的类型。`int`类型是通用的;如果你关心整数占用的位数,Go鼓励你明确指定。
### Go中的常量是如何工作的? {#constants}
尽管Go对不同数值类型变量之间的转换要求严格,但语言中的常量却灵活得多。
字面常量如`23`、`3.14159`和[`math.Pi`](/pkg/math/#pkg-constants)存在于一种理想的数字空间中,具有任意精度,没有上溢或下溢。
例如,`math.Pi`的值在源代码中被指定到63位小数,涉及该值的常量表达式能保持超出`float64`所能表示的精度。
只有当常量或常量表达式被赋值给一个变量——程序中的一个内存位置时,它才会变成一个具有常规浮点属性和精度的“计算机”数字。
此外,由于它们只是数字,不是有类型的值,Go中的常量可以比变量更自由地使用,从而缓和了严格转换规则带来的一些不便。
可以编写这样的表达式,例如```
sqrt2 := math.Sqrt(2)
```因为理想数字 `2` 可以安全且准确地转换为 `float64` 类型以用于 `math.Sqrt` 函数调用,所以编译器不会报错。
一篇题为 [Constants](/blog/constants) 的博客文章对此主题有更深入的探讨。
### 为什么映射是内置的? {#builtin_maps}
原因与字符串相同:映射是一种强大而重要的数据结构,提供一种优秀的实现并辅以语法支持,能让编程更加愉悦。我们相信 Go 的映射实现足够强大,能满足绝大多数使用场景。如果特定应用能从自定义实现中获益,确实可以编写一个,但语法上不会那么方便;这似乎是一个合理的权衡。
### 为什么映射不允许使用切片作为键? {#map_keys}
映射查找需要相等运算符,而切片没有实现它。切片没有实现相等性是因为在此类类型上相等性定义不明确;其中涉及诸多考量,包括浅比较与深比较、指针比较与值比较、如何处理递归类型等等。我们可能会重新审视这个问题——为切片实现相等性不会使任何现有程序失效——但在对切片相等性的含义没有清晰共识之前,目前将其省略更为简单。
结构体和数组定义了相等性,因此它们可以用作映射的键。
### 为什么映射、切片和通道是引用类型,而数组是值类型? {#references}
这个话题有很多历史渊源。早期,映射和通道在语法上就是指针,无法声明或使用非指针实例。同时,我们在数组的工作方式上也进行了多次尝试。最终我们决定,将指针与值严格区分会使语言更难使用。将这些类型更改为引用相关联的共享数据结构,解决了这些问题。这一变化给语言增加了一些令人遗憾的复杂性,但对易用性产生了巨大影响:当这一改变引入时,Go 变成了一门更高效、更舒适的编程语言。
## 编写代码 {#Writing_Code}
### 库如何被文档化? {#How_are_libraries_documented}
要从命令行访问文档,[go](/pkg/cmd/go/) 工具提供了一个 [doc](/pkg/cmd/go/#hdr-Show_documentation_for_package_or_symbol) 子命令,它为声明、文件、包等提供文本界面的文档查看。
全局包发现页面 [pkg.go.dev/pkg/](/pkg/) 运行着一个服务器,它可以从网络上任何地方的 Go 源代码中提取包文档,并将其作为 HTML 页面提供服务,其中包含指向声明和相关元素的链接。这是了解现有 Go 库的最简便方式。
在项目早期,有一个类似的程序 `godoc`,它也可以用来提取本地机器上文件的文档;[pkg.go.dev/pkg/](/pkg/) 本质上是其后继者。另一个后继者是 [`pkgsite`](https://pkg.go.dev/golang.org/x/pkgsite/cmd/pkgsite) 命令,它像 `godoc` 一样可以在本地运行,尽管它尚未集成到 `go` `doc` 显示的结果中。
### 是否有 Go 编程风格指南? {#Is_there_a_Go_programming_style_guide}
虽然没有明确的风格指南,但无疑存在一种可识别的“Go 风格”。
Go 建立了惯例来指导关于命名、布局和文件组织的决策。文档 [Effective Go](effective_go.html) 包含了一些关于这些主题的建议。更直接地,程序 `gofmt` 是一个代码美化工具,其目的是强制执行布局规则;它取代了通常需要解释的、充满“该做”与“不该做”的准则汇编。代码库中的所有 Go 代码,以及开源世界中的绝大部分代码,都经过了 `gofmt` 的格式化处理。
题为 [Go Code Review Comments](/s/comments) 的文档收集了许多关于 Go 习惯用法的短文,这些细节常被程序员忽略。它是为 Go 项目进行代码审查人员的便捷参考。
### 如何向 Go 库提交补丁? {#How_do_I_submit_patches_to_the_Go_libraries}
库源代码位于代码仓库的 `src` 目录中。如果你想进行重大更改,请在开始之前先在邮件列表上讨论。
请参阅文档 [Contributing to the Go project](contribute.html) 以了解如何操作的更多信息。
### 为什么 “go get” 在克隆仓库时使用 HTTPS? {#git_https}
公司通常仅允许在标准 TCP 端口 80 (HTTP) 和 443 (HTTPS) 上进行出站流量通信,阻止其他端口的出站流量,包括 TCP 端口 9418 (git) 和 TCP 端口 22 (SSH)。当使用 HTTPS 而非 HTTP 时,`git` 默认会强制执行证书验证,从而提供针对中间人攻击、窃听和篡改攻击的防护。因此,`go get` 命令出于安全考虑使用 HTTPS。
`Git` 可以配置为通过 HTTPS 进行身份验证,或使用 SSH 代替 HTTPS。要通过 HTTPS 进行身份验证,你可以在 git 会查阅的 `$HOME/.netrc` 文件中添加一行:```
machine github.com login *USERNAME* password *APIKEY*
```对于 GitHub 账户,密码可以使用[个人访问令牌](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/)。
`Git` 也可以配置为使用 SSH 代替 HTTPS 来访问匹配特定前缀的 URL。
例如,若要对所有 GitHub 访问使用 SSH,请将以下内容添加到你的 `~/.gitconfig` 文件中:```
[url "ssh://git@github.com/"]
insteadOf = https://github.com/
```当使用公共模块代理处理依赖项,但需要使用私有模块时,你可能需要设置 `GOPRIVATE`。
详情及额外设置请参阅[私有模块](/ref/mod#private-modules)。
### 如何使用 "go get" 管理包版本? {#get_version}
Go 工具链内置了一个用于管理版本化关联包集合的系统,称为*模块*。
该功能在 [Go 1.11](/doc/go1.11#modules) 中引入,并在 [1.14](/doc/go1.14#introduction) 版本后达到生产就绪状态。
要使用模块创建项目,请运行 [`go mod init`](/ref/mod#go-mod-init)。
此命令会生成一个 `go.mod` 文件,用于跟踪依赖项版本。```
go mod init example/project
```要添加、升级或降级某个依赖项,请运行 [`go get`](/ref/mod#go-get) 命令:```
go get golang.org/x/text@v0.3.5
```参阅[教程:创建模块](/doc/tutorial/create-module.html)了解更多入门信息。
参阅[开发模块](/doc/#developing-modules)获取使用模块管理依赖的指南。
模块内的包在演进时应遵循[导入兼容性规则](https://research.swtch.com/vgo-import)以保持向后兼容:
> 如果旧包和新包拥有相同的导入路径,
> 则新包必须与旧包向后兼容。
[Go 1 兼容性指南](/doc/go1compat.html)是一个很好的参考:
不要移除导出的名称,鼓励使用带标签的复合字面量,等等。
如果需要不同的功能,请添加新名称,而不是修改旧名称。
模块通过[语义化版本控制](https://semver.org/)和语义化导入版本化将此规则形式化。
如果必须破坏兼容性,请在新的主版本上发布模块。
主版本号 2 及更高的模块需要在其路径中包含[主版本后缀](/ref/mod#major-version-suffixes)(例如 `/v2`)。
这保留了导入兼容性规则:一个模块的不同主版本中的包具有不同的路径。
## 指针与分配 {#Pointers}
### 函数参数何时是按值传递的? {#pass_by_value}
与所有 C 家族的语言一样,Go 中一切都是按值传递的。
也就是说,函数总是获得被传递对象的一个副本,就像是有一个赋值语句将该值赋给参数一样。例如,将一个 `int` 值传递给函数会复制该 `int`,而传递一个指针值则会复制该指针,但不会复制它指向的数据。
(关于这如何影响方法接收器的讨论,请参见[后续章节](/doc/faq#methods_on_values_or_pointers))。
映射(map)和切片(slice)值的行为类似指针:它们是包含指向底层映射或切片数据的指针的描述符。复制一个映射或切片值不会复制它指向的数据。复制一个接口(interface)值会复制存储在接口值中的对象。如果接口值持有一个结构体(struct),复制接口值会复制该结构体。如果接口值持有一个指针,复制接口值会复制该指针,但同样不会复制它指向的数据。
请注意,这里的讨论是关于操作的语义。实际的实现可能会应用优化来避免复制,只要这些优化不改变语义即可。
### 我何时应该使用指向接口的指针? {#pointer_to_interface}
几乎从不。指向接口值的指针只出现在罕见、棘手的情况下,这些情况涉及为延迟评估而伪装接口值的类型。
将指向接口值的指针传递给期望接口的函数是一个常见错误。编译器会报告此错误,但情况可能仍然令人困惑,因为有时需要[指针来满足接口](#different_method_sets)。
关键在于,虽然指向具体类型的指针可以满足接口,但有一个例外:*指向接口的指针永远不能满足接口*。
考虑变量声明,```
var w io.Writer
```打印函数 `fmt.Fprintf` 将满足 `io.Writer` 接口的值作为其第一个参数——即实现了标准 `Write` 方法的对象。因此我们可以这样写:```
fmt.Fprintf(w, "hello, world\n")
```然而,如果我们传递 `w` 的地址,程序将无法编译。```
fmt.Fprintf(&w, "hello, world\n") // [编译时错误。]
```唯一例外是,任何值(包括指向接口的指针)都可以赋值给空接口类型(`interface{}`)的变量。即便如此,如果该值是指向接口的指针,这几乎肯定是一个错误;其结果可能会令人困惑。
### 应该在值上还是指针上定义方法? {#methods_on_values_or_pointers}```
func (s *MyStruct) pointerMethod() { } // 指针上的方法
func (s MyStruct) valueMethod() { } // 值上的方法
```对于不习惯指针的程序员来说,这两个例子的区别可能会令人困惑,但实际上情况非常简单。在定义类型上的方法时,接收者(上例中的 `s`)的行为与作为方法的参数完全相同。那么,是将接收者定义为值还是指针,这与函数参数应该是值还是指针的问题是相同的。
有几个考虑因素。
首先,也是最重要的,方法是否需要修改接收者?
如果需要,接收者*必须*是指针。
(切片和映射表现得像引用,因此它们的情况更微妙一些,但例如要在方法中改变切片的长度,接收者仍然必须是指针。)
在上面的例子中,如果 `pointerMethod` 修改了 `s` 的字段,
调用者将看到这些更改,但 `valueMethod` 是使用调用者参数的副本调用的(这就是传递值的定义),因此它所做的更改对调用者来说是不可见的。
顺便说一句,在Java中,方法接收者始终是指针,尽管其指针特性有些隐蔽(最近的发展正在将值接收者引入Java)。Go 中的值接收者才是不寻常的。
其次是效率的考虑。如果接收者很大,例如一个大型 `struct`,使用指针接收者可能会更高效。
接下来是一致性。如果类型的某些方法必须使用指针接收者,那么其余方法也应该如此,以便无论类型如何使用,方法集都是一致的。
有关详细信息,请参阅关于[方法集](#different_method_sets)的部分。
对于基本类型、切片和小型 `struct` 等类型,值接收者的开销非常小,因此除非方法的语义要求指针,否则值接收者既高效又清晰。
### `new` 和 `make` 有什么区别? {#new_and_make}
简而言之:`new` 分配内存,而 `make` 初始化切片、映射和通道类型。
更多详细信息,请参阅[《Effective Go》的相关章节](/doc/effective_go.html#allocation_new)。
### 64位机器上 `int` 的大小是多少? {#q_int_sizes}
`int` 和 `uint` 的大小是特定于实现的,但在同一平台上彼此相同。
为了可移植性,依赖特定大小值的代码应使用显式大小的类型,例如 `int64`。
在32位机器上,编译器默认使用32位整数,而在64位机器上,整数是64位。
(历史上,这并非总是如此。)
另一方面,浮点标量和复数类型始终是有大小的(没有 `float` 或 `complex` 基本类型),因为程序员在使用浮点数时应注意精度。
(无类型)浮点常量的默认类型是 `float64`。
因此 `foo` `:=` `3.0` 声明了一个类型为 `float64` 的变量 `foo`。
对于由(无类型)常量初始化的 `float32` 变量,必须在变量声明中显式指定变量类型:```
var foo float32 = 3.0
```或者,必须通过类型转换给常量指定类型,例如 `foo := float32(3.0)`。
### 如何判断变量分配在堆上还是栈上? {#stack_or_heap}
从正确性角度看,你无需关心这点。在 Go 中,只要存在对变量的引用,该变量就会一直存在。实现选择存储位置与语言语义无关。
存储位置确实会影响高效程序的编写。只要可能,Go 编译器会将函数局部变量分配到该函数的栈帧中。但如果编译器无法证明变量在函数返回后不会被引用,则必须将其分配到垃圾回收堆中,以避免悬空指针错误。此外,如果局部变量体积很大,将其存储在堆上可能比栈上更合理。
在当前编译器中,如果变量被取地址,该变量就会成为堆分配的候选对象。但基本的逃逸分析能识别出某些情况下此类变量不会存活到函数返回之后,从而可将其分配在栈上。
### 为什么我的 Go 进程使用这么多虚拟内存? {#Why_does_my_Go_process_use_so_much_virtual_memory}
Go 内存分配器会预留一大块虚拟内存作为分配区域。此虚拟内存专属于特定 Go 进程;这种预留不会剥夺其他进程的内存。
要查看 Go 进程实际分配的内存量,请使用 Unix 的 `top` 命令并查看 `RES`(Linux)或 `RSIZE`(macOS)列。
## 并发 {#Concurrency}
### 哪些操作是原子的?互斥锁呢? {#What_operations_are_atomic_What_about_mutexes}
Go 中操作的原子性说明可参阅 [Go 内存模型](/ref/mem) 文档。
底层同步和原子原语可通过 [sync](/pkg/sync) 和 [sync/atomic](/pkg/sync/atomic) 包获得。这些包适用于简单任务,如递增引用计数或保证小规模互斥。
对于更高层的操作(如并发服务器间的协调),采用更高级的技术能带来更优的程序结构,Go 通过 goroutine 和 channel 支持这种方法。例如,你可以设计程序使某个时刻仅有一个 goroutine 负责特定数据。这种方法被概括为最初的 [Go 语言箴言](https://www.youtube.com/watch?v=PAAkCSZUG1c):
不要通过共享内存来通信,而要通过通信来共享内存。
详情请参阅[通过通信共享内存](/doc/codewalk/sharemem/)代码走读及其[配套文章](/blog/share-memory-by-communicating)。
大型并发程序很可能同时借鉴这两种工具包。
### 为什么增加 CPU 数量后程序没有变快? {#parallel_slow}
程序是否随 CPU 数量增加而变快,取决于其解决的问题类型。Go 语言提供了并发原语(如 goroutine 和 channel),但只有当底层问题本质上可并行时,并发才能实现并行加速。本质上串行的问题无法通过增加 CPU 加速,而可分解为并行执行部分的问题则能获得加速,有时效果显著。
有时增加 CPU 反而会拖慢程序。实际中,若程序将更多时间用于同步或通信而非有效计算,在使用多线程时性能可能下降。这是因为线程间传递数据涉及上下文切换,开销较大且会随 CPU 数量增加而上升。例如,Go 规范中的[素数筛示例](/ref/spec#An_example_package)虽启动了许多 goroutine,但并无显著并行性;增加线程(CPU)数量更可能使其变慢而非变快。
更多关于此主题的细节,请参阅题为[并发并非并行](/blog/concurrency-is-not-parallelism)的演讲。
### 如何控制 CPU 数量? {#number_cpus}
可同时执行 goroutine 的 CPU 数量由 `GOMAXPROCS` shell 环境变量控制,其默认值为可用的 CPU 核心数。因此有并行执行潜力的程序在多核机器上默认就能实现并行。要更改使用的并行 CPU 数量,可设置该环境变量或使用同名的[函数](/pkg/runtime/#GOMAXPROCS)来配置运行时支持,使其使用不同数量的线程。将其设为 1 将消除真正并行的可能性,强制独立的 goroutine 轮流执行。
运行时可分配比 `GOMAXPROCS` 值更多的线程来处理多个未完成的 I/O 请求。`GOMAXPROCS` 仅影响同时实际执行的 goroutine 数量;更多 goroutine 可能阻塞在系统调用中。
Go 的 goroutine 调度器能很好地平衡 goroutine 和线程,甚至可抢占某个 goroutine 的执行以确保同一线程上的其他 goroutine 不会饥饿。但这并不完美。若遇到性能问题,按应用程序设置 `GOMAXPROPS` 可能有帮助。
### 为什么没有 goroutine ID? {#no_goroutine_id}
Goroutine 没有名称;它们只是匿名工作者。它们不向程序员暴露唯一标识符、名称或数据结构。有些人对此感到惊讶,期望 `go` 语句能返回某个可供后续访问和控制该 goroutine 的项目。goroutine 匿名的根本原因在于,这样在编写并发代码时可以充分利用完整的 Go 语言特性。相比之下,当线程和 goroutine 被命名时形成的使用模式,可能会限制使用它们的库所能实现的功能。
这里通过一个困难场景来说明这一点:一旦为一个 goroutine 命名并围绕它构建模型,它就变得特殊化,开发者会倾向于将所有计算任务都关联到该 goroutine,而忽略了使用多个(可能共享的)goroutine 来进行处理的可能性。如果 `net/http` 包将每个请求的状态与一个 goroutine 关联起来,客户端在处理请求时将无法使用更多的 goroutine。
此外,像图形系统库那样要求所有处理都在"主线程"上执行的经验表明,这种模式在并发语言中部署时会显得笨拙且具有局限性。一个特殊线程或 goroutine 的存在,会迫使程序员扭曲程序结构,以避免因意外在错误线程上操作而导致的崩溃和其他问题。
对于那些真正需要某个特定 goroutine 的场景,该语言提供了诸如 channel(通道)等特性,可以通过灵活的方式与之进行交互。
## 函数和方法 {#Functions_methods}
### 为什么 T 和 *T 有不同的方法集? {#different_method_sets}
正如 [Go 语言规范](/ref/spec#Types) 所述,类型 `T` 的方法集包含所有接收者类型为 `T` 的方法,而对应的指针类型 `*T` 的方法集则包含所有接收者为 `*T` 或 `T` 的方法。这意味着 `*T` 的方法集包含了 `T` 的方法集,但反之则不然。
这种区分的原因在于:如果一个接口值包含指针 `*T`,方法调用可以通过解引用指针来获取值;但如果一个接口值包含值 `T`,则方法调用没有安全的方式来获取指针。(允许这样做将使方法能够修改接口内部的值,这是语言规范所不允许的。)
即使在某些情况下编译器可以获取值的地址并传递给方法,如果方法修改了该值,这些修改在调用方也会丢失。
例如,如果以下代码是有效的:```
var buf bytes.Buffer
io.Copy(buf, os.Stdin)
```它会将标准输入复制到 `buf` 的**副本**中,而不是 `buf` 本身。这几乎总不是期望的行为,因此该语言禁止了这种用法。
### 以协程运行的闭包会发生什么? {#closures_and_goroutines}
由于循环变量的工作方式,在 Go 1.22 版本之前(本节末尾有更新说明),在并发中使用闭包时可能会产生一些混淆。考虑以下程序:
<pre>
func main() {
done := make(chan bool)
values := []string{"a", "b", "c"}
for _, v := range values {
go func() {
fmt.Println(v)
done <- true
}()
}
// 等待所有协程完成后再退出
for _ = range values {
<-done
}
}
</pre>
人们可能错误地期望看到输出 `a, b, c`。但你可能看到的输出实际上是 `c, c, c`。这是因为循环的每次迭代都使用变量 `v` 的同一个实例,因此每个闭包共享这个单一的变量。当闭包运行时,它会打印执行 `fmt.Println` 时 `v` 的值,但 `v` 可能在协程启动后已被修改。为了帮助在问题发生前检测到此类问题和其他问题,请运行 [`go vet`](/cmd/go/#hdr-Run_go_tool_vet_on_packages)。
为了在闭包启动时将 `v` 的当前值绑定到每个闭包,必须修改内部循环,使其在每次迭代时创建一个新变量。一种方法是将该变量作为参数传递给闭包:
<pre>
for _, v := range values {
go func(<b>u</b> string) {
fmt.Println(<b>u</b>)
done <- true
}(<b>v</b>)
}
</pre>
在此示例中,`v` 的值作为参数传递给匿名函数。然后该值在函数内部作为变量 `u` 可访问。
更简单的方法是创建一个新变量,使用一种声明方式,虽然看起来有点奇怪但在 Go 中运行良好:
<pre>
for _, v := range values {
<b>v := v</b> // 创建一个新的 'v'。
go func() {
fmt.Println(<b>v</b>)
done <- true
}()
}
</pre>
语言中这种不为每次迭代定义新变量的行为,现在看来被认为是一个错误,并且已在 [Go 1.22](/wiki/LoopvarExperiment) 中得到解决,该版本确实为每次迭代创建一个新变量,从而消除了这个问题。
## 控制流 {#Control_flow}
### 为什么 Go 没有 `?:` 运算符? {#Does_Go_have_a_ternary_form}
Go 中没有三元测试运算符。你可以使用以下方式来达到相同的结果:```
if expr {
n = trueVal
} else {
n = falseVal
}
```Go语言不提供三元运算符`?:`的原因是,语言设计者发现该运算符常被滥用以创建极其复杂的表达式。尽管`if-else`形式代码更长,但其清晰度毋庸置疑。一门语言只需要一种条件控制流结构。
## 类型参数 {#Type_Parameters}
### 为什么Go语言引入类型参数? {#why_generics}
类型参数支持所谓的泛型编程,允许函数和数据结构基于使用时才指定的类型进行定义。例如,通过类型参数可以编写一个函数来返回任意有序类型的两个值中的最小值,而无需为每种可能类型编写独立版本。更深入的说明和示例请参阅博客文章[为什么需要泛型?](/blog/why-generics)。
### Go语言的泛型是如何实现的? {#generics_implementation}
编译器可以选择为每个实例化分别编译,或将相似的实例化编译为单一实现。单一实现方式类似于带有接口参数的函数。不同编译器会针对不同情况做出不同选择。标准Go编译器通常会为具有相同"形状"的类型参数生成单一实例化,其中"形状"由类型的大小、包含的指针位置等特性决定。未来版本可能会在编译时间、运行时效率和代码体积之间进行权衡实验。
### Go语言的泛型与其他语言有何区别? {#generics_comparison}
所有语言的基本功能相似:都能使用后续指定的类型来编写类型和函数。但仍存在一些差异。
* **Java**
在Java中,编译器在编译时检查泛型类型,但在运行时擦除类型信息,这被称为[类型擦除](https://en.wikipedia.org/wiki/Generics_in_Java#Problems_with_type_erasure)。例如,编译时名为`List<Integer>`的Java类型在运行时将变为非泛型类型`List`。这意味着在使用Java的类型反射时,无法区分`List<Integer>`和`List<Float>`类型的值。而在Go中,泛型类型的反射信息包含完整的编译时类型信息。
Java使用类型通配符(如<code>List<? extends Number></code>或<code>List<? super Number></code>)实现泛型的协变与逆变。Go语言没有这些概念,这使得Go的泛型类型更为简单。
* **C++**
传统C++模板不对类型参数施加任何约束,尽管C++20通过[concepts](https://en.wikipedia.org/wiki/Concepts_(C%2B%2B))支持可选约束。在Go中,所有类型参数都必须有约束。C++20的concepts由必须能与类型参数一起编译的小段代码表达;而Go的约束是定义所有允许类型参数集合的接口类型。
C++支持模板元编程;Go则不支持。实际上,所有C++编译器都在模板实例化处编译每个模板;如前所述,Go可以且确实对不同实例化采用不同方法。
* **Rust**
Rust中的约束称为trait bound。在Rust中,trait bound与类型之间的关联必须显式定义,无论是在定义trait bound的crate中还是在定义类型的crate中。在Go中,类型参数隐式满足约束,就像Go类型隐式实现接口类型一样。Rust标准库为比较或加法等操作定义了标准trait;而Go标准库没有这样做,因为这些可以通过用户代码中的接口类型表达。唯一的例外是Go的`comparable`预定义接口,它捕获了类型系统无法表达的特性。
* **Python**
Python不是静态类型语言,因此可以合理地认为所有Python函数默认总是泛型的:它们始终可以使用任何类型的值调用,任何类型错误都在运行时检测。
### 为什么Go语言使用方括号作为类型参数列表? {#generic_brackets}
Java和C++使用尖括号表示类型参数列表(如Java的`List<Integer>`和C++的`std::vector<int>`)。但Go无法使用尖括号,因为这会导致语法解析问题:在函数内解析代码时(如`v := F<T>`),看到`<`时无法确定这是泛型实例化还是使用`<`运算符的表达式。没有类型信息时,这个问题很难解决。
例如,考虑以下语句:
<pre>
a, b = w < x, y > (z)
</pre>
没有类型信息时,无法确定赋值右侧是一对表达式(`w < x`和`y > z`),还是一个泛型函数实例化及其返回两个结果值的调用(`(w<x, y>)(z)`)。
Go的一个关键设计决策是解析无需类型信息,而使用尖括号表示泛型时似乎无法实现这一点。
Go使用方括号并非独创;其他语言(如Scala)也使用方括号表示泛型代码。
### 为什么Go语言不支持带类型参数的方法? {#generic_methods}
Go允许泛型类型拥有方法,但除接收器外,这些方法的参数不能使用参数化类型。我们预期Go不会添加泛型方法。问题在于如何实现它们。
具体而言,考虑检查一个接口中的值是否实现了包含额外方法的另一个接口。
例如,考虑以下类型——一个空结构体,它带有一个泛型`Nop`方法,该方法可返回任意可能类型的参数:```
type Empty struct{}
func (Empty) Nop[T any](x T) T {
return x
}
```现在假设一个`Empty`值被存储在`any`中,并传递给其他代码来检查它能执行哪些操作:```
func TryNops(x any) {
if x, ok := x.(interface{ Nop(string) string }); ok {
fmt.Printf("string %s\n", x.Nop("hello"))
}
if x, ok := x.(interface{ Nop(int) int }); ok {
fmt.Printf("int %d\n", x.Nop(42))
}
if x, ok := x.(interface{ Nop(io.Reader) io.Reader }); ok {
data, err := io.ReadAll(x.Nop(strings.NewReader("hello world")))
fmt.Printf("reader %q %v\n", data, err)
}
}
```这段代码在 `x` 为 `Empty` 类型时是如何工作的?
似乎 `x` 必须同时满足所有三个测试,以及任何其他使用任何其他类型的形式。
当调用这些方法时,运行的是什么代码?
对于非泛型方法,编译器会为所有方法实现生成代码,并将它们链接到最终的程序中。
但对于泛型方法,可能会有无限多个方法实现,因此需要不同的策略。
有四种选择:
1. 在链接时,列出所有可能的动态接口检查,然后查找那些满足检查但缺少已编译方法的类型,然后重新调用编译器来添加这些方法。
这会显著减慢构建速度,因为需要在链接后停止并重复一些编译过程。尤其会减慢增量构建的速度。更糟糕的是,新编译的方法代码本身可能包含新的动态接口检查,那么这个过程就必须重复。甚至可以构造出导致这个过程永远无法结束的例子。
2. 实现某种即时编译器(JIT),在运行时编译所需的方法代码。
Go 语言受益于纯提前编译所带来的简洁性和可预测的性能。
我们不愿意仅仅为了实现一个语言特性就引入 JIT 的复杂性。
3. 为每个泛型方法安排一个慢速回退路径,该路径使用一个函数表来处理类型参数上所有可能的语言操作,然后将该回退实现用于动态测试。
这种方法会导致由意外类型参数化的泛型方法比编译时观察到的类型参数化的相同方法慢得多。
这将使性能变得更加不可预测。
4. 规定泛型方法根本不能用于满足接口。
接口是 Go 语言编程的重要组成部分。
从设计角度来看,禁止泛型方法满足接口是不可接受的。
这些选择都不理想,因此我们选择了“以上都不是”。
与其使用带类型参数的方法,不如使用带类型参数的顶级函数,或者将类型参数添加到接收器类型上。
更多详情和示例,请参见[提案](/design/43651-type-parameters#no-parameterized-methods)。
### 为什么我不能为参数化类型的接收器使用更具体的类型? {#types_in_method_declaration}
泛型类型的方法声明使用包含类型参数名称的接收器来编写。
也许是因为在调用站点指定类型的语法与此相似,
有些人认为这提供了一种机制,可以通过在接收器中指定具体类型(例如 `string`)来生成针对某些特定类型参数定制的方法:```
type S[T any] struct { f T }
func (s S[string]) Add(t string) string {
return s.f + t
}
```这段代码会失败,因为编译器将单词 `string` 视为该方法中类型参数的名称。编译器错误信息可能类似 "`operator + not defined on s.f (variable of type string)`"。这可能会令人困惑,因为 `+` 运算符在预定义的 `string` 类型上完全正常工作,但该声明覆盖了此方法中 `string` 的定义,而该运算符在这个不相关的 `string` 版本上无法使用。像这样覆盖预定义名称是合法的,但这是一个奇怪的做法,通常是一个错误。
### 为什么编译器无法推断我程序中的类型参数? {#type_inference}
存在许多情况,程序员可以轻易看出泛型类型或函数的类型参数必须是什么,但语言不允许编译器进行推断。类型推断被有意限制,以确保推断出的类型永远不会产生混淆。其他语言的经验表明,意外的类型推断在阅读和调试程序时会导致相当大的混淆。始终可以在调用中指定要使用的显式类型参数。未来可能会支持新的推断形式,只要规则保持简单和清晰。
## 包与测试 {#Packages_Testing}
### 如何创建多文件包? {#How_do_I_create_a_multifile_package}
将包的所有源文件单独放在一个目录中。源文件可以随意引用不同文件中的条目;无需前向声明或头文件。
除了被分割成多个文件外,该包的编译和测试方式与单文件包完全相同。
### 如何编写单元测试? {#How_do_I_write_a_unit_test}
在与包源文件相同的目录中,创建一个以 `_test.go` 结尾的新文件。在该文件内部,`import "testing"` 并编写如下形式的函数:
在该目录下运行 `go test`。该脚本会查找 `Test` 函数,构建测试二进制文件并运行它。
更多详情,请参阅 [How to Write Go Code](/doc/code.html) 文档、[`testing`](/pkg/testing/) 包以及 [`go test`](/cmd/go/#hdr-Test_packages) 子命令。```
func TestFoo(t *testing.T) {
...
}
```在该目录下运行 `go test`。该脚本会查找 `Test` 函数,构建测试二进制文件并运行它。
更多详情,请参阅 [如何编写 Go 代码](/doc/code.html) 文档、[`testing`](/pkg/testing/) 包以及 [`go test`](/cmd/go/#hdr-Test_packages) 子命令。
### 我喜欢的测试辅助函数在哪里?{#testing_framework}
Go 的标准 [`testing`](/pkg/testing/) 包使得编写单元测试变得简单,但它缺乏其他语言测试框架中提供的功能,例如断言函数。本文[前面的章节](#assertions)解释了为什么 Go 没有断言,同样的论点也适用于在测试中使用 `assert`。正确的错误处理意味着让一个测试失败后其他测试仍能继续运行,以便调试失败的人能全面了解问题所在。一个测试报告 `isPrime` 对于 2、3、5 和 7(或者 2、4、8 和 16)给出了错误答案,并且因此不再运行更多测试,这比仅仅报告 `isPrime` 对于 2 给出了错误答案更有用。触发测试失败的程序员可能并不熟悉失败的代码。投入时间编写良好的错误信息,会在日后测试失败时带来回报。
一个相关观点是,测试框架往往会发展出自己的“迷你语言”,带有条件判断、控制结构和打印机制,但 Go 已经具备所有这些能力;为什么要重复创建它们?我们更倾向于用 Go 编写测试;这样可以少学一门语言,并且这种方法能保持测试直接明了、易于理解。
如果为了编写良好的错误信息所需的额外代码看起来重复且繁重,那么使用表驱动测试可能会更好,即遍历在数据结构中定义的输入和输出列表(Go 对数据结构字面量有很好的支持)。这样,编写好的测试和良好错误信息的工作量将被分摊到多个测试用例中。标准 Go 库中有大量说明性示例,例如 [`fmt` 包的格式化测试](/src/fmt/fmt_test.go)。
### 为什么标准库中没有 *X*?{#x_in_std}
标准库的目的是支持运行时库、连接操作系统,并提供许多 Go 程序所需的关键功能,例如格式化 I/O 和网络编程。它还包含对网络编程至关重要的元素,包括密码学以及对 HTTP、JSON 和 XML 等标准的支持。
对于什么应该包含在内,并没有一个明确的标准,因为很长一段时间以来,这是*唯一*的 Go 库。然而,如今确实存在一些决定新增内容的标准。
新增到标准库的内容很少,并且门槛很高。包含在标准库中的代码承担着巨大的持续维护成本(通常由原作者之外的人承担),受制于 [Go 1 兼容性承诺](/doc/go1compat.html)(这会阻止对 API 任何缺陷的修复),并且受制于 Go 的[发布计划](/s/releasesched),这使得错误修复无法快速提供给用户。
大多数新代码应该存在于标准库之外,并通过 [`go` 工具](/cmd/go/)的 `go get` 命令访问。这样的代码可以有自己的维护者、发布周期和兼容性保证。用户可以在 [pkg.go.dev](https://pkg.go.dev/) 上找到包并阅读其文档。
尽管标准库中有些部分实际上并不属于这里,例如 `log/syslog`,但由于 Go 1 兼容性承诺,我们继续维护库中的所有内容。但我们鼓励大多数新代码存放在其他地方。
## 实现 {#Implementation}
### 用于构建编译器的技术是什么?{#What_compiler_technology_is_used_to_build_the_compilers}
Go 有多个生产级编译器,还有许多其他正在为各种平台开发的编译器。
默认的编译器 `gc` 包含在 Go 发行版中,作为对 `go` 命令支持的一部分。`Gc` 最初是用 C 编写的,这是因为引导启动的困难——你需要一个 Go 编译器来设置 Go 环境。但情况已经改善,自 Go 1.5 发布以来,编译器已经是一个 Go 程序。该编译器使用自动翻译工具从 C 转换为 Go,如这份[设计文档](/s/go13compiler)和[演讲](/talks/2015/gogo.slide#1)所述。因此,编译器现在是“自托管”的,这意味着我们需要面对引导启动问题。解决方案是预先有一个可用的 Go 安装,就像通常需要一个可用的 C 安装一样。如何从源代码构建新的 Go 环境的故事在[这里](/s/go15bootstrap)和[这里](/doc/install/source)有描述。
`Gc` 是用 Go 编写的,使用了递归下降解析器,并使用了一个自定义的加载器(也用 Go 编写,但基于 Plan 9 加载器)来生成 ELF/Mach-O/PE 二进制文件。
`Gccgo` 编译器是一个用 C++ 编写的前端,带有递归下降解析器,并与标准的 GCC 后端耦合。一个实验性的 [LLVM 后端](https://go.googlesource.com/gollvm/) 使用相同的前端。
项目初期,我们考虑过将 LLVM 用于 `gc`,但认为它太大且速度太慢,无法满足我们的性能目标。回顾来看,更重要的是,从 LLVM 开始会使得引入一些 Go 所需的 ABI 和相关变更(例如栈管理)变得更加困难,而这些变更并非标准 C 设置的一部分。
事实证明,Go 是一种实现 Go 编译器的优秀语言,尽管这并非其最初的目标。从一开始就不自托管,使得 Go 的设计能够专注于其最初的用例——联网服务器。如果我们早期就决定让 Go 自身编译自己,我们可能会最终得到一种更侧重于编译器构造的语言,这是一个有价值的目标,但并非我们最初的目标。尽管 `gc` 有其自身的实现,但 [`go/parser`](/pkg/go/parser/) 包中提供了原生的词法分析器和解析器,并且还提供了一个原生的[类型检查器](/pkg/go/types)。`gc` 编译器使用了这些库的变体。
### 运行时支持是如何实现的? {#How_is_the_run_time_support_implemented}
同样是由于引导问题,运行时代码最初主要用 C 编写(附带少量汇编),但此后已被翻译成 Go(除了一些汇编部分)。`Gccgo` 的运行时支持使用 `glibc`。`gccgo` 编译器使用一种称为分段栈的技术来实现 goroutine,这得益于近期对 gold 链接器的修改。`Gollvm` 同样构建在相应的 LLVM 基础设施之上。
### 为什么我的简单程序是如此大的二进制文件? {#Why_is_my_trivial_program_such_a_large_binary}
`gc` 工具链中的链接器默认创建静态链接的二进制文件。因此,所有 Go 二进制文件都包含 Go 运行时,以及支持动态类型检查、反射甚至运行时恐慌(panic)堆栈跟踪所必需的运行时类型信息。
一个简单的 C 语言 "hello, world" 程序在 Linux 上使用 gcc 编译并静态链接后,大小约为 750 kB,这包含了 `printf` 的实现。一个使用 `fmt.Printf` 的等效 Go 程序大小约为几兆字节,但它包含了更强大的运行时支持以及类型和调试信息。
使用 `gc` 编译的 Go 程序可以通过 `-ldflags=-w` 标志禁用 DWARF 调试信息的生成,从而从二进制文件中移除调试信息,但不会导致其他功能损失。这可以显著减小二进制文件的大小。
### 我能让这些关于未使用变量/导入的报错消失吗? {#unused_variables_and_imports}
存在未使用的变量可能表明代码中存在 bug,而未使用的导入只会减慢编译速度,随着程序代码和程序员数量的增加,这种影响会变得显著。基于这些原因,Go 编译器拒绝编译含有未使用变量或导入的程序,以此换取长期的编译速度提升和程序清晰度,牺牲短期的便利性。
尽管如此,在开发代码时,临时出现这些情况很常见,而在程序能够编译前不得不先编辑掉它们可能会令人烦恼。
有人曾要求提供一个编译器选项来关闭这些检查,或者至少将它们降低为警告。然而,这样的选项并未被添加,因为编译器选项不应影响语言的语义,并且 Go 编译器不报告警告,只报告阻止编译的错误。
不使用警告有两个原因。首先,如果值得抱怨,那就值得在代码中修复。(反过来说,如果不值得修复,那就不值得提及。)其次,让编译器生成警告会鼓励实现对一些不那么重要的情况进行警告,这可能使编译输出变得嘈杂,掩盖了那些**应该**被修复的真正错误。
不过,解决这个问题很容易。在开发时,使用空白标识符让未使用的东西保留下来即可。```go
import "unused"
// 此声明通过引用包中的项来标记该导入已被使用。
var _ = unused.Item // TODO: Delete before committing!
func main() {
debugData := debug.Profile()
_ = debugData // 仅在调试期间使用。
....
}
```如今,大多数Go程序员会使用
[goimports](https://godoc.org/golang.org/x/tools/cmd/goimports)
这一工具,它能自动重写Go源文件以包含正确的导入,从而在实际使用中消除未使用导入的问题。
该程序可以轻松连接到大多数编辑器和IDE,在编写Go源文件时自动运行。
此功能也已内置到`gopls`中,正如
[上文所述](/doc/faq#ide)。
### 为什么我的病毒扫描软件认为Go发行版或编译后的二进制文件被感染了? {#virus}
这种情况很常见,尤其是在Windows机器上,并且几乎总是误报。
商业病毒扫描程序经常被Go二进制文件的结构所迷惑,因为它们不像其他语言编译的文件那样常见。
如果你刚刚安装了Go发行版,而系统报告它被感染了,那肯定是个错误。
为了彻底确认,你可以通过将校验和与
[下载页面](/dl/)上的校验和进行比对来验证下载的文件。
无论如何,如果你认为报告有误,请向病毒扫描软件的供应商提交错误报告。
也许有朝一日,病毒扫描程序能学会理解Go程序。
## 性能 {#Performance}
### 为什么Go在基准测试X上表现不佳? {#Why_does_Go_perform_badly_on_benchmark_x}
Go的设计目标之一是使可比较程序的性能接近C语言,然而在某些基准测试上它的表现相当糟糕,包括[golang.org/x/exp/shootout](https://go.googlesource.com/exp/+/master/shootout/)中的几个。
最慢的测试依赖于Go中尚未提供具有可比性能版本的库。
例如,[pidigits.go](https://go.googlesource.com/exp/+/master/shootout/pidigits.go)
依赖于一个高精度数学包,而C语言版本(与Go不同)使用了[GMP](https://gmplib.org/)(它是用优化的汇编器编写的)。
依赖于正则表达式的基准测试(例如
[regex-dna.go](https://go.googlesource.com/exp/+/master/shootout/regex-dna.go))
本质上是在将Go原生的[regexp包](/pkg/regexp)与像PCRE这样成熟、高度优化的正则表达式库进行比较。
基准测试游戏的胜利依赖于广泛的调优,而大多数基准测试的Go版本需要更多关注。
如果你测量真正可比的C和Go程序(
[reverse-complement.go](https://go.googlesource.com/exp/+/master/shootout/reverse-complement.go)
就是一个例子),你会发现两种语言在原始性能上比这套测试所显示的要接近得多。
尽管如此,仍有改进的空间。编译器已经不错,但可以更好,许多库需要重大的性能改进,而且垃圾回收器还不够快。(即使它够快,注意避免产生不必要的垃圾也能带来巨大影响。)
无论如何,Go通常可以非常具有竞争力。
随着语言和工具的发展,许多程序的性能已经有了显著提升。
参见关于[Go程序性能分析](/blog/profiling-go-programs)的博客文章,其中有一个信息丰富的例子。虽然它相当古老,但仍然包含有用的信息。
## 与C的差异 {#change_from_c}
### 为什么语法与C如此不同? {#different_syntax}
除了声明语法外,差异并不大,主要源于两个愿望。首先,语法应该感觉轻便,没有太多强制性的关键字、重复或晦涩之处。其次,该语言被设计为易于分析,并且可以在不使用符号表的情况下进行解析。这使得构建调试器、依赖分析器、自动文档提取器、IDE插件等工具变得更加容易。C及其派生语言在这方面是出了名的困难。
### 为什么声明是反向的? {#declarations_backwards}
只有在你习惯了C语言的情况下,它们才是反向的。在C中,概念是变量的声明类似于一个表示其类型的表达式,这是一个很好的想法,但类型和表达式语法混合得不太好,结果可能令人困惑;考虑函数指针。Go大多将表达式和类型语法分开,这简化了事情(对指针使用前缀`*`是证明规则的一个例外)。在C中,声明```
int* a, b;
```声明了`a`是指针,但`b`不是;而在Go语言中,```
var a, b *int
```两者均被声明为指针。这种方式更清晰、更规则。此外,`:=`短声明形式暗示,完整的变量声明应与`:=`保持相同的顺序,即```
var a uint64 = 1
```具有相同的效果```
a := uint64(1)
```解析也因拥有独立的类型语法而得到简化,这种语法不仅仅是表达式语法;像`func`和`chan`这样的关键字使语义更加清晰。
更多细节请参见关于[Go声明语法](/doc/articles/gos_declaration_syntax.html)的文章。
### 为什么没有指针算术? {#no_pointer_arithmetic}
出于安全性考虑。如果没有指针算术,就有可能创建一种永远不会错误地派生出非法地址的语言。编译器和硬件技术已经发展到这样的程度:使用数组索引的循环可以和使用指针算术的循环一样高效。此外,缺少指针算术可以简化垃圾回收器的实现。
### 为什么`++`和`--`是语句而不是表达式?为什么使用后缀而不是前缀? {#inc_dec}
如果没有指针算术,前缀和后缀自增运算符的便利性价值就会降低。通过将它们完全移出表达式层次结构,表达式语法得以简化,同时消除了围绕`++`和`--`求值顺序的混乱问题(例如考虑`f(i++)`和`p[i] = q[++i]`)。这种简化意义重大。至于后缀与前缀的选择,两者都能很好地工作,但后缀版本更为传统;坚持使用前缀源于STL,这是一个讽刺地包含后缀自增的语言库。
### 为什么有大括号但没有分号?为什么不能把开始大括号放在下一行? {#semicolons}
Go使用花括号进行语句分组,这是任何使用过C家族语言的程序员都熟悉的语法。然而,分号是为解析器准备的,而不是为人类准备的,我们希望尽可能消除它们。为实现这一目标,Go借鉴了BCPL的一个技巧:在形式语法中,分隔语句的分号是存在的,但它们由词法分析器自动插入,无需向前看,在任何可能作为语句结束的行尾插入。这在实践中效果很好,但副作用是它强制了一种大括号风格。例如,函数的开始大括号不能单独出现在一行上。
有人认为词法分析器应该进行前向查看,以允许大括号出现在下一行。我们不同意。既然Go代码旨在由[`gofmt`](/cmd/gofmt/)自动格式化,就必须选择*某种*风格。这种风格可能与你在C或Java中使用的不同,但Go是一门不同的语言,`gofmt`的风格与其他任何风格一样好。更重要的是——远远更重要——为所有Go程序提供单一的、通过程序强制执行的格式所带来的优势,远远超过特定风格的任何感知劣势。还要注意,Go的风格意味着Go的交互式实现可以一次使用一行标准语法,而无需特殊规则。
### 为什么要进行垃圾回收?代价会不会太高? {#garbage_collection}
系统程序中最主要的簿记开销之一是管理分配对象的生命周期。在像C这样需要手动管理的语言中,这会消耗程序员大量的时间,并且常常是导致严重bug的根源。即使在像C++或Rust这样提供辅助机制的语言中,这些机制也会对软件设计产生重大影响,往往增加额外的编程开销。我们认为消除这种程序员开销至关重要,而过去几年垃圾回收技术的进步使我们有信心,它可以以足够低的成本和足够低的延迟来实现,从而成为网络化系统的可行方案。
并发编程的大部分困难源于对象生命周期问题:当对象在线程之间传递时,保证它们安全释放变得非常繁琐。自动垃圾回收使得编写并发代码变得容易得多。当然,在并发环境中实现垃圾回收本身就是一个挑战,但一次性解决它,而不是在每个程序中都解决,对所有人都有帮助。
最后,即使不考虑并发,垃圾回收也使接口更简单,因为它们不需要指定内存如何跨接口管理。
这并不是说最近在Rust等语言中,为解决资源管理问题带来新思路的工作是误导;我们鼓励这些工作,并很高兴看到它的发展。但Go采取了更传统的方法,即通过垃圾回收——并且仅通过垃圾回收——来解决对象生命周期问题。
当前的实现是一个标记-清除收集器。如果机器是多处理器的,收集器将在单独的CPU核心上与主程序并行运行。近年来对收集器的主要改进已将暂停时间通常降低到亚毫秒级,即使是对于大型堆也是如此,这几乎消除了在网络服务器中对垃圾回收的主要反对意见之一。改进算法、进一步降低开销和延迟、探索新方法的工作仍在继续。Go团队的Rick Hudson在2018年[ISMM主旨演讲](/blog/ismmkeynote)中描述了迄今为止的进展,并提出了一些未来的方法。
在性能方面,请记住,Go给予程序员对内存布局和分配的相当大的控制权,这比典型的垃圾回收语言要多得多。一个细心的程序员可以通过合理使用语言来显著降低垃圾回收开销;请参阅关于[分析Go程序](/blog/profiling-go-programs)的文章,其中包含一个实际案例,包括对Go分析工具的演示。