Golang常见问题和答案

golang 专栏收录该内容
3 篇文章 1 订阅

原文:https://golang.org/doc/faq
能力一般,水平有限,翻译纯属兴趣,如有错误请多指正~不过我不一定会改~

起源

这个项目的目的是什么?

在Go语言成立之初,虽只有十来年,编程界与如今已不相同。生产软件通常用C++或Java写的,Github还不存在,大多数计算机还不是多核的,除了Visual Studio和Eclipse之外,很少有IDE或其他高级工具可用,更别说在互联网上免费了。

于此同时,我们对使用我们使用的语言开发服务器软件所需的过度复杂性感到沮丧。计算机变得非常快,因为C,C++,Java等语言是最早开发的,但变成行为本身并没有提高。此外,很明显多核处理器变得普遍,但是大多数语言提供很少的支持来高效和安全地编程。

我们决定退一步来思考,随着技术的发展,在未来几年哪些问题即将主导软件工程,而且怎样的一门新语言可能帮助解决这一问题。例如,多核CPU的兴起认为一种语言应该为某种并发或并行提供一流的支持。并且使大并发程序中的资源管理更加从容,垃圾回收,或者至少提供某种安全自动的内存管理。

从Go出现时,这些考虑引起了一些列的讨论,首先是一系列的想法和愿望,然后变成一门语言。Go的首要目标是通过启用工具、例如代码格式化等自动执行的任务、以及移除在大代码库上工作的障碍来帮助工作中的程序员。

Go目标的一个更加广泛的描述,以及如何实现,或者至少是接近,可见这篇文章,Go在Google:软件工程中服务的语言设计

这个项目的历史是什么?

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吉祥物的起源是什么?

吉祥物和徽标是由Renne French设计的,他也设计了Glenda, the Plan 9 Bunny。一篇关于gopher的博客解释了它是如何从几年前用于WFMU T恤设计的徽标衍生出来的。这个吉祥物和徽标持有Creative Commons Attribution 3.0许可。

gopher有一个模型表来说明他的特征以及如何正确表示他们。模型表在2016年的gopher大会上由Renee在一个讲座上第一次展示。他有独特的特征;他是Go gopher,而不仅仅是老地鼠。

为什么要创建一个新语言?

Go语言诞生于我们对在Google工作的现有语言和环境感到沮丧。编程变得越来越困难,语言的选择就归咎于此。要么选择高效编译的,要么选择高效执行的,要么选择易于编码的;这三者不能在同一门主流语言中同时可得。程序员可以通过转向动态类型语言来选择安全性和效率,比如从C++或更轻量的Java转向Python和Javascript。

我们并不孤独。编程语言在经历了几年的风平浪静之后,Go几乎是这些新语言(Rust, Elixir, Swift以及更多)中的第一个使编程语言开发重新成为一个活跃,几乎是主流的领域。

Go语言通过尝试结合解释性语言的简易性,动态类型语言的高效性和静态类型编译语言的安全性来解决这些问题。它也致力于实现现代化,支持网络和多核计算。最后,使用Go语言也是为了快:它应该最多只需几秒钟就可以在单台计算机上编译一个大型可执行程序。要实现这些目标需要解决大量语言问题:一个富有表现力但轻量级的类型系统,并发和垃圾回收,严格以来规范,等等。这些不能通过库或工具来解决,一门新的语言应由而生。

Go在谷歌这篇文章论述了Go语言设计背后的背景和动机,也提供了关于这个FAQ中许多问题的详细答案。

Go的祖先是什么?

Go大多属于C家族(基本语法),从Pascal/Modula/Oberon家族(声明,包)吸收了很多,加上了受Tony Hoare的CSP的启发的语言上的一些想法,比如Newqueak和Limbo(并发)。然而,它是一门全新的语言。在每个方面,这门语言都是通过思考程序人员做什么以及如何编程来设计的,至少我们做的这个类型的编程,更高效,意味着更有趣。

设计中的指导原则是什么?

当Go在设计的时候,Java和C++是最常用的编写服务器程序的语言,至少在Google是这样的。我们觉得这些语言需要太多的簿记和重复代码。一些编程人员转向像Python这样更动态、流畅的语言,以效率和类型安全为代价。我们觉得在一门语言中拥有高效性、安全性和流动性也应该是可能的。

Go试图减少单词的输入量。在整个设计中,我们努力减少混乱和复杂性。没有前置声明,没有头文件;所有东西只声明一次。初始化是直接表现的、自动的、且易于使用。语法清晰且关键字明了。像口吃一样的(foo.Foo* myFoo = new(foo.Foo))简化成简单的类型推导,使用 := 声明-初始化来构造。最根本的是,没有类型继承:是什么类型就是什么类型,不需要宣布它们的关系。这些简化使Go的表达更直观也不牺牲可理解性。好吧~诡辩~~~

另一个重要原则是保持概念正交。方法可以实现任何类型、结构体表现数据而接口表现抽象,等等。当多个事物结合到一起时,正交原则使其更容易理解。

应用

谷歌内部使用Go吗?

是的。在谷歌内部,Go广泛应用于生产。一个简单的例子是golang.org的后端服务。仅仅是godoc文档服务运行在Google App Engine的生产配置上。

