Go 官方翻译:Go 常见问题(Go FAQ)
来源
项目的起源
就在十年前,Go 刚成立的时候,编程世界与今天不同。生产软件通常是用 C++ 或 Java 编写的,GitHub 还不存在,大多数计算机还没有多处理器,除了 Visual Studio 和 Eclipse 之外,几乎没有 IDE 或其他高级工具可用,更别说在互联网上免费使用了。
与此同时,我们对用语言来开发服务软件的复杂程度感到沮丧。自从 C、C++ 和 Java 等语言被开发出来后,计算机的运行速度大大提高了,但编程本身却没有那么发达。而且,尽管多处理器系统已经普遍,对语言的高效性和安全性却几乎没有帮助。
我们决定退一步思考,随着技术的发展,在未来的几年里软件工程将会面临哪些主要问题,以及一种新的语言将如何帮助解决这些问题。例如,多核 CPU 的兴起表明,一种语言应该为某种并发性或并行性提供一流的支持。为了使资源管理在大型并发程序中易于处理,需要垃圾收集,或者至少需要某种安全的、自动的内存管理。
这些考虑引发了一系列的讨论 ,从这些讨论开始,首先作为一套想法和愿望,然后作为一种语言。一个主要的目标是,通过启用工具、自动化一些常见的任务 ( 如代码格式化 ) 和消除在大型代码库中工作的障碍,更多地帮助正在工作的程序员。
有关 Go 的目标以及如何实现这些目标 ( 至少是如何实现这些目标 ) ,更广泛的描述可以在这篇文章中可以找到。
项目历史
2007 年 9 月 21 日,Robert Griesemer、Rob Pike和Ken Thompson开始在白板上勾画新语言的目标。几天之内,目标就形成了,既一个做某事的计划和一个公平的想法。计划在非全职时间继续进行,同时进行一些其他工作。到 2008 年 1 月,Ken 已经开始研究编译器,用它来探索思想;它用C
代码作为输出。到年中,该语言已经成了一个全职项目,并且稳定下来,可以尝试一个产品编译器。2008 年 5 月,Ian Taylor 独立地开始在 GCC 前端使用草案规范。Russ Cox 于 2008 年底加入,帮助将语言和库从原型推向现实。
Go
于 2009 年 11 月 10 日成为一个公共开源项目。社区中有无数人提供了想法、讨论和代码。
现在全世界有数百万的Go
程序员——gopherg
,而且每天都在增加。Go
的成功远远超出了我们的预期。
地鼠吉祥物的起源是什么?
吉祥物和徽标是由 Renée French设计的,他还设计了 Glenda,Plan 9 兔子。一篇关于gopher 的 博客文章 解释了它是如何从几年前她用于 WFMU T 恤设计中衍生出来的。徽标和吉祥物以Creative Commons Attribution 3.0 许可发行。
Gopher 有一份 设计指南 ,说明了他的特征以及如何正确表示它们。
Go 还是 Golang?
语言准确的名称为 Go。Golang 这个名字的出现是因为网站 golang.org,而不是 go.org(我们无法使用此域名)。不过,很多人把 Golang 作为一个表情,例如,该语言的 Twitter 话题标签是 #golang
。不管怎样,这门语言的名字只是普通的 Go。
另一个注意事项:尽管 官方标志 有两个大写字母,但语言名称是 Go,而不是 GO(注意大小写)。
你们为什么要创造新的语言?
Go
在既有语言与环境下进行系统编程的挫折中诞生。编程变得太难,对语言的选择有一定的责任。 我们必须在高效编译、高效执行或轻松编程之间选择其一,在同样主流的语言中,三者不能同时达到。 程序员们通过转移到Python
和JavaScript
之类的动态类型语言,而非C++
或一定程度上的Java
上, 来选择轻松在安全和效率之上。
Go
试图成为结合解释型编程的轻松、动态类型语言的高效以及静态类型语言的安全的编译型语言。 它也打算成为现代的,支持网络与多核计算的语言。要满足这些目标,需要解决一些语言上的问题: 一个富有表达能力但轻量级的类型系统,并发与垃圾回收机制,严格的依赖规范等等。 这些无法通过库或工具解决好,必须创造新的语言。
文章 Go 在 Google 中讨论了Go
语言设计的其背景和动机,关于本 FAQ 中的许多为题,该文章提供了更多详情。
Go的前身是什么?
Go
主要是C
家族的(基本语法),从 Pascal/Modula/Oberon 家族引入了重要的东西(声明,包), 加上一些由 Tony Hoare 的 CSP 所激发的语言的理念,例如 Newsqueak 与 Limbo(并发)。然而, 它是一个全新的语言。在各个方面上,该语言的设计都考虑到程序员做的事情以及如何去编程, 至少是我们进行的那种编程。更实际,也就意味着更有趣。
设计的指导原则是什么?
如今的编程包含了太多记账式的、重复的、文书式的工作。就像 Dick Gabriel 说的那样: “老程序读起来就像健谈的研究工作者与善于学习的书呆子同事之间平和的对话, 而不像同编译器之间的争辩。谁会认为成熟必然带来杂乱?”这样的成熟是值得的—— 没有人想要回到老的语言——但它能更安静地被实现么?
Go
试图在两种意义上减少文字的键入次数。贯穿其设计,我们试图减少混乱与复杂性。它没有前置声明与头文件;任何东西都只声明一次。初始化富有表现力,自动且易于使用。 语法的关键字清晰而轻量。啰嗦的表达式(foo.Foo* myFoo = new(foo.Foo)
) 可使用 :=
声明并初始化结构,通过简单的类型推断来简化。 也许最根本的是,这里没有类型层级:类型就是类型,无需说明它们之间的关系。 这些简化允许Go
无需牺牲成熟而富有表现力且易于理解。
另一个重要的原则是保持概念正交。方法可被任何类型实现,结构代表数据而接口代表抽象等等。 正交性使一些东西相结合时发生的事情更易理解。
使用
Google 是否在内部使用 Go ?
是的。现在有几个Go
程序正部署在Google
内部产品中。一个例子就是在 golang.org 后台支持的服务。它仅仅是在Google
应用引擎上配置的产品中运行的文档服务。
还有个例子就是Google
的下载服务器 dl.google.com
,它用于释放Chrome
二进制文件和其它类似 apt-get
的大型可安装包。
Go
并不是谷歌使用的唯一语言,但它是许多领域的关键语言,包括 site reliability engineering (SRE) 和大规模数据处理。
还有哪些公司在用 Go ?
Go
在全球范围内的使用量都在增长,尤其是在云计算领域。用Go
编写的几个主要的云基础设施项目有Docker
和Kubernetes
,还有更多。
它不只是云。Go Wiki
有一个页面,定期更新,其中列出了一些使用Go
的公司。
Wiki
还有一个页面,其中有关于使用该语言的公司和项目的 success stories 的链接。
Go程序能否与C/C++程序链接?
可以在相同的地址空间中一起使用C和Go,但这并不是很自然的做法,可能需要特殊的接口软件。同样,将C与Go代码链接会放弃Go提供的内存安全性和堆栈管理属性。有时候,绝对有必要使用C库来解决问题,但是这样做总是会引入纯Go代码所没有的风险元素,因此请务必谨慎。
如果确实需要将C与Go一起使用,如何进行取决于Go编译器的实现。Go团队支持了三种Go编译器实现。它们是gc
-默认编译器,gccgo
-使用GCC后端的gccgo,和gollvm
-使用LLVM基础架构的不那么成熟的gollvm。
Gc
使用与C不同的调用约定和链接器,因此不能直接从C程序调用,反之亦然。cgo
程序提供了“外部函数接口”的机制,以允许在Go代码中安全地调用C库。SWIG将此功能扩展到了C++库。
您也可以将cgo
和SWIG与Gccgo
和gollvm
一起使用。由于它们使用传统的API,因此将这些编译器的代码直接与GCC/LLVM编译的C或C++程序链接也是可能的,在非常小心的情况下。但是,要安全的执行此操作,需要了解所有相关语言的调用约定,以及从Go调用C或C++时需要注意的堆栈限制。
You can also use cgo
and SWIG
Go支持哪些IDE?
Go项目不包括一个自定义的IDE,但是已经设计语言和库来简化源代码分析。结果就是,大多数知名的编辑器和IDE都直接或通过插件良好地支持Go。
具有良好的Go支持的知名IDE和编辑器列表包括Emacs,Vim,VSCode,Atom,Eclipse,Sublime,IntelliJ(通过自定义变体Goland)等。您最喜欢的环境很可能就是您在Go中进行编程的高效环境。
Go 支持谷歌的 Protocol Buffers 吗?
有一个单独的开源项目来提供编译插件和库支持。 在 github.com/golang/protobuf/ 上可以找到。
我能把 Go 的主页翻译成其他语言吗?
当然可以。我们鼓励开发者使用他们的母语重做 Go 语言主页。然而,如果你打算在网站中添加谷歌的 Logo 或商标( golang.org 上面并没有), 你必须遵守 www.google.com/permissions/guidelines.html 上的用户手册。
设计
Go 有运行时吗?
Go 确实有一个扩展库叫做 runtime (运行时),每一个 Go 程序都会使用它。运行时库实现了垃圾回收,并发,栈管理等重要的语言特性。尽管它对于 Go 语言很重要,但是它更类似于 C 语言的 libc
库。
值得注意的是,Go 的运行时不包括 JVM 那样的虚拟机。Go 代码会被预先编译成原生的机器码(某些特别的编译器也可以把它编译为 JavaScript 和 WebAssembly )。因此,尽管「运行时」这个词通常指程序运行的虚拟环境,在 Go 语言中它指的只是一个支持语言重要特性的库。
是否支持 Unicode 标识符?
在设计 Go 时,我们希望确保它不要过于以ASCII为中心,这意味着将标识符空间从7位ASCII的界限中扩展出来。Go 现在的规则——标识符字符必须是由Unicode定义的字母或数字——易于理解和实现,但有一些限制。例如,组合字符被设计排除在外,这也排除了一些语言,如梵文。
这条规则还有一个不幸的后果。由于导出的标识符必须以大写字母开头,根据定义,由某些语言中的字符创建的标识符不能导出。目前唯一的解决办法是使用像X日本語
这样的东西,这显然不能令人满意。
自该语言的最早版本以来,人们就一直在思考如何最好地扩展标识符空间,以适应使用其他母语的程序员。具体该怎么做仍然是一个活跃的讨论话题,未来版本的语言在标识符的定义上可能会更加自由。例如,它可能采纳统一编码(Unicode)组织建议 中关于标识符的一些想法。无论发生什么,都必须兼容地完成,同时保持(或者扩展)字母大小写决定标识符可见性的方式,这仍然是我们最喜欢的Go特性之一。
目前,我们有一个简单的规则,可以在不破坏程序的情况下进行扩展,这个规则可以避免错误,这些错误肯定是由允许不明确标识符的规则引起的。
为什么 Go 没有特性 X?
每种语言都包含新颖的特征,省略了某人最喜欢的特征。Go 的设计着眼于编程的正确性、编译的速度、概念的正交性,以及支持并发性和垃圾收集等特性的需求。您最喜欢的特性可能会丢失,因为它不适合,因为它影响编译速度或设计的清晰度,或者因为它会使基本的系统模型变得太困难。
如果Go缺少功能X,您会感到困扰,请原谅我们,并调查Go的功能。你可能会发现它们以有趣的方式弥补了X的不足。
为什么Go不支持泛型?
我们可能会在未来的某些时候支持泛型。到现在不觉得它们有紧迫感,尽管我们理解一些程序员会觉得急需此功能。
Go是一种编写服务器程序的语言,随着时间的推移这些程序将保持一定的可维护性。
更多背景信息请参阅本文
设计集中于可伸缩性,可读性和并发性。当时,多态编程对于该语言的目标似乎并不重要,因此为简单起见而被省略。
该语言现在变得更成熟了,可以考虑某种形式的通用编程。但是,仍然存在一些警告。
泛型很方便,但是它们会增加类型系统和运行时的复杂性。尽管我们一直在考虑它,但我们尚未找到一种能使价值与复杂性成比例的设计。同时,Go的内置映射和切片,加上使用空接口构造容器的能力(通过显式取消装箱),意味着在许多情况下可以编写代码来实现泛型所能实现的功能(如果不太流畅)。
该主题保持开放。要查看以前为Go设计良好的泛型解决方案的失败尝试,请参阅此建议 。
为什么 Go 没有异常?
我们认为,将异常耦合到控制结构,就像 try-catch-final
中的那样,会产生复杂的代码。它还倾向于鼓励程序员将太多的普通错误(如未能打开文件)标记为异常。
Go 采用不同的方法。对于普通的错误处理,Go 的多值返回使报告错误变得容易。Go 一种规范的错误类型,与 Go 的其他功能相结合,使错误处理令人愉快,但与其他语言中的错误处理有很大不同。
Go 还具有几个内置函数,用于发出信号并从真正的异常条件中恢复。恢复机制仅作为出错后被拆除的函数状态的一部分来执行,这足以处理报错,但不需要额外的控制结构,并且如果使用得当,可以产生干净的错误处理代码。
有关详细信息,请参阅 Defer, Panic, and Recover 。同样 把错误当成值 描述了一种在 Go 中干净地处理错误的方法,它演示了由于错误只是值,Go 的全部功能都得以依赖在错误处理中。
为什么 Go 没有断言?
它们无可否认是方便的,但我们的经验是程序员将它们用作拐杖,以避免考虑适当的错误处理和报告。正确的错误处理意味着服务器在发生非致命错误后继续运行,而不是崩溃。正确的错误报告意味着直击要害,避免程序员翻阅大量的程序运行错误堆栈。当程序员不熟悉代码时,精确的错误尤其重要。
我们理解这是一个争论点。Go 语言和库中有许多不同于现代实践的东西,只是因为我们觉得有时值得尝试不同的方法。
为什么在CSP的思想上建立并发性?
并发和多线程编程开发非常困难,现在已经是众所周知的事情。我们认为一部分原因是设计太过复杂,例如pthreads 另一部分是太过强调简单的细节,例如互斥锁,条件变量和内存屏障。 更高级别的接口支持更简单的代码,即使仍然存在互斥体等
Hoare的通信顺序过程(CSP)是为并发提供高级语言支持的最成功模型之一。 Occam和Erlang是源自CSP的两种众所周知的语言。 Go的并发原语来自家族树的不同部分,其主要贡献是将通道作为一流对象的强大概念。几种早期语言的经验表明,CSP模型非常适合过程语言框架。
为什么用 goroutines 代替线程?
Goroutines 是使并发编程变得简单的一部分。这个想法已经存在了一段时间,它是将独立执行的功能(协程)复用到一组线程上当协程被阻止时(例如通过调用阻止系统调用),运行时会自动将同一操作系统线程上的其他协程移动到另一个可运行的线程中,这样它们就不会被阻止。 程序员看不到这些,这就是重点。 结果是,goroutines 的运行变得非常廉价:它们在堆栈内存(只有几千字节)之外的开销很小。
为了缩小堆栈,Go在运行时使用可调整大小的有界堆栈。每次给一个新的 goroutine 几千字节,这就足够了 。否则,运行时会增加(或缩小)用于自动存储堆栈的内存,从而使许多 goroutine 可以驻留在适度的内存中。每个函数调用的 CPU 开销平均约为 3 条简单指令。这使得在同一个地址空间中创建数十万个goroutine 是很实际的。如果 goroutine 只是线程,那么使用少量的 goroutine 就会耗尽系统资源。
为什么未将 映射(map) 操作定义为原子操作?
经过长时间的讨论,人们决定使用 maps 的典型用法不需要从多个 goroutine 安全访问,并且在需要的情况下,map可能是某些已经同步的较大数据结构或计算的一部分。因此,要求所有 map 操作都获取互斥量将减慢大多数程序的速度,并增加少数程序的安全性。但是,这并不是一个容易的决定,因为这意味着不受控制的 map 访问可能会使程序崩溃。
该语言不排除原子 map 更新。在需要时,例如在托管不受信任的程序时,该实现可以互锁 map 访问。
仅在进行更新时,map 访问才是不安全的。只要所有 goroutine 仅读取-查找 map中的元素(包括使用for range
循环对其进行遍历)并且不通过分配元素或进行删除来更改 map,它们就可以安全访问并发 map,无需同步。
为了更正 map 的使用,该语言的某些实现包含特殊检查,当通过并发执行不安全地修改 map 时,该检查会在运行时自动报告。
你接受我的语言更改吗?
人们经常建议对语言进行改进-邮件列表包含此类讨论的丰富历史-但很少接受这些更改。
尽管Go是一个开源项目,但语言和库受compatibility promise 的保护,该更改至少在源代码级别可防止破坏现有程序的更改(有时可能需要重新编译程序以保持最新状态)。如果您的提议违反了 Go 1 规范,则无论其优点如何,我们都无法接受。 Go 的未来主要发行版可能与 Go 1 不兼容,但是关于该主题的讨论才刚刚开始,并且可以肯定的是:在此过程中几乎不会引入这种不兼容性。此外,兼容性承诺鼓励我们为出现这种情况的旧程序提供自动前进的路径。
即使您的建议与Go 1规范兼容,也可能不符合Go的设计目标。文章 *转到Google:软件工程服务中的语言设计* 解释了Go的起源以及其设计背后的动机。
数据类型
Go 是一种面向对象的编程语言吗?
是也不是。尽管 Go 有类型和方法,并且允许面向对象风格的编程,但没有类型层次结构。Go 中的『接口』概念提供了一种不同的实现方式,我们认为它易于使用,并且在某些方面更通用。还有一些方法可以将类型嵌入到其他类型中,以提供类似于(但不完全相同)子类化的东西。此外,Go 中的方法比 C++ 或 Java 中的方法更通用:它们可以为任何类型的数据定义,甚至可以定义内置类型,如普通的整数。它们不限于 structs (类)。
同时,缺少类型层次结构使 Go 中的『对象』感觉上比 C++ 或 Java 等语言中的『对象』轻很多。
如何获得方法的动态分派?
动态分派方法的唯一方式是通过接口(interface)。结构或其他具体类型上的方法始终是静态的。
为什么没有类型继承?
面向对象编程,至少在最知名的语言中,涉及了太多关于和类型之间关系的讨论,而这些关系通常可以自动导出。Go 采用了不同的方法去处理。
在Go中,一种类型会自动满足指定其方法子集的任何接口,无需要求程序员提前声明两种类型相关联。除了减少簿记(bookkeeping)之外,这种方法还具有其他真正的优势。类型可以一次满足许多接口,而没有传统的多重继承的复杂性。 接口可以是非常轻量级的-具有一个或甚至零个方法的接口就可以表达一个有用的概念。如果有新想法出现或要进行测试,则可以在事之后添加接口,而无需注释原始类型。因为类型和接口之间没有明确的关系,所以没有类型层次结构可以管理或讨论。
I可以使用这些想法来构造类似于类型安全的Unix管道的东西。例如, fmt.Fprintf
为什么可以格式化任何类型输出,不仅仅是文件,或者 bufio
包如何和文件 I/O 完全分离,或者 image
包如何生成压缩的图像文件。 所有这些思路都有基于一个接口 (io.Writer
) 代表 一个方法(Write
)。而这仅仅是表面。 Go 的接口对程序的结构产生了深远的影响。
这需要一些时间来习惯,但是这种隐式类型依赖类型是Go最具生产力的事情之一。
为什么len是函数而不是方法?
我们讨论了这个问题,但是决定以函数实现 len
,因为函数在实践中更友好,并且没有使关于基本类型的接口(从Go类型意义上来说)的问题复杂化。
为什么Go不支持方法和运算符的重载?
如果方法调度不需要进行类型匹配,则简化该方法调度。其他语言的经验告诉我们,拥有同名但不同签名的各种方法有时很有用,但在实践中也可能令人困惑和脆弱。在 Go 的类型系统中,仅按名称匹配和需要类型一致性是一项重大简化决策。
关于操作过载,它似乎比绝对要求更为方便。同样,没有它,事情就简单多了。
为什么Go没有“实现”声明?
Go类型通过实现该接口的方法来满足该接口,仅此而已。此属性允许定义和使用接口,而无需修改现有代码。它启用了一种结构化类型,该结构促进了关注点的分离并改善了代码的重用性,并使得在代码开发时出现的模式上更容易构建。接口的语义是Go敏捷,轻便的感觉的主要原因之一。
有关更多详细信息,请参见关于类型继承的问题。
如何才能确保自定义类型实现了接口?
你可以尝试像下面这样用 T
的零值或 T
的指针来进行赋值,编译器将为你检查类型 T
是否实现了 接口I
。
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 接口?
思考这个表示可以同另一个值比较的对象的简单接口:
type Equaler interface {
Equal(Equaler) bool
}
和这种类型, T
:
type T int
func (t T) Equal(u T) bool { return t == u } // 不满足 Equal
与某些多态类型系统中的类似情况不同, T
不实现 Equaler
。 T.Equal
的参数类型是 T
,而不是字面意义上必需的 Equaler 类型
。
在 Go 中,类型系统不会提升 Equal
的参数;这是程序员的责任,如 T2
类型所示,它确实实现了 Equaler
:
type T2 int
func (t T2) Equal(u Equaler) bool { return t == u.(T2) } // 满足 Equaler
不过,即使这与其他类型系统都不一样, 因为在 Go any 类型中,可以将满足 Equaler
的类型作为 T2.Equal
的参数传递,并且在运行时我们必须检查该参数是否为类型 T2
。一些语言安排在编译时做出保证。
一个相关的例子是相反的:
type Opener interface {
Open() Reader
}
func (t T3) Open() *os.File
I在Go中,T3
不满足 Opener
,尽管它可能是另一种语言。
在这种情况下,Go的类型系统确实对程序员没有多大作用,但是缺少子类型化使得关于接口满意度的规则非常容易陈述:函数的名称和签名是否与接口的名称和签名完全相同?Go的规则也易于有效实施。 我们认为这些好处弥补了自动类型提升的不足。如果 Go 有一天可以采用某种形式的多态类型输入,我们希望将有一种表达这些示例思想的方法,并且可以对它们进行静态检查。
我是否可以将 []T 转换为 []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 有相同的基础类型?
此代码示例的最后一行不会被编译。
type T1 int
type T2 int
var t1 T1
var x = T2(t1) // OK
var st1 []T1
var sx = ([]T2)(st1) // NOT OK
在Go中,类型与方法紧密相关,因为每个命名类型都有一个(可能为空)方法集。 T通用规则是 你可以改变要转换类型的名称 (这可能改变它的方法集) ,单改变复核类型元素的名称 (和方法集) 。 Go 需要你明确说明类型转换。
为什么 nil 错误值不等于 nil?
在底层, interface
是由两种元素组成, 类型T
和值V
. V
是一个像int
, struct
或者指针类型的具体值,不是接口类型本身,并且有一个类型T
. 例如, 如果我们赋值一个int
类型的值 3 到一个 interface, 那么这个 interface 值相应地有T=int
V3
组成. 值V
是在 interface 中是动态的, 在程序执行过程中,一个指定的 interface 类型可能有不同值V
和相应的类型T
一个接口类型的值为nil
当且仅当值V
和T
都未设置的时候,(T=nil
, V
没有设置), 典型的, 一个nil
接口对应的类型也总是nil
, 如果我们赋值一个*int
类型的空指针给 一个interface,那么这个接口 interface 内部的类型T就是*int
, 不论他这个指针本身是否为 nil: (T=*int
, V=nil
). 这样一个接口类型就不为 nil
, 即使V
部分是一个nil
指针.
这种情况可能令人困惑, 当一个nil
值存储赋值给一个接口类型作为返回值例如error
func returnsError() error {
var p *MyError = nil
if bad() {
p = ErrBad
}
return p // 总是返回一个非空类型的错误
}
如果运行良好, 函数返回一个空指针, 因此返回值是一个error
接口值, 内部类型是(T=*MyError
, V=nil
). 这意味着如果调用者将返回值与nil
比较, 结果总是有错误, 即(returnsError() != nil
总是成立的). 要正确的返回一个nil
错误给调用者, 函数必须返回一个明确的nil
:
func returnsError() error {
if bad() {
return ErrBad
}
return nil
}
对于总是返回错误的函数,最好直接在签名中使用error
类型而不是一个例如*MyError
具体的错误类型, 以保证返回我们希望的错误, 例如标准库中的os.Open函数,返回错误类型,如果不是nil
, 总是返回一个具体的类型os.PathError
无论何时使用接口类型的时候,类似的情况都会出现. 谨记, 如果具体类型赋值给接口, 接口就不为nil
, 更多详细信息,参考 反射规则.
为什么没有像 C 中那样的无标记的联合??
未标记的联合将违反 Go 的内存安全保证。
为什么 Go 没有变体类型??
变体类型,也称为代数类型,提供一种方法来指定值可以采用一组其他类型中的一种,但只能是那些类型。系统编程中的一个常见示例将指定错误为网络错误,安全错误或应用程序错误,并允许调用者通过检查错误的类型来区分问题的根源。另一个示例是语法树,其中每个节点可以是不同的类型:声明,语句,赋值等等。
我们考虑过将变体类型添加到 Go 中,但是经过讨论后,他们决定将它们排除在外,因为它们与接口的混淆方式相互重叠。如果变量类型的元素本身是接口,将会发生什么?
同样,该语言已经涵盖了某些变体类型所针对的内容。使用接口值来保存错误并使用类型开关来区分大小写,很容易表达错误示例。语法树示例也是可行的,尽管不那么优雅。
为什么 Go 没有协变结果类型?
协变结果类型将意味着类似下面的这个接口
type Copyable interface {
Copy() interface{}
}
你对这个方法满意吗
func (v Value) Copy() Value
因为 Value
实现了空接口。在 Go 中,方法类型必须完全匹配,因此 Value
不会实现 Copyable
。Go 将类型的作用(方法)与类型的实现分开。如果两个方法返回不同的类型,则它们不会做相同的事情。想要协变结果类型的程序员通常试图通过接口表达类型层次。在 Go 中,接口与实现之间的清晰区分是很自然的。
Values
为什么Go不支持隐式的数值转换?
在C语言中,数字类型间的自动转换所带来的困扰要超过它所带来的便利。如果表达式是无符号的呢?值占多少位?它会不会越界?在不同的平台结果是否一致?同时,还使得编译器更加复杂;所谓“常用的数值转换”实现起来并不容易,尤其是,它还具有平台相关性。出于可移植性的考虑,我们决定使用直接明了的显式数值转换。但我们对常量的定义做了一些改进,定义常量时,不需要标明是否有符号和具体的位数。
不像C语言,在Go中 int
和 int64
是完全不同的类型,即使 int
是64位的。int
是通用类型,如果你关心数字到底占多少位,Go建议你最好明确标明。
Go 的常量是如何运作的?
Go限制了不同数字类型的变量之间的转换,但常量要灵活得多。诸如23、3.14159和 [math.Pi
](https://golang.org/pkg/math/#pkg-constants) 之类的常量字面量占据了会占据合适的数字空间,可以具有任意精度且不会上溢或下溢。例如,math.Pi
的值在源代码中指定为63位,并且使用该值的常量表达式会确保精度超过float64
可以容纳的范围。只有将常量或常量表达式分配给变量时(程序中的存储位置),它才会成为具有常规浮点数的属性和精度的“可计算的”数字。
同时,由于常量只是没有类型信息的单纯的数字,它的运用比变量自由得多,进而可以抵消一部分强制类型转换的缺点。你可以写出这样的表达式
sqrt2 := math.Sqrt(2)
而不会触发编译错误,因为2
可以安全而准确地转换为用于调用math.Sqrt
的 float64
类型 。
标题为 [常量](https://blog.golang.org/constants) 的博客文章更详细地探讨了此主题。
为什么内置 map ?
与字符串相同的原因:它们是如此强大和重要的数据结构,提供一个具有语法支持的优秀实现会使编程更愉快。我们相信,GO的 Map 实现足够强大,它将服务于绝大多数用途。如果特定应用程序可以从自定义实现中受益,则可以编写一个,但在语法上不会那么方便;这似乎是一个合理的权衡。
为什么 map 不允许切片(slices)作为键?
映射查找需要相等运算符,而切片不实现。它们不实现相等的原因是在这些类型上没法很好地定义相等;存在多种考虑因素,包括浅与深比较、指针与值比较、如何处理递归类型等。我们可能会重新讨论这个问题——实现切片的相等不会使任何现有的程序失效。但是没有明确的切片相等的含义,现在省略它会更简单。
在 GO 1 中,与以前的版本不同,相等是为结构和数组定义的,所以这样的类型可以用作映射键。不过,切片仍然没有相等的定义。
为什么 Map、切片和通道是引用,而数组是值?
关于该话题有很多历史。早期,映射和通道在语法上是指针,不可能声明或使用非指针实例。此外,我们还为数组应该如何工作而苦苦挣扎。最终我们决定,指针和值的严格分离使得语言更难使用。更改这些类型以充当对关联的共享数据结构的引用解决了这些问题。此更改为该语言添加了一些令人遗憾的复杂性,但对可用性产生了很大影响:GO在引入时成为一种更高效、更舒适的语言。
编码
库文档在哪?
有一个用 Go 编写的程序 godoc
,它从源代码中提取包文档,并将其用作带有声明、文件等链接的网页。一个实例运行在 golang.org/pkg/ 上,事实上,整个 golang.org/ 都是用 godoc
构建的。
godoc
实例可以被配置为对程序中的符号进行交互式静态分析;此处 列出了详细信息。
有关从命令行访问文档的信息,可使用 go 工具提供文本 doc 子命令,该子命令提供了文本形式的文档。
是否有Go 编程风格指南?
没有明确的风格指南, 但是有一些公认的 “Go 风格”
Go 在命名,组织结构,文件组织目录方面有一些约定, Effective Go给出了一些此类的建议.更直接的, gofmt
是一个旨在强制性遵循这些规则的代码格式工具;它取代了通常意义上的”做与不做”的风格; Go 仓库中的所有代码和在开源界的绝大部分代码, 都已经通过gofmt
格式化.
标题为Go 代码审核评论的文档, 是一些开发者容易忽略的关于 Go风格细节的文章的合集. 它是Go 项目审核人员的实用参考手册.
如何向 Go 库提交补丁?
库源码位于 项目的src 目录中, 如果您要进行重大更改, 请在行动之前先邮件进行沟通讨论.
有关如何进行的更多信息,请参阅文档为Go项目贡献 。
go get
克隆仓库时为什么要用 HTTPS?
很多公司通常对出站流量仅开放标准 TCP 端口 80(HTTP)和 443(HTTPS),禁止出站流量访问包括 TCP 端口 9418(git)和 22(SSH)在内的其他端口。git
使用 HTTPS 代替 HTTP,增强了默认的鉴权机制,可以防止中间人攻击,窃听和干预攻击。因此 git get
命令为了安全使用 HTTPS。
你可以配置 Git
用 HTTPS 认证还是 SSH 认证。你可以在 $HOME/.netrc
文件中添加一行,来使用 HTTPS 认证,git 会读取这些参数:
machine github.com login USERNAME password APIKEY
对于 GitHub 账户,密码可以是 个人 access token。
你可以配置 Git
对于匹配某些前缀的 URL 用 SSH 来代替 HTTPS。例如,如果你想要对所有的 GitHub 的访问请求都使用 SSH,把下面这些内容添加到你的 ~/.gitconfig
:
[url "ssh://git@github.com/"]
insteadOf = https://github.com/
怎样使用“go get”来管理包版本
从 Go 语言最开始的时候,并没有显性地定义包版本的概念,但是这种情况已经发生了变化。版本控制会产生明显的复杂性,特别实在代码规模巨大的情况下。我们花费的大量时间,为所有 Go 语言用户开发了一套适合各种代码规模、各种场景的版本控制方案。
Go 1.11 版本实验性地在 go
命令上增加了 modules
模块,来支持包的版本控制,具体细节请参考Go 1.11 更新声明 和 go
命令行文档。
不论实际的包管理技术是什么,「go get」和其他 Go 语言工具链都能通过不同的导入路径来提供包的隔离。例如,标准库 html/template
和 text/template
包名都是「package template」,但可以共存。这个论断,可以为包坐着和使用者提供了一些建议。
公开使用的包,在演进时,应该保持向后兼容。Go 1 兼容性参考 就是一个很好的参考指南:不要删除已经被导出的变量名、常量名或者函数名,鼓励为复合数据类型增加标注,等等。如果必须实现不同的功能,增加一个新的名字,而不是修改原来的名字。如果需要与以前的包完全切割关系,创建一个新的导入路径的包。
如果你在使用第三方的包,但是担心它的变更会无法控制,并且还没有使用 Go modules,最简单的办法就是将它的代码复制到你的本地仓库中。这是 Google 内部使用的方法,并且 go
命令通过「vendoring」技术来提供支持。这种方法涉及到存储一份依赖的副本,然后使用本地的导入路径,具体细节可以参考这份设计文档。
指针和内存分配
什么时候函数参数是传值?
像 C 家族中的其他所有语言一样,Go 语言中的所有传递都是传值。也就是说,函数接收到的永远都是参数的一个副本,就好像有一条将值赋值给参数的赋值语句一样。例如,传递一个 int
值给一个函数,函数收到的是这个 int
值得副本,传递指针值,获得的是指针值得副本,而不是指针指向的数据。(请参考[later section] (https://golang.org/doc/faq#methods_on_values_or_pointers)来了解这种方式对方法接收者的影响)
映射和切片的值表现和指针一样:它们是对内部映射或者切片数据的指针的描述符。复制映射或者切片的值,不会复制它们指向的数据。复制接口的值,会产生一个接口值存储的数据的副本。如果接口值存储的是一个结构体,复制接口值将产生一个结构体的副本。如果接口值存储的是指针,复制接口值会产生一个指针的副本,而不是指针指向的数据的副本。
注意讨论是针对这些操作的语义,实际实现时,只要代码优化不改变语义,才会在规避复制数据的情况下,实施代码优化。
我应该在什么时候使用指向接口的指针?
几乎从不。接口值的指针仅在罕见,棘手的情况下出现,这些情况涉及伪装接口值的类型以进行延迟评估。
将指向接口值的指针传递给需要接口的函数是一个常见错误。编译器这时会报错,这会令人费解,因为有时候 指针需要实现接口. 事实是,指向具体类型的指针可以实现接口,而指向接口的指针永远不可能用来实现接口。
观察这个变量声明,
var w io.Writer
打印函数fmt.Fprintf
第一个参数必须实现 io.Writer
也就是必须实现Write
方法。
因此我们可以这样写
fmt.Fprintf(w, "hello, world.")
如果我们传递的是 w
的地址,程序不会编译成功。
fmt.Fprintf(&w, "hello, world.") // 编译时错误。
一个例外是,所有值,即使它是接口指针,都可以赋值给空接口变量(interface{}
)。 即使赋值成功,如果他是接口指针,往往会导致错误发生,结果会使人费解。
我应该使用值还是指针定义方法?
func (s *MyStruct) pointerMethod() { } // 指针方法
func (s MyStruct) valueMethod() { } // 值方法
对于不熟悉指针的程序员,很难理解这两个例子的区别,实际上这很简单。为类型定义方法时,接收者(上面例子中的s
)实际扮演的是方法的一个参数。判断使用值还是指针定义方法与判断使用值还是指针作为函数参数是相同的问题。有以下几点考虑。
首先,最重要的是,这个方法是否需要改变接收者?如果是,接收者必须是指针。(切片和映射扮演引用的角色,他们的情况更微妙,但如果要在一个方法中改变切片的长度,接收者依然必须是指针。)在上面的例子中,如果指针方法
改变s
的字段,调用者可以察觉那些更改,但是值方法
使用的是调用者参数的一个副本(这就是值传递的定义),所以更改对调用者是不可见的。
顺便说一下,在Java中,方法的接收者永远是指针,尽管它们的指针是经过包装的(也有提议加入值接受者)。所以go的值接收者并不常见。
其次,是性能上的考虑。如果接收者是一个很大的结构体
实例,使用指针接收者的开销要小很多。
然后是一致性的考虑。如果一个类型的某些方法必须是指针接收者,那么其他方法也应该如此,这样使用者才可以一致地调用该类型的方法集。查看 方法集 获取更多细节。
对于基础类型,切片,和小的结构体
,值接收者的开销更小,所以除非方法在语义上需要指针接收者,使用值接收者会更高效和明确。
new和make的区别?
简单地说: new
请求分配内存,返回指针,而make
只用于初始化切片,映射,channel。
查看 relevant section of Effective Go 获取更多细节。
int
在64位机器上有多大?
int
和uint
的大小与具体实现相关,但在同一个平台上是一样的。为了可移植性,依赖特定大小值的代码应该使用明确标明大小的类型,如int64
。在32位机器上,int
默认是32位,而在64位机器上整数占64位。(从历史角度,事实并非总是如此)
另一方面,浮点数和复数的大小是确定的(没有float
或complex
类型),因为程序员在使用浮点数时应该知道它的精度。没有指定类型的浮点数常量默认为64位,也就是float64
。因此foo
:=
3.0
声明foo
变量为float64
类型。使用浮点数字面量初始化一个float32
类型变量时,声明中必须明确标明它的类型:
var foo float32 = 3.0
在短声明中,必须使用显式的类型转换 foo := float32(3.0)
。
我怎么知道一个变量是分配在栈上还是分配在堆上?
严格说来,您不需要知道。只要有引用,Go中的变量就会一直存在。
语言对存储位置如何选择的实现,与语言的语义是不相关的。
存储位置的选择会影响程序的效率。Go编译器会尽可能将变量分配在函数的栈帧中。但是,如果编译器发现在函数返回后变量依然被引用,会将变量移到垃圾回收堆中,避免出现悬空指针错误。同时,如果一个本地变量很大,存储到堆上要更加合理。
在当前的编译器中,如果一个变量被其他变量引用,它就可能被分配到堆上。编译器会做逃逸分析,如果这些变量在函数返回时不再存活,那么该变量就会待在栈上。
为什么Go进程占用这么多虚拟内存?
Go内存分配器保留了较大的虚拟内存区域作为分配空间。这些虚拟内存属于特定的Go进程;保留这些虚拟内存不会占用其他进程的内存空间。
要查看Go进程的实际内存占用量,请使用Unixtop
命令并查询RES
(Linux)或RSIZE
(macOS)列。
并发
什么是原子操作?什么是互斥量?
对原子操作的描述可以查看 Go 内存模型l 文档。
[sync] (https://golang.org/pkg/sync) 和[sync / atomic] (https://golang.org/pkg/sync/atomic) 包中提供了低级别同步和原子操作原语。这些包适用于简单的任务,例如增加引用计数或保证小规模的互斥。
Go通过goroutine和channel支持更高级别的并发操作,如协调服务器中的并发请求。你还可以在同一时间使一个数据的特定分片只被一个goroutine操作,Go proverb总结了这种方法。
不要通过共享内存来通信,而是通过通信来共享内存。
查看 Share Memory By Communicating 和 相关文章 以更深入理解这个概念。
当编写大型并发程序时,您能从以上两个地方获益良多。
为什么增加CPU核数不能使我的程序跑得更快?
程序能否从多核CPU获得速度上的提升,取决于它的实现方式。虽然Go提供了goroutine和channel这样的并发原语,只有问题的解决方式是并行的,才能真正从多核CPU获益。串行化执行的任务无法通过增加CPU核数来提升速度,只有那些可以被分解成多个执行流的任务可以,有时候这种提升会相当显著。
有时候,增加CPU核数反而会减慢程序的速度。实际上,当程序使用多个系统线程更多地是去同步和通信,而不是进行有价值的计算任务时,程序的性能会被削弱。这是因为在线程间传递数据会触发上下文切换,消耗很大,而且,上下文切换的消耗还会随着CPU核数的增加而增加。这个例子prime sieve example 中,程序开启了很多goroutine,但并没有并行化;增加线程(CPUs)只会使它跑得更慢。
并发不是并行,您可以在这里深入这个主题。
我如何控制CPU核数?
可同时执行goroutine的可用CPU数量由GOMAXPROCS
shell环境变量控制,默认值是CPU核心的数量。因此,有并行潜力的程序,在多核机器上使用默认值就可以了。要改变这个参数可以通过设置环境变量,或者使用runtime包下的函数。当它被设置成1时,意味着程序程序严格地串行化运行,迫使多个goroutine在一个线程上轮流运行。
当有多个耗时的 I/O 操作时,Go runtime 会申请多于 GOMAXPROCS
数量的线程。 GOMAXPROCS
参数只会影响同时运行的goroutine数量;毕竟它们中相当一部分会阻塞在系统调用中。
尽管优化了很多次,Go的调度器依然不够完善。未来,会进一步优化调度器对系统线程的运用。目前,性能问题可以通过调整特定程序的 GOMAXPROCS
来改善。
为什么没有 goroutine ID?
Goroutine 没有名字;它们只是匿名的执行流。它们的唯一标识符,名字,或数据结构对程序员来说都是不可见的。有些人很惊讶,go
语句居然没有返回一些可以在创建goroutine之后访问或控制它的东西。
编写并发程序时,只有使goroutine匿名,才能充分利用所有的goroutine。相反,为线程和goroutine命名的开发模式会增加之后对他们的复用的难度。
这是一个例子。一旦命名了一个goroutine并给它强行套上一个模型,它将变得很特别,并且人们还会倾向于将所有计算与该goroutine相关联,而忽略了使用多个共享的goroutine进行处理的可能性。如果在net/http
包中,每个请求绑定一个goroutine,那么就不能使用更多的goroutine来服务客户端请求。
而且,那些图形系统库的经验表明,当用并发语言在“主线程”上运行所有处理流程时,这种方法有多么尴尬和憋屈。特殊的线程或goroutine迫使程序员编写出别扭的程序以避免崩溃和一些无意间使用了错误的线程而引发的问题。
对于确实需要特定goroutine的情况,Go语言提供了诸如channel之类的特性,可以灵活地使用它们与之交互。
函数和方法
为何T和指针类型*T有不同的方法集
正如Go 规范 所述, 类型T
的方法集由接受者类型为T
的所有方法组成,而相应的指针类型*T
的方法集由接受者为指针类型*T
和类型T
的所有方法组成, 这意味着类型T
的方法集为类型*T
方法集的子集。
产生这种区别的原因是, 如果接口值包含指针* T
,则方法调用可以通过取消引用指针来获取值,但是如果接口值包含值T
, 则没有安全的方法可以调用获取指针。(这样做将允许一种方法修改接口内部值的内容, 这是语言规范锁不允许的。)
即使在编译器可以将值的地址传递给方法的情况下,如果方法修改该值,则更改也将在调用方中丢失。 例如, 如果bytes.Buffer
的Write
方法使用一个值接受者而非指针, 代码:
var buf bytes.Buffer
io.Copy(buf, os.Stdin)
会将标准输入复制到buf
的副本中, 而不是buf
本身。这几乎从来不是期望的行为。
以协程goroutines运行的闭包会发生什么
当并发使用闭包的时候, 可能会引起一些困惑。 考虑如下程序:
func main() {
done := make(chan bool)
values := []string{"a", "b", "c"}
for _, v := range values {
go func() {
fmt.Println(v)
done <- true
}()
}
// wait for all goroutines to complete before exiting
for _ = range values {
<-done
}
}
一个可能的错误期待的输出是a,b,c
, 而可能看到的是c,c,c
。 这是因为每一个循环迭代都使用了相同的实例变量v
, 因此每一个闭包都共享该单个变量。当闭包运行时, 它会在执行fmt.Println
时打印变量v
的值, 但是自从goroutine启动以来,v
可能已经被修改了,为了帮助在此问题和其他问题发生之前发现他们, 执行 go vet
.
为了在每个闭包启动时绑定当前的v
值, 必须在每次迭代时修改内部循环以创建一个新变量。 一种方法是讲变量作为参数传递给闭包:
for _, v := range values {
go func(u string) {
fmt.Println(u)
done <- true
}(v)
}
在这个例子中, v
的值作为参数传递给一个匿名的函数。 然后在函数内部可以将值作为变量u
进行访问。
更简单是使用声明风格创建一个新变量, 这可能看起来很奇怪,但是在Go中可以正常工作。
for _, v := range values {
v := v // create a new 'v'.
go func() {
fmt.Println(v)
done <- true
}()
}
回想起来,这种语言的行为(未为每次迭代定义新的变量)可能是一个错误。可能会在更高版本中解决,但出于兼容性考虑,在Go版本1中无法更改。
控制流
为什么Go没有 ?:
三元运算符?
你可以用下面的if-else
语句替代三元运算符:
if expr {
n = trueVal
} else {
n = falseVal
}
Go之所以没有三元运算符,是因为设计者们遇到过太多由于过度使用三元运算符而产生的及其复杂的表达式。if-else
格式虽然更长,但毫无疑问更加明了。一个编程语言只需要一种条件控制结构。
包和测试
我应该怎样创建多文件的包?
将所有源文件放到同一个目录。同一目录下的源文件之间共用一个作用域;没必要提前声明或是额外创建一个头文件。
虽然包被分解成多个文件,在编译和测试时,与单文件的包没有任何区别。
如何编写单元测试?
Create a new file ending in _test.go
in the same directory as your package sources. Inside that file, import "testing"
and write functions of the form
在同一目录下创建以 _test.go
结尾的文件。引入测试包, import "testing"
,以下面的格式编写测试函数:
func TestFoo(t *testing.T) {
...
}
在当前目录下运行 go test
命令。它会找到那些 Test
开头的函数,创建一个测试程序并运行它。
查看 如何编写 Go 代码 文档, testing
包和 go test
子命令进一步学习。
我最喜欢的测试辅助函数在哪里?
Go的标准 testing 测试包 使编写单元测试变得很容易,但它缺少其他语言测试框架(如断言函数)中提供的功能。 本文的前一节 解释了为什么 Go 没有断言,此处也适用。 正确的错误处理意味着在一个测试失败后给与准确的提示,这样运行测试失败的人就可以完全了解错误所在。使用断言,只是报告输出的值不符合预期,如果是另一个不熟悉程序的新人看到这个错误,可能会花很多时间来调查到底怎么回事,而如果是清晰的错误提示,发现测试错误的团队成员就可以很快的解决问题。
另一个相关点是,测试框架倾向于开发自成一系的迷你语言,带有条件、控件和打印机制,但是 Go 已经具备了所有这些能力;为什么要重新创建它们?我们宁愿用 Go 编写测试。不仅可以少学一门语言,而且使编写的测试简单易懂。
无论如何,保持测试的简洁与表达清晰,是我们推荐的。
为什么 XXX 不在标准库中?
标准库的目的是支持运行时,连接到操作系统,并提供许多 Go 程序所需的关键功能,例如格式化的 I/O 和网络。它还包含对 Web 编程很重要的元素,包括加密技术和对 HTTP、JSON 和 XML 等标准的支持。
没有具体明确的方法来定义哪些要作为标准库,因为很多情况下,他们是唯一的 Go 库。然而,有一些标准定义了添加的内容。
目前来讲,标准库中新添加的内容很少,并且包含的门槛很高。标准库中包含的代码要承担巨大的持续维护成本(通常由原始作者以外的人承担),要遵守 Go 1兼容性承诺(修复 API 中的任何缺陷),并且要遵守 Go 发布计划,防止错误修复快速提供给用户。
大多数新代码应该位于标准库之外,并且可以通过 go
工具 的 go get
命令访问。这样的代码可以有自己的维护者、发布周期和兼容性保证。用户可以在 godoc.org 上找到软件包并阅读其文档。
尽管标准库中有些部分并不是太有必要,例如 log/syslog
,但由于 Go 1 兼容性承诺,我们继续维护库中的所有内容。但是我们鼓励大多数新代码存在于其他地方。
实现
使用什么编译器技术来构建编译器?
有几个用于 Go 的生产编译器,还有一些正在为各种平台开发。
默认编译器,gc
,包含在 Go 发行版中,作为对 Go
命令的支持的一部分。gc
最初是用 C 编写的,因为启动困难,您需要 Go 编译器来设置 Go 环境。但是现在已经有所改观,自从 Go 1.5 发布以来,编译器已经是一个 Go 程序了。编译器使用自动翻译工具从 C 转换为 Go,如本 设计文档 和 演讲 所述。因此编译器现在是 “自托管”,这意味着我们需要自己面对引导问题。解决方案是让安装一个 Go,就像 C 一样。如何从源代码创建一个新的 Go 环境的故事被描述 这里 和这里。
Gc
is written in Go with a recursive descent parser and uses a custom loader, also written in Go but based on the Plan 9 loader, to generate ELF/Mach-O/PE binaries.
Gc
是用递归下降解析器用 Go 编写的,并使用一个也用 Go 编写但基于 Plan 9 加载器的自定义加载器来生成 ELF / Mach-O / PE二进制文件。
在项目开始时,我们考虑过将 LLVM 用于 gc
,但认为它太大且太慢而无法满足我们的性能目标。回顾起来更重要的是,使用 LLVM 将使引入 ABI 和相关更改变得更加困难(例如堆栈管理)。但是,新的 LLVM实施 现已开始融合在一起。
Gccgo
编译器是一个用 C++ 编写的前端,具有递归下降解析器和标准 GCC 后端。
事实证明,使用 Go 语言是实现 Go 编译器的一种很好的方案,尽管这并不是其最初的目标。从一开始就没有自我托管功能,早期的 Go 只专注于最初的用例,即网络服务器。如果我们决定 Go 应该尽早进行编译,那么我们可能最终会获得一种针对更多编译器构造的语言,这是一个值得为之努力的目标,但不是我们最初的目标。
尽管 gc
尚不需要它们(还未需要),但原生的 lexer 和解析器可以在 go
软件包中使用,也有原生的 type 检查器 。
如何实现运行时支持?
同样由于引导程序的问题,运行时代码最初主要是用 C 语言编写的(带有少量汇编程序),但后来被翻译为 Go(除了一些汇编程)。Gccgo
的运行时支持使用 glibc
。gccgo
编译器使用一种称为分段堆栈的技术实现 goroutines,该技术由修改版的 gold 链接器所支持。Gollvm
类似地构建在相应的 LLVM 基础设施上。
为什么我的小程序是如此大的二进制文件?
gc
的链接器在默认情况下会创建静态链接的二进制文件。因此,所有 Go 二进制文件都包括 Go 运行时,以及支持动态类型检查、反射,甚至是 panic 堆栈跟踪所需的运行时类型信息。
在 Linux 上使用 gcc 静态编译和链接的简单 C 版本的 hello,world 程序大约为 750kB,包括 printf
的实现。使用 fmt.Printf
的功能相同的 Go 棋程序会重达几兆字节,但这包括强大的运行时支持以及类型和调试信息。
用 gc
编译的 Go 程序可以使用-ldflag=-w
标志来禁用 DWARF 生成,从二进制文件中删除调试信息,这可很大程度上减小二进制大小。
我可以停止这些关于未使用的变量/导入的抱怨吗?
存在未使用的变量可能表示存在 bug,而未使用的导入只会减慢编译速度,随着时间的推移,随着程序积累代码和参与程序员的增加,这种影响可能会变得很明显。由于这些原因,Go 拒绝编译未使用的变量或导入,用短期方便换取长期的构建速度和程序清晰度。
仍然,在开发代码时,有时候你需要有临时创建变量的情况,且必须在程序编译之前将其删掉掉可能很烦人。
有些人要求使用编译器选项来关闭这些检查,或者至少将它们降级为警告。我们尚未添加这样的选项,因为编译器选项不应影响语言的语义,而且 Go 编译器不报告警告,只报告阻止编译的错误。
不使用警告,有两个原因。首先,如果值得抱怨的话,就值得在代码中修复它。(如果不值得修复,就不值得一提。) 第二,让编译器生成警告会让编码随意,导致警告泛滥,从而掩饰真正的 应该 被修复的错误。
其实这种情况很容易解决,在开发过程中使用空白标识符即可:
import "unused"
// This declaration marks the import as used by referencing an
// item from the package.
var _ = unused.Item // TODO: Delete before committing!
func main() {
debugData := debug.Profile()
_ = debugData // Used only during debugging.
....
}
现在的解决方案是,大多数 Go 程序员使用一个工具 goimports, 它自动重写 Go 源文件以具有正确的导入,消除了实际中未使用的导入问题。此程序可以轻松集成到大多数编辑器中,以便在编写 Go 源文件时自动运行。
为什么我的病毒扫描软件认为我的 Go 编译的二进制文件受到感染?
这是一种常见情况,特别是在 Windows 计算机上,并且几乎总是误报。商业病毒扫描程序通常对 Go 二进制文件的结构感到困惑,它们不像从其他语言编译的程序那样经常看到 Go 二进制文件。
如果你刚刚安装了 Go (官方下载),而系统报告它已感染,则这肯定是错误的。保险起见,您可以那文件校验值与 下载页面 上的校验值进行匹配。
任何情况下,如果您认为报告有错误的,请向您的病毒扫描程序的供应商报告错误。也许随着时间的推移,病毒扫描程序可以学会理解 Go 程序。
性能
为什么 Go 在基准测试 X 上表现不佳?
Go 的设计目标之一是接近 C 的性能,但在一些基准测试中,它的表现相当糟糕,包括几个 golang.org/x/exp/shootout 。最慢的速度取决于在 Go 中没有可比较性能的库。例如 pidigits.go 依赖于一个多精度的数学包,而 C 版本与 Go 不同,使用的是 GMP (用优化的汇编程序编写)。依赖于正则表达式的基准测试 如 regex-dna.go 实际上是在将 Go 的原生 regexp包 与成熟的高度优化的正则表达式库(如PCRE)进行比较。
在基准测试游戏中,通过不断调整代码往往能取得胜利。而大多数基准测试的 Go 版本需要许多调整。如果您测量可比较的 C 和 Go 程序,例如说这个 (reverse-complement.go ,你会看到两个语言的原始性能上要接近很多。
不过,仍有改进的余地。编译器很好,但还可以更好,许多库需要更多的性能调优,而垃圾收集器还不够快。(注意编码时不要产生不必要的垃圾也会对性能产生巨大影响。)
在任何情况下,Go 通常都是非常有竞争力的。随着语言和工具的发展,许多程序的性能都有了显着的提高。有关详细的示例,请参阅博客文章 调试 Go 程序 。
与 C 的不同点
为什么语法与 C 有如此大差异?
除了声明语法之外,差异并不大,源于两个需求。首先,语法应该比较轻松,没有太多的强制性关键字,重复项或令人费解。其次,该语言被设计为易于分析,并且可以在没有符号表的情况下进行解析。这使得构建调试器(debugger)、依赖关系分析器(analyzer)、自动文档提取器(extractor)、IDE 插件等工具变得更加容易。C 及其衍生品在这方面都做得很差。
为什么声明是反向的?
只有当你习惯了 C 时,它们才是反向的。C 中的概念是变量像的表达式一样声明,这是一个好主意,但是类型和表达式语法不能很好地混合,结果可能会令人混淆,函数指针就是一个例子。Go 主要将表达式和类型语法分开,这简化了事情许多东西(使用前缀 *
作为指针是一个例外,证明了这一规则)。在 C 中,声明:
int* a, b;
声明 a 为指针,但 b 不为指针。在 Go 中:
var a, b *int
声明两个都是指针。这更清晰,更规则。此外,完整的变量声明也与:=
具有相同的顺序,因此
var a uint64 = 1
与以下具有相同的效果
a := uint64(1)
通过对类型使用独特的语法(不仅是表达式),还可以简化解析。诸如 func
和 chan
之类的关键字可使一切保持清晰。
有关更多详细信息,请参见有关Go的声明语法 。
为何没有指针算法?
安全性考虑。如果没有指针运算,就有可能创建一种永远不会派生出不正确地址的语言。编译器和硬件技术已经发展到可以使用数组索引的循环与使用指针算术的循环一样高效的地步。另外,缺乏指针算法可以简化垃圾收集器的实现。
为什么 ++ 和 – 是语句而不是表达式?为什么是后缀而不是前缀?
如果没有指针运算,前缀和后缀递增运算符的便利性就会下降。通过将它们从表达式层次结构中一起删除,表达式语法得到了简化,并且也消除了围绕 ++
和 --
求值顺序的混乱问题(考虑f(i+)
和p[i] = q[++i]
)。简化是有意义的。至于后缀与前缀,两者都可以很好地工作,但后缀版本更传统;对前缀的坚持源于 STL,具有讽刺意味的是,STL 是一种语言的库,其名称包含后缀增量。
为什么有花括号但没有分号?为什么我不能在下一行放开括号?
Go 使用大括号进行语句分组,这是使用过 C 系列中任何语言的程序员都熟悉的语法。然而,分号是用于解析器的,而不是用于人的,我们希望尽可能地消除它们。为了实现这一目标,Go 借用了 BCPL 的一个技巧:分号表示单独的语句位于形式语法中,但由词法分析器在任何可能的行尾自动注入,而无需先行查看。这在实践中非常有效,但其缺点是需要强制使用大括号的风格。例如,函数的左大括号不能单独出现在一行上。
有些人认为解析器应该先行查看,以允许大括号位于下一行。我们不同意。由于 Go 代码有自动格式化 gofmt
,因此必须选择一些代码风格。这种风格可能与你在 C 或 Java 中使用的风格不同,但 Go 是一种不同的语言,并且 gofmt
的风格和其他风格一样好。更重要的是,所有 Go 程序的单一编程强制格式的优点远远超过特定风格的所带来的缺点。还要注意,Go 的风格意味着 Go 的交互式实现可以按照标准逐行读取,而不需要特殊规则。
为什么要进行垃圾收集?开销会不会太大?
系统程序语言中管理已分配对象的生存期是一个巨大的话题。在诸如 C 这样的语言中,它是手动完成的,它可以消耗大量的程序员时间,并且常常是致命错误的原因。即使在 C++ 或 Rust 等提供辅助机制的语言中,这些机制也可以对软件的设计产生重大影响,通常会增加自身的编程开销。我们认为消除这样的程序员开销是至关重要的,过去几年垃圾收集技术的进步让我们相信,它可以以足够低的成本和延迟来实现,对于网络系统来讲这是可行的。
并发编程的许多困难都源于对象生存期问题:当对象在线程之间传递时,保证它们安全地被释放变得很麻烦。自动垃圾收集使并发代码更易于编写。当然,在并发环境中实现垃圾收集本身就是一个挑战,但在每个程序中实现一次垃圾收集会帮助每个人。
并发编程的很多困难都源于对象生存期问题:当对象在线程之间传递时,保证它们安全释放变得很麻烦。自动垃圾收集使得并发代码更容易编写。当然,在并发环境中实现垃圾收集本身是一个挑战,但这总比每一个程序员都去实现一遍要强。
最后,除了并发性,垃圾收集使接口更简单,因为它们不需要指定如何在他们之间管理内存。
这并不是说 Rust 这样的语言的新的想法是错误的;我们鼓励这项工作,并兴奋地看到它如何发展。但是 Go 采用了一种更传统的方法,通过垃圾收集和单独的垃圾收集来处理对象生存期。
当前的实现是一个标记-清除-收集器(mark-and-sweep collector)。如果机器是多处理器,则收集器与主程序并行运行在单独的 CPU 核心上。近年来关于收集器的进展是将暂停时间减少到亚毫秒范围,甚至对于大型堆也是如此,几乎根除了网络服务器中垃圾收集的异议。接下来的工作将继续完善算法,进一步减少开销和延迟,并探索新的方法。2018年 ISMM Keynote 由 GO 团队的 Rick Hudson 介绍了迄今为止的进展并提出了一些未来的计划。
关于性能这块,请记住,Go 给予程序员对内存布局和分配的控制权相当大。细心的程序员可以通过很好地使用该语言来显著降低垃圾收集开销;请参阅关于 profiling go Programs 的文章,以获得一个有效的示例,包括Go 的调试工具的演示。