一个更重要的例子是谷歌的下载服务器,dl.google.com,用于提供Chrome二进制文件和其它大安装包,例如 apt-get

Go不是谷歌使用的唯一语言,远不止于它,但它是许多领域的关键语言,包括网站可靠工程(SRE)和大规模数据处理。

哪些其它公司使用Go?

Go的应用在全世界增长,特别但不仅限于云计算领域。两个用Go写的主要云基础架构项目是Docker和Kubernetes,但还有更多。

不仅仅是云,Go Wiki有一,会定期更新,列出了许多使用Go的公司中的一些。

Wiki也有一页关于使用这门语言的公司和项目的成功故事

Go程序能链接C/C++程序吗?

在同一个地址空间内同时使用C和Go是可以的,但它不是天然适合,需要特殊的接口软件。而且,Go代码链接C程序会丧失Go提供的内存安全和栈管理特性。有时不得不用C库来解决问题,但这样做总会带来纯Go代码所没有的风险因素,所以这样做时需谨慎。

如果你需要在Go中使用C,如何使用取决于Go编译器的实现。有三种Go团队支持的Go编译器。它们是默认的编译器gc,背后使用GCC的gccgo和使用LLVM架构的还不太成熟的gollvm。

gc使用与C不同的调用链,因此不能直接从C陈谷调用,反之亦然。cgo程序提供“外部方法接口”机制来允许Go代码中安全调用C语言库。SWIG将这个能力扩展到C++库。

你也可以将cgo和SWIG与Gccgo和gollvm一起使用。由于它们使用惯用的API,在万分小心的情况下也可以从这些编译器上直接链接GCC/LLVM编译的C或C++程序。但是,安全地这样做需要了解所有相关语言的调用约定,以及Go调用C或C++时的栈限制。

Go支持哪些IDE?

Go工程不包含自定义IDE,但是这门语言和库已经被设计成能轻松解剖源代码。因此,大多数知名的编辑器和IDE能够很好地直接或通过插件支持Go。

这些知名IDE或编辑器中有很好的Go支持的有Emacs, Vim, VSCode, Atom, Eclipse, Sublime, IntelliJ(通过名为Goland的自定义变种), 以及更多。对于Go编程来说,你最喜欢的就是最适用的。

Go支持谷歌的Protocol Buffers吗?

一个独立的开源项目提供了必要的编译器插件和库。详见github.com/golang/protobuf

我可以将Go主页翻译成其它语言吗?

完全可以。我们鼓励开发者将Go语言网站翻译成他们自己的语言。但是,如果你需要添加Google商标品牌到你的网站(又没有出现在golang.org),你需要遵守www.google.com/permissions/guidelines.html上的准则。

设计

Go有运行时吗?

Go有一个名为runtime的扩展库,它是每个Go程序的一部分。运行时库实现了垃圾回收、并发、栈管理和其它Go语言的关键功能。虽然它对语言更为重要,但Go的运行时类似于libc,即C库。

重要的是要理解Go的运行时不包括(像Java运行时支持的)虚拟机环境。Go程序提前编译为本机机器码(或者JavaScript, WebAssembly,用于某些变体实现)。因此,即使这个词经常用于描述一个程序运行时的虚拟环境,但在Go中,"runtime"这个词只是提供关键语言服务库的名称。

关于Unicode标识符

当设计Go的时候,我们想要确保它不完全以ASCII为中心,这意味着从7位ASCII的范围扩展标识符空间。Go的规则——标识字符必须为Unicode编码的字母或数字——便于理解和实现,但也有限制。结合字符被排除在设计之外了,例如,排除了一些像Devanagari的语言。

这个规则还有另一个不幸的结果。由于一个可导出字符必须以大写字母开头,一些语言的字符创建的标识符可以被定义,但不能被导出。目前唯一的方案是用像X日本語这样的定义方式,但明显是不满意的。

从这门语言最早的版本开始,就投入大量精力来思考如何方便编程人员使用他们的本地语言来最好地扩展标识符空间。究竟该做什么仍是一个活跃的讨论主题,并且该语言的未来版本在其标识符的定义上可能更加自由。比如,可能采纳一些来自Unicode组织的推荐作为标识符。无论发生什么,必须完全兼容,来保护(也许是扩展)字母大小写决定标识符可见性的方式,这将保留Go最有趣的特性之一。

目前,我们有一个简单的规则,可以在不破坏程序的情况下进行扩展,避免出现含糊不清标识符规则引起的错误。

为什么Go没有某某特性?

所有语言包含新颖的特性也会漏掉某些人最喜爱的特性。Go的设计着眼于编码得体、编译迅速、概念正交,以及支持如并发和垃圾回收特性的需要。你最喜爱的特性可能会缺失,因为它不满足Go的需要,因为它影响编译速度或设计清晰性,又或者因为它可能会导致基本系统模型太复杂。

如果Go缺失的某某特性困扰了你,请原谅我们并审视Go是否确实需要这个特性。你也许会发现它会以有趣的方式来补偿某某特性的缺失。

为什么Go没有泛型?

泛型可能会在某个时间点加进来。我们不认为它很紧急,尽管我们理解一些编程人员认为如此。

Go旨在作为一门写服务端程序的语言,它应该随着时间推移也易于维护。(更多背景见这篇文章)它设计集中在像可扩展性、可读性和并发特性等事情上。对于这门语言当前的目标,多态编程看起来不是必须的,所以为了简单就放下了。

这门语言现在更加成熟了,可以考虑某种形式的泛型编程。然而,仍有一些告警。

泛型是很方便,但也增加类型系统和运行时环境的复杂性。我们还没有找到一个设计使其带来的价值与复杂性相当,尽管我们仍然在想办法。Go内置的map和slice,增加了使用空接口构造容器(显示拆箱)的能力,如果不太顺利,意味着在许多情况下可以编写泛型能够支持的代码。

这个主题会持续开放。看一看之前Go在设计好的泛型解决方案上不成功的尝试,见这个提案

为什么Go没有异常?

我们认为将异常耦合到控制结构,像 try-catch-finally 风格,会导致错综复杂的代码。它也趋向于鼓励程序人员标记像打开文件失败这样的通用错误作为异常。

Go采取了一个不一样的途径。为简化错误处理,Go的多值返回使它易于报告一个错误而不需要使返回值过载。规范的错误类型结合Go的其它特性,使异常处理比较舒适,也完全不同于其它语言。

Go也有两个内置的信号处理函数和从完全异常条件下恢复。在出现错误之后,函数状态被终止,恢复机制仅作为其中的一部分被执行,它足够去处理异常,且不需要额外控制结构,如果使用的好,会使代码的错误处理异常简洁。

详见文章Defer,Panic和Recover。而且,Errors are Values这篇博客通过示范描述了Go中干净处理错误的一种方法,由于错误即是值,Go在错误处理上的全部力量都能展示出来。

为什么Go没有断言?

Go不支持断言。它无疑是方便的,但我们的经验得知程序猿们经常把它作为避免思考正确错误处理和报告的拐杖。正确的错误处理意味着当一个非致命错误发生时服务继续运行而不是宕掉。正确的错误报告意味着错误是直接扼要的,避免程序猿们理解一大堆错误堆栈跟踪日志。当程序猿看到不熟悉的代码的错误时,精确的错误描述尤为重要。

我们知道这是一个争论点。Go语言和库中有许多东西与现代实践不同,仅仅是因为我们觉得有时候尝试一些不同的方法是值得的。

为什么用CSP思想构建并发?

并发和多线程编程以编码困难著称。我们认为这一部分原因是像pthreads这样复杂的设计,一部分在于过分强调像互斥、竞态变量和内存屏障这样的底层细节。更高级接口可以使代码实现简单,即使在底层仍有互斥等概念。

为并发支持高级语言的一个最成功模型之一的是来自霍尔的Communicating Sequential Processes,即CSP。Occam和Erlang是两个众所周知的源于CSP的语言。Go的并发原语源自于这个家族树中不同的一部分,它的主要贡献是通道作为第一类对象的强大概念。一些更早语言的经验表明CSP模型的非常适用于面向过程语言框架。

为什么用goroutines替代线程?

Goroutines是使并发易于使用的一部分。这个想法已经存在一段时间了,它将独立执行的函数——协程——复用到一组线程上。当一个协程阻塞了,比如调用了一个阻塞的系统调用,运行时环境会自动移动在同一个操作系统线程上的其它协程到一个不同的、可运行的线程,这样它们就不用被阻塞。对于程序猿来说这些是不可见的,却也是关键点。结果,goroutines就非常便宜:它们在堆栈的内存之外几乎没有开销,只需要几千字节。

为了使堆栈空间变小,Go的运行时使用可调整大小的、有边界的栈。一个新创建的goroutine只给几千字节,而这几乎总是够的。当不够的时候,运行时会自动增加或收缩内存在存储堆栈,这允许大量的goroutine生存在适度的内存中。平均每个函数调用的CPU开销为三个廉价指令。实验证明在同一个地址空间上可以创建成百上千个goroutine。如果goroutines仅仅是线程,系统资源只能够运行更小的数量。

为什么map操作不定义成原子的?

经过一段长时间的讨论后决定map的典型用法不需要多goroutine安全访问,在那些情况下,map可能是那些大型数据结构的一部分,或着是已经同步了的计算。因此要求所有的map操作持有互斥锁会降低大多数程序效率,且只增加少量的安全性。这不是一个轻松的决定,然而,这意味着不受控制的map操作可能使程序挂掉。

这门语言并不排除原子的map更新。当需要的时候,比如当托管不受信任的程序时,可以实现内部锁访问的map。

map访问仅在发生更新时不安全。只要所有的goroutine只读取——检索map中的元素,包括使用for loop循环遍历,只要不指定元素来改变map或者删除,在没有同步的情况下并发访问map是安全的。

作为正确使用map的辅助手段,一些该语言的实现包含一个特殊的检测,当一个map被并发执行不安全的修改时会自动上报给运行时环境。

你会接受我语言的变化吗?

人们经常建议改进这门语言——mailing list包含此类讨论的丰富历史——但是这些改变中的很小一部分被接受。

尽管Go是一个开源项目,这门语言和库受一个兼容性协议保护,来防止破坏现有程序,至少源代码层面(程序可能需要偶尔编译来保持最新)的改变。如果你的提议违反了Go 1的规范,无论其优点如何,我们都无法接受这个想法。Go的未来某个重大发布可能与Go 1不兼容,这个话题的讨论只会在这个事情被确认的情况下才会开始:在这个过程中引入的不兼容性很少。此外,兼容性承诺鼓励我们为旧程序提供自动路径,以便在出现这种情况时进行调整。

即使你的提议与Go 1规范兼容,它可能不符合Go的设计目标。Go at Google: Language Design in the Service of Software Engineering这篇文章解释了Go在其设计背后的起源和动机。

类型

Go是一个面向对象的语言吗?

是,也不是。尽管Go有类型和方法,且允许面向对象风格的编程,但没有类型继承。Go的“接口”概念提供了一种不同的途径,我们认为它易于使用且某些方式上更通用。也有一些方式在一个类型中嵌套另一个类型来提供类似的功能,但与子类不完全相同。而且,Go中的方法比C++或Java更通用:可以为任何类型的数据定义它们,甚至类如文本、拆箱整型等内置类型。对structs(classes)没有任何限制。

而且,类型继承的缺失使Go中的"objects"感觉比C++或Java语言中的更轻量级。

如何动态调度方法?

动态调度方法的唯一方式是通过接口。结构体或其它类型的方法始终是静态解析的。

为什么没有类型继承?

面向对象编程的,至少在最熟知的语言中,涉及很多类型之间关系的讨论,这些关系通常可以自动继承。Go采取了一种不同的途径。

Go类型自动满足指定其方法子集的任何接口,而不是要求程序猿提前声明两个类型之间的关系。除了减少簿记,这种方法还有实质的好处。类型可以在没有复杂的传统多重继承的情况下同时满足多个接口。接口可以非常轻量级——一个只有一个或没有方法的接口可以表达有用的概念。如果一个新的想法蹦出来或者为了测试,接口可以在实体之后添加进来——不需要申明原始类型。因为类型和接口之间没有复杂的关系,也没有类型继承来管理和讨论。

可以用这些想法来构造类似于类型安全的Unix管道的东西。例如,看fmt.Fprintf如何实现格式化打印任何输出,而不仅仅是一个文件,或者看bufio包如何完全独立于文件I/O,或者看image包如何生成压缩图片文件。所有这些思想源自单个方法(Write)的单个接口(io.Writer)。这只是表面问题。Go的接口在如何构造程序方面有着深刻的影响。

这需要一些习惯,但这种隐式的类型依赖风格是Go的最有成果的事情之一。

为什么len是一个函数而不是方法?

我们讨论了这个问题,但决定以实践中更好的函数方式实现len,且并没有使关于基本类型的接口(Go类型意义上)问题复杂化。

为什么Go不支持方法和运算符的重载?

如果不需要进行类型匹配,则会简化方法调度。其它语言的经验告诉我们拥有一种相同名字但不同意义的方法偶尔有用,但实践中也可能令人困惑和脆弱。仅按名称匹配并要求在类型中保持一致性是Go类型系统中的一个主要简化策略。

关于运算符重载,相比于绝对的需求,它似乎是更方便的。再次重申,如果没有它,事情会更简单。

为什么Go没有“implements”声明?

Go类型通过实现接口的方法来满足一个接口,而不需要更多。这个特性允许接口可以在不需要改动现有代码的情况下被定义和使用。它支持一种结构化类型,可以促进关注点的分离并提高代码的重用性,并且可以轻松地构建随着代码开发而出现的模式。接口的语义是Go灵活轻巧的主要原因之一。

详见类型继承问题

如何保证我的类型满足一个接口?

你可以问编译器来检查类型T是否实现了接口I,通过尝试给类型T或指针T赋零值,如下:

type T struct{}
var _ I = T{}        // Verify that T implements I.
var _ I = (*T)(nil)  // Verify that *T implements I.

如果 T(或者 *T)没有实现接口 I,编译时会导致错误。

如果你希望一个接口的使用者显示地声明实现了它,你可以给接口的方法集添加一个描述性名字的方法。例如:

type Fooer interface {
	Foo()
	ImplementsFooer()
}

一个类型必须实现了 ImplementsFooer 方法菜呢罡成为一个 Fooer,清晰地文档化这个事情,又在godoc的输出中描述了它。

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} // dose not sitisfy Equaler

与某些多态类型系统中的类似情况不同,T 没有实现 Equaler 。参数 T.Equal 的类型是 T ,不是按照字面上要求的类型 Equaler

在Go中,类型系统不会提升参数 Equal;那是程序猿的责任,如下面的类型 T2 ,实现了 Equaler

type T2 int
func (t T2) Equal(u Equaler) bool {return t == u.(T2)} // satisfies Equaler

尽管这不像其它的类型系统,因为Go里面任何满足 Equaler 的类型都可以作为作为参数传给 T2.Equal ,在运行时我们必须检查参数是 T2 类型。而一些语言在编译时做这个保证。

另一种方式的相关例子:

type Opener interface {
	Open() Reader
}

func (t T3) Open() *os.File

在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
}

如果 T1T2 有相同的底层类型,可以将 []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中,类型和方法是紧密联系的,每一个命名的类型都有一个方法集(可能是空的)。常用规则是你可以改变要被转换的类型的名字(这也可能要改变它的方法集)但你不能改变合成类型里面元素的名字(和方法集)。Go需要你对类型转换非常明确。

为什么 nil 错误值不等于 nil

在封面下(直译,没找到上下文),接口由两个元素实现,类型 T 和值 VV 是一个例如整型、结构体或者指针等具体的值,而不是接口自身,且有类型 T 。例如,如果我们在接口中存储整型值3,结果接口值示意图为(T=int, V=3)。值V也称作接口的动态值,由于给定的接口变量在程序执行过程中可能有不同的值V(以及相对应的类型T)。

一个接口的值只有在V和T都没有设值的时候是nil的,(T=nil, V没有设值),特别是,一个nil接口会一直持有nil类型。如果我们在一个接口值中存储*int类型的nil指针,内部类型会是*int,而不管指针的值:(T=*int, V=nil)。这样的接口值会是非空的,即使当指针值V内部是nil的。

这种情况会令人困惑,当一个nil值存储在一个接口值中时会发生,比如一个 error 返回:

func returnsError() error {
	var p *MyError = nil
	if bad() {
		p = ErrBad
	}
	return p // Will always return a non-nil error.
}

如果一切运行良好,这个函数返回一个nil p,所以返回值是一个持有(T=*MyError, V=nil)的 error 接口值。这意味着如果调用者拿返回的错误与nil对比,将会看起来一直有一个错误,即使没人任何 bad 发生。要向调用者返回正确的nil错误,该函数必须返回一个显示的nil:

func returnsError() error {
	if bad() {
		return ErrBad
	}
	return nil
}

需要返回错误的函数总是在其签名中使用错误类型(如上)而不是像 *MyError 这样的具体类型,以保证错误被正确的创建,这是一个好主意。比如 os.Open返回一个错误,即使不是nil,它总是具体的类型 *os.PathError

无论何时接口被使用时,上面描述的类似的情况都会出现。只要谨记在心如果任何确切的值存储在接口中,接口就不会为nil。获取更多信息,见反射法则

为什么没有像C中的untagged unions?

Untagged unions会破坏Go的内存安全保障。

为什么Go没有variant(变种)类型?

variant类型,也称作代数类型,提供一种方式来指定一个值可能是一些类型集中的一种,但也只能是这些类型。系统编程中一个常见例子是指定错误,例如,网络错误、安全错误或应用错误,并允许调用者通过检查错误类型来区分问题的来源。另一个例子是语义树中的每一个节点都可以是不同的类型:声明、陈述、赋值等等。

我们考虑过为Go添加variant类型,但经过一番讨论之后决定把它们移除掉,因为它们与接口的重叠会令人困惑。想一想如果一个variant类型的元素是它们自己的接口会发生什么?

此外,该语言的已经涵盖了variant类型的一些目的。上面错误类型的例子很容易表达使用接口值来报错错误并用类型来区分场景。语义树的例子也可行,尽管没有这么优雅。

为什么Go没有协变结果类型?

协变结果类型意味着像这样的接口

type Copyable interface {
	Copy() interface{}
}

会被如下方法满足

func (v Value) Copy() Value

因为 Value 实现了空接口。Go的方法类型必须完全匹配,所以 Value 没有实现 Copyable 。Go将类型所做的(即方法)概念与类型的实现分开。如果两个方法返回不同的类型,它们就不是做同样的事情。想要协变结果类型的程序猿经常尝试通过接口表达类型继承。Go更天然地将接口和实现清晰的分开了。

为什么Go不支持隐式数值转换?

C里面数值类型自动转换的便利性被它导致的混乱给抵消了。什么时候是一个无符号的表达式?值有多大?它会溢出吗?结果是否可移植,与其执行的机器有没有关系?它也使编译器变得复杂;“常用的算术转换”不容易实现,而且夸架构中也不一致。出于可移植性的原因,我们决定以代码中的一些显示转换为代价,使事情变得清晰明了。Go中常量的定义——任意精度值不需要符号和位数注释——但大大改善了问题。

一个相关的细节是,与C不同,即使int是64位类型,int和int64也是不同的类型。int类型是通用的;如果你关心一个整型占用多少字节,Go鼓励你使用显示的。

Go里面的常量是如何工作的?

尽管Go中不同数字类型的变量的转换是严格的,这门语言中的常量却更加灵活。例如23,3.14159和math.Pi这样的字面常量占用着一种理想数量的空间,它有任意精度且没有溢出。例如,在源码中math.Pi的值占用63位的空间,并且涉及该值的常量表达式保持超出float64可以容纳的精度。只有当常量或常量表达式分配给一个变量——程序中的内存位置——它才具有通常的浮点属性和精度的“计算机”数字。

而且,因为它们只是数字,不是有类型的值,Go中的常量可以比变量更自由地使用,从而缓解严格转换规则的一些尴尬。我们可以写如下的表达式

sqrt2 := math.Sqrt(2)

而没有来自编译器的抱怨,理想数字2可以被安全地转换成float64来调用math.Sqrt。

一篇发表主题为常量的博客更加详细的探索了这个话题。

为什么maps是内置的?

与strings相同的原因是:它们是如此强大和重要的数据结构,提供一种语法上支持的优秀实现使编程更加有乐趣。我们相信Go的maps实现足够强壮,能够用于绝大多数用途。如果一个特殊的应用可以从一个自定义的实现中获益,那有可能重写一个,但它不会语法上如此方便;这似乎是一个合理的权衡。

为什么maps不允许切片作为keys?

Map的搜索需要一个相等运算,这是切片没有实现的。因为相等在这个类型上没有定义好,所以他们没有实现;有多重因素需要考虑,包括浅与深比较、指针与值比较、如何处理递归类型等等。我们可能会重新审视这个问题——并且实现切片的相等接口且不破坏现有程序——但如果没有相等性的清晰含义,那么暂时将其排除在外是更简单的。

在Go1中,与先前的版本不同,为结构体和数字定义了相等性,因此这些类型能作为map的keys。但是,切片仍没有相等性的定义。

为什么maps, slices和channels是引用而arrays是值?

关于这个话题有许多的历史。早期,maps和channels是语法指针,且不能申明或使用一个非指针的实例。而且,我们也困扰于数组应该如何工作。最终我们决定严格分离指针和值使语言更难使用。改变这些类型以用作对关联的共享的数据接口的引用来解决这些问题。这些改变给这个语言添加了一些令人遗憾的复杂性,但对可用性有很大的影响:当它被引入时,Go成为一种高效的、更舒适的语言。

写代码

库是如何记录的?

有一个用Go写的程序,godoc,从源代码中提取包文档,并且提供一个链接有声明、文件等的web页面。一个实例在golang.org/pkg/上运行。实际上,godoc实现了在golang.org上的所有网页。

godoc实例可以配制成为其显示的程序提供丰富的、交互式的静态分析符号。详情见

为了从命令行查看文档,go工具有一个doc子命令来为相同的信息提供文本界面。

有没有Go编程风格规范?

没有一个明确的风格规范,尽管肯定承认有“Go风格”。

Go已经创办了大会来指导决定命名、布局和文件组织。Effective Go这篇文章包含了这些话题的一些建议。

更直接的是,一个格式化美化程序 gofmt 强制执行布局规则。它替代了需要解释的常用的“这样做而不是那样做”的的纲要。仓库中中左右Go代码,和开源界的绝大部分,都经过 gofmt 运行过。

名为Go Code Review Review Comments的文档是一些列关于程序猿经常会碰到的Go惯用语法细节的简单随笔。对于为Go项目做代码审查的人来说,这是一个方便的参考。

如何给Go库提交补丁?

库源码在仓库的 src 目录中。如果你想做一个重大意义的改动,请在着手之前在邮件列表中讨论下。

关于如何操作的更多信息请见文档Contributing to the Go project

为什么"go get"克隆仓库的时候用HTTPS?

企业经常只允许在标准TCP端口80(HTTP)和443(HTTPS)暴露对外流量,阻塞其它端口的对外流量,包括TCP端口9418(git)和TCP端口22(SSH)。当使用HTTPS来替代HTTP,git默认执行证书认证,提供针对中间人窃听和篡改等攻击的保护。go get 命令为了安全因此使用HTTPS。

Git可以配置成通过HTTPS或使用SSH替代HTTPS来进行验证。要通过HTTPS验证,你可以在 $HOME/.netrc 文件中添加一行:

machine github.com login USERNAME password APIKEY

对于Github账户,密码可以是个人访问令牌

对于给定前缀的URL,也可以将Git配置为使用SSH来代替HTTPS。例如,使用SSH来访问所有Github项目,在你的 /.gitconfig 中添加这些行:

[url "ssh://git@github.com"]
	insteadOf = https://github.com/

如何通过"go get"管理包版本?

项目之初,Go没有明确的包版本概念,但现在变了。版本控制非常复杂,尤其是在大型代码库中,而且它花了一些时间来开发一个在足够多的请夸下可以很好地适用于所有Go用户的方案。

Go 1.11版本以Go模块的形式为 go 命令添加了对包版本控制的新的实验性支持。获取更多信息,见Go 1.11 release notesgo command documentation

先不管实际中的包管理技术,“go get"和大量的Go工具链确实提供了不同引用路径的包的隔离。例如,标准库 html/templatetext/template 能够共存,尽管它们都是"template包”。这一观察结果为包作者和包用户提供了一些建议。

意向公开使用的包应该要随着它们的发展保持向后兼容。Go 1兼容性指导是一个很好的参考:不要删除导出的名字,鼓励标记复合单词等等。如果需要不同的功能,新增一个新的名字而不是修改旧的。如果需要完全中断,创建一个新的包并引用新的路径。

如果你使用了一个外部提供的包并且担心它可能以不可预知的方式发生改变,但也还没有使用Go modules,最简单的方式是把它复制到你的本地仓库中。这就是谷歌内部使用的方式,并且被 go 命令通过名为"verdoring"的技术来提供支持。这个涉及到在一个新的引用路径下存储一个依赖的副本,该路径标识其为本地副本。详见设计文档

指针和分配

函数参数什么时候传值?

就像C家族中的所有语言一样,Go语言中任何时候都是传值。也就是,一个函数总是获取到一个要传入进去的东西的副本,就像有一个分配语句来分配值给参数。比如,传一个int值给一个函数就会创建一个int副本,传一个指针就会创建一个指针的副本,而不是指针指向的数据。(看下一章关于这回如何影响方法接收的讨论)

map和slice值就像指针:它们是包含指向底层map和slice数据的指针的描述符。拷贝map或者slice值不会拷贝它指向的数据。拷贝一个接口值会创建一个接口值存储的数据。如果接口值有一个结构体,拷贝该接口值会创建结构体的副本。如果接口值有一个指针,拷贝该接口值会创建指针的副本,但也不是指向的数据。

注意这些讨论是关于这些操作的语义。实际实现可能应用优化来在不改变语义的情况下避免拷贝。

什么时候应该使用指针指向接口?

几乎从来不。用指针指向接口值仅在极少数诡异的场景发生,包括为延迟评估而掩饰接口值的类型。

传一个接口值的指针而不是接口给函数是常见的错误。编译器会报出此错误,但这种情况仍然令人困惑,因为有时候需要指针来满足接口。实际上即使一个指针指向了具体的类型后能满足一个接口,但有一个例外,指向接口的指针永远不能满足接口。

考虑下这个变量声明

var w io.Writer

打印函数 fmt.Printer 将第一个参数作为满足 io.Writer 的值——实现了具体的 Write 方法。因此我们可以写

fmt.Fprintf(w, "hello, world\n")

但是,如果我们传 w 的地址,程序将无法编译。

fmt.Fprintf(&w, "hello, world\n") // Compile-time error.

有一个例外的情况是任何值甚至是指向接口的指针,可以分配给一个空接口类型(interface{})的变量。即使如此,如果值是一个指向接口的指针,几乎确定是一个错误;结果会令人困惑。

应该在值还是在指针上定义方法?

对于不习惯用指针的程序猿来说,这两个例子之间的讨论会令人困惑,但这个场景实际上非常简单。当在一个类型上定义一个方法,接收者(上例中的s)的行为就像它是方法的参数一样。要把接收者定义成值还是指针是同样的问题,因此,就像一个函数的参数应该定义成值还是指针一样。有几个注意事项。

第一,也是最重要的,方法是否需要修改接收者?如果是,接收者必须是指针。(Slices和maps作为引用,所以它们的故事有一点微妙,但比如要在方法中修改slice的长度,接收者也必须仍是指针。)在上面的例子中,如果 pointerMethod 修改了字段 s 调用者会看得到这些改变,但是 valueMethod 调用的是调用者参数的副本(这是传值的定义),所以修改它对调用者是不可见的。

另外,在Java中方法接收者永远是指针,尽管它们的指针性质有点伪装(有人提议在语言中添加值接收器)。Go中的值接收器是不常用的。

第二是出于效率的考虑。如果接收者比较大,比如一个大 struct,使用指针接收者会更高效。

下一个是一致性。如果该类型的某些方法必须具有指针接收器,余下也应该一样,所以方法集应该是一样的,而不管类型如何使用。详见这章:方法集

对于基本类型,slice和小 structs,除非方法的语义需要指针,值接收器也很方便,而且高效和明显。

newmake 之间有什么不同?

简而言之:new 分配内存,而make初始化slice, map和channel类型。
详见relevant section of Effective Go

int在64位机器上占多大空间?

intuint的大小是特定实现的,但在特定平台上是相同的。为了可移植性,依赖于特定大小的值的代码应该使用显示大小的类型,如int64。在32位机器上编译器默认使用32位整型,而在64位机器上整型有64位。(从历史观点上看,这不总是正确的)

另一方面,浮点型标量和复杂类型总是固定大小的(没有 floatcomplex基本类型),因为程序猿在使用浮点型数字的时候应该清楚精度。用于(无类型)浮点常量的默认类型是float64。因此foo := 3.0声明了一个float64类型的变量foo。对于由(无类型)常量初始化的float32变量,必须在变量声明中显示指定类型:

var foo float32 = 3.0

或者,常量必须给一个类型转换,如:foo := float32(3.0)

如何知道变量分配到堆还是栈?

从正确性的角度来看,你并不需要知道。Go里面的每个变量只要有引用它就会一直存在。存储位置的选择实现跟语言的语义是不相关的。

在写高效率程序的时候存储位置确实有一定的影响。如果可能,Go编译器将为该函数的堆栈中分配本地变量。然而,如果编译器不能证明函数返回后变量未被引用,则编译器必须在垃圾回收堆上分配变量以避免悬空指针错误。而且,如果本地变量非常大,存储到堆空间比存储到栈空间更能说得通。

在当前的编译器中,如果变量具有其地址,则该变量是堆上分配的候选变量。但是,基本的转义分析可以识别某些情况,这些变量不会超过函数的返回值并且可以驻留在堆栈上。

为什么我的Go进程使用了那么多虚拟内存?

Go内存分配器预留了大量虚拟内存区域作为分配舞台。此虚拟内存是特定Go进程的本地内存;这个预留不会剥夺其它进程内存。

要查看分配给Go进程的实际内存,使用Unix的top命令并参考RES(Linux)或RSIZE(macOS)列。

并发

什么运算是原子的?什么是互斥的?

Go原子操作的描述可见Go内存模型文档。

syncsync/atomic提供了低级同步和原子原语。这些包对于一些简单任务非常好用,比如递增引用计数或者保证小规模互斥。

对于高级的操作,比如并发服务器间的协同,高级技术可以带来更精彩的程序,而且Go通过它的goroutines和channels来支持这一方法。比如,你可以构造你的程序对于一块指定的数据,在一个时间点只有一个goroutine对它负责。最初的Go proverb总结了这一方法。

不要通过共享内存来通信,而是通过通信来共享内存。

关于这一概念的详细讨论,见通过通信来共享内存代码走查和它的相关文章

大型并发系统都喜欢借用这些工具集。

为什么我的程序在更多CPU的时候没有运行更快?

一个程序在更多CPU的情况下是否运行更快取决于它解决的问题。Go语言提供了并发原语,如goroutines和channels,但并发只有在底层问题本质上并行的情况下才能实现并行。问题本质是顺序的则不能通过添加CPU来加快速度,但这写问题如果可以被切分成小块并并发的去执行,那有时候就可以显著加速。

有时候添加CPU会使程序慢下来。在实践中,当使用多OS线程的时候,程序花费在同步和通信上的时间比执行实际运算的时间长可能导致性能退化。这是因为线程之间传递数据涉及上下文切换,这是一重大消耗,而且这一消耗会加大的CPU使用。比如,Go说明书中的筛选质数的例子就没有有效的并行,尽管它起了很多goroutines;增加线程(CPU)数量更有可能慢下来而不是加快它。

这个话题上的更详细内容可见名为并发不是并行的讨论。

如何控制CPU的数量?

可同时执行goroutines的CPU数量是通过 GOMAXPROCS 环境变量控制的,它的默认值是CPU的核数。因此,具有并行执行潜力的程序默认在多CPU机器上实现。要改变使用并行CPU的数量,设置环境变量或者使用runtime包中类似名字的函数来配置运行时环境,以支持利用不同数量的线程。将其设置为1可消除真正并行性的可能性,迫使独立的goroutine轮流执行。

运行时环境可以分配比GOMAXPROCS值更多的线程来服务多个未完成的I/O请求。GOMAXPROCS只影响同一时刻能实际执行的goroutines数量;在系统调用中可能会被阻塞。

Go的goroutine调度器没有它需要的那样好,尽管它一直都在改进。将来,它可以更好地优化其OS线程的使用。在目前,如果有性能问题,根据每个应用程序来设置GOMAXPROCS可能有些帮助。

为什么没有goroutine ID?

Goroutines没有名字;它们只是匿名工作者。它们不对程序猿暴露唯一标识、名称或者数据结构。有些人对此感到惊讶,期望 go 语句能返回一些值用于在之后访问和控制goroutine。

Goroutines是匿名的根本原因是当编写并发代码时整个Go语言是可用的。相比之下,命名线程和goroutine的开发使用模式会限制它们使用的库的发挥。

以下是这些困难的例证。一旦有人命名一个goroutine并围绕它构建了一个模型,他就变得特殊了,然后他就会尝试把所有的计算关联到那个goroutine,而忽略了使用多个可能共享的goroutines进行处理的可能行。如果 net/http 包关联了每一个请求的状态到一个goroutine中,当处理一个请求时客户端可能不能使用更多的goroutines。

此外,对于需要在"主线程"上进行所有处理的图形系统库的经验表明,当以并发语言部署时,该方法有多么笨拙和局限。特殊线程或goroutine的存在迫使程序猿扭曲程序以避免由于无意中在错误的线程上操作而导致的崩溃和其它问题。

对于特定goroutine真正特殊的情况,该语言提供了诸如可以灵活地与其交互的channel等功能。

函数和方法

就像Go规范中说的,类型T的方法集由接收者类型T的所有方法组成,然而对应的指针类型*T则是由接收者*T和T的所有方法组成。这就意味着*T的方法集包括T的,但反过来不是。

出现区别的原因是如果一个接口值包含一个指针*T,一个方法调用可以通过引用这个指针来获取这个值,但是如果一个接口值包含一个值T,没有一个安全的方式给方法调用来获取一个指针。(这样做的话会允许方法修改接口内部值的内容,这是语言规范不允许的。)

即使在编译器可以将值地址传递给方法的情况下,如果方法修改了值,这一改动会在调用者那里丢失。例如,如果 bytes.BufferWrite 方法使用了一个值接收器而不是指针,这样的代码:

var buf bytes.Buffer
io.Copy(buf, os.Stdin)

会拷贝标准输入到buf的一个副本中,而不是buf自身。这几乎不是理想的行为。

闭包作为goroutine运行会发生什么?

当使用并发的闭包时可能会引起一些混乱。考虑一下下面的程序:

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 {
		<-
	}
}

人们可能误以为预期看到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为什么没有?:运算符?

Go中没有三元测试操作。你可以使用如下的方法来达到同样的结果:

if expr {
	n = trueVal
} else {
	n = falseVal
}

Go摒弃 ?: 的原因是这门语言的设计者发现这个运算符经常用于写出顽固、复杂的表达式。
尽管if-else格式长一点,却不可否认更清楚。一门语言只需要一种条件的控制流结构。

  • 3
    点赞
  • 0
    评论
  • 1
    收藏
  • 扫一扫,分享海报

参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值