GO语言官方问题解答

 福利开篇推荐

老铁们,你们是否平常开发有这样的问题:

1.需要公网访问内网的一个服务?
2.需要一个开发环境直接支持https,但是弄本地证书既不合法,又很麻烦?
3.本地有强劲的服务器,需要对公网提供服务,但是拉宽带既贵,又麻烦?
4.有云主机,但同时云主机需要访问内部的服务器?
5.需要本地调试微信小程序、支付功能?

老铁们,男人女人都需要的技术活 拿去不谢:远程调试,发布网站到公网演示,远程访问内网服务,微信支付调试,没有https等问题,远程桌面,点击福利推荐链接

该项目的目的是什么?

2007 年 Go 诞生时,编程世界与今天不同。生产软件通常是用 C++ 或 Java 编写的,GitHub 还不存在,大多数计算机还不是多处理器,除了 Visual Studio 和 Eclipse 之外,几乎没有可用的 IDE 或其他高级工具,更不用说在互联网上免费了。

与此同时,我们对使用我们正在使用的语言及其相关构建系统构建大型软件项目所需的过度复杂性感到沮丧。自从 C、C++ 和 Java 等语言首次开发以来,计算机的速度已经变得非常快,但编程行为本身却没有进步那么多。此外,很明显,多处理器正在变得普遍,但大多数语言对高效、安全地对其进行编程几乎没有提供帮助。

我们决定退后一步,思考随着技术的发展,未来几年哪些主要问题将主导软件工程,以及新语言如何帮助解决这些问题。例如,多核 CPU 的兴起表明,语言应该为某种并发或并行性提供一流的支持。为了使大型并发程序中的资源管理易于处理,需要垃圾收集,或者至少需要某种安全的自动内存管理。

这些考虑引发了 一系列讨论,Go 随之诞生,首先作为一组想法和需求,然后作为一种语言。总体目标是,Go 通过启用工具、自动执行代码格式化等日常任务以及消除大型代码库工作的障碍,为工作中的程序员提供更多帮助。

关于 Go 的目标以及如何满足或至少实现这些目标的更广泛的描述,请参见《 Google 的 Go:软件工程服务中的语言设计》一文。

该项目的历史是什么?

2007 年 9 月 21 日,Robert Griesemer、Rob Pike 和 Ken Thompson 开始在白板上勾勒出一种新语言的目标。几天之内,这些目标就变成了一项计划,并对其具体内容有了一个清晰的想法。设计工作与不相关的工作同时进行。到 2008 年 1 月,Ken 开始开发一个编译器来探索想法;它生成 C 代码作为其输出。到年中,该语言已成为一个全职项目,并且已经足够稳定以尝试生产编译器。 2008 年 5 月,Ian Taylor 使用规范草案独立开始了 Go 的 GCC 前端。 Russ Cox 于 2008 年底加入,帮助将语言和库从原型转变为现实。

Go 于 2009 年 11 月 10 日成为公共开源项目。来自社区的无数人贡献了想法、讨论和代码。

现在世界各地有数以百万计的 Go 程序员(地鼠),而且还在不断增加。 Go 的成功远远超出了我们的预期。

地鼠吉祥物的由来是什么?

吉祥物和标志由 Renée French设计,她还设计了 Plan 9 兔子Glenda 。一篇关于地鼠的博客文章解释了它是如何从她 几年前 用于WFMU T 恤设计的地鼠衍生出来的。徽标和吉祥物受知识 共享署名 4.0 许可证保护。

地鼠有一张 模型表 ,说明了它的特征以及如何正确地表示它们。该模型表最初是 在 2016 年 Renée 在 Gophercon 的演讲中展示的。他是Go地鼠,而不是普通的地鼠。

该语言是 Go 还是 Golang?

该语言称为 Go。 “golang”这个绰号的出现是因为该网站最初是golang.org。 (当时还没有.dev域。)不过,许多人使用 golang 名称,并且它作为标签很方便。例如,该语言的社交媒体标签是“#golang”。无论如何,该语言的名称就是简单的 Go。

旁注:虽然 官方标志 有两个大写字母,但语言名称写的是Go,而不是GO。

你为什么创造一种新语言?

Go 的诞生是出于对我们在 Google 所做工作的现有语言和环境的失望。编程变得太困难了,语言的选择是部分原因。人们必须在高效编译、高效执行或易于编程之间做出选择;这三者都没有以同一种主流语言提供。程序员可以通过转向动态类型语言(例如 Python 和 JavaScript,而不是 C++,或者在较小程度上使用 Java)来选择简单性而不是安全性和效率。

我们并不是唯一有这种担忧的人。多年来,编程语言一直处于相当平静的状态,而 Go 是最早的几种新语言之一(Rust、Elixir、Swift 等),这些语言使编程语言开发再次成为一个活跃的、几乎是主流的领域。

Go 通过尝试将解释型动态类型语言的编程简易性与静态类型编译语言的效率和安全性结合起来来解决这些问题。它还旨在更好地适应当前的硬件,支持网络和多核计算。最后,使用 Go 的目的是快速:在一台计算机上构建一个大型可执行文件最多需要几秒钟。实现这些目标使我们重新思考当前语言的一些编程方法,从而导致:组合而不是分层类型系统;支持并发和垃圾收集;严格的依赖关系规范;等等。这些无法通过库或工具很好地处理;人们需要一种新的语言。

Google 的 Go 文章 讨论了 Go 语言设计背后的背景和动机,并提供了有关此常见问题解答中的许多答案的更多详细信息。

Go的祖先是什么?

Go 大部分属于 C 家族(基本语法),有来自 Pascal/Modula/Oberon 家族(声明、包)的重要输入,加上一些受 Tony Hoare 的 CSP 启发的语言的想法,例如 Newsqueak 和 Limbo(并发)。然而,它是一种全新的语言。从各个方面来看,该语言的设计都考虑了程序员的工作以及如何使编程(至少是我们所做的编程)更有效,这意味着更有趣。

设计的指导原则是什么?

当 Go 被设计时,Java 和 C++ 是编写服务器最常用的语言,至少在 Google 是这样。我们认为这些语言需要太多的记账和重复。一些程序员的反应是转向更动态、更流畅的语言,比如 Python,但代价是效率和类型安全。我们认为应该可以用单一语言实现高效、安全和流畅。

Go 试图减少两种意义上的打字量。在整个设计过程中,我们努力减少混乱和复杂性。没有前向声明,也没有头文件;一切都被声明一次。初始化具有表现力、自动且易于使用。语法简洁,关键字较少。foo.Foo* myFoo = new(foo.Foo)通过使用:= 声明和初始化构造的简单类型派生可以减少重复 ( ) 。也许最根本的是,不存在类型层次结构:类型只是类型,它们不必声明它们的关系。这些简化使得 Go 既具有表达能力又易于理解,而且不会牺牲生产力。

另一个重要原则是保持概念正交。方法可以为任何类型实现;结构代表数据,接口代表抽象;等等。正交性使我们更容易理解事物组合时会发生什么。

用法

Google 内部使用 Go 吗?

是的。 Go 在 Google 内部的生产中被广泛使用。一个例子是 Google 的下载服务器,dl.google.com它提供 Chrome 二进制文件和其他大型可安装文件(例如apt-get 软件包)。

Go 并不是 Google 使用的唯一语言,但它是 站点可靠性工程 (SRE) 和大规模数据处理等许多领域的关键语言。它也是运行 Google Cloud 的软件的关键部分。

还有哪些公司使用 Go?

Go 在全球范围内的使用量正在不断增长,尤其是但绝不只在云计算领域。 Docker 和 Kubernetes 是用 Go 编写的几个主要云基础设施项目,但还有更多。

不过,这不仅仅是云,正如您可以从 go.dev 网站上的公司列表 以及一些 成功案例中看到的那样。此外,Go Wiki 还包含一个 定期更新的页面,其中列出了一些使用 Go 的公司。

Wiki 还有一个页面,其中包含 有关使用该语言的公司和项目的 更多成功故事的链接。

可以在同一地址空间中一起使用 C 和 Go,但这并不自然,并且可能需要特殊的接口软件。此外,将 C 与 Go 代码链接起来会放弃 Go 提供的内存安全和堆栈管理属性。有时,使用 C 库来解决问题是绝对必要的,但这样做总是会带来纯 Go 代码所不存在的风险,因此请务必小心。

如果您确实需要将 C 与 Go 一起使用,如何进行取决于 Go 编译器的实现。 “标准”编译器是 Google Go 团队支持的 Go 工具链的一部分,称为gc.此外,还有基于 GCC 的编译器 ( gccgo) 和基于 LLVM 的编译器 ( gollvm),以及越来越多的用于不同目的的不寻常编译器,有时实现语言子集,例如TinyGo

Gc使用与 C 不同的调用约定和链接器,因此不能直接从 C 程序调用,反之亦然。该cgo程序提供了“外部函数接口”的机制,以允许从 Go 代码安全调用 C 库。 SWIG 将此功能扩展到 C++ 库。

您还可以将cgoand SWIG 与gccgoand一起使用gollvm。由于它们使用传统的 ABI,因此也可以非常小心地将这些编译器的代码直接与 GCC/LLVM 编译的 C 或 C++ 程序链接。然而,安全地这样做需要了解所有相关语言的调用约定,以及从 Go 调用 C 或 C++ 时关注堆栈限制。

Go 支持哪些 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 的运行时对于语言来说更为核心,但它类似于libcC 库。

然而,重要的是要理解,Go 的运行时不包括虚拟机,例如 Java 运行时提供的虚拟机。 Go 程序提前编译为本机机器代码(或 JavaScript 或 WebAssembly,对于某些变体实现)。因此,尽管该术语经常用于描述程序运行的虚拟环境,但在 Go 中,“运行时”一词只是提供关键语言服务的库的名称。

Unicode 标识符怎么了?

在设计 Go 时,我们希望确保它不会过度以 ASCII 为中心,这意味着将标识符的空间扩展到 7 位 ASCII 的范围之外。 Go 的规则(标识符字符必须是 Unicode 定义的字母或数字)很容易理解和实现,但有限制。例如,组合字符在设计上被排除在外,并且排除了某些语言,例如梵文。

这条规则还有另一个不幸的后果。由于导出的标识符必须以大写字母开头,因此根据定义,由某些语言的字符创建的标识符不能导出。目前唯一的解决方案是使用类似的东西X日本語,这显然不能令人满意。

自该语言的最早版本以来,人们对如何最好地扩展标识符空间以适应使用其他本机语言的程序员进行了相当多的思考。到底要做什么仍然是一个活跃的讨论话题,并且该语言的未来版本在标识符的定义上可能会更加自由。例如,它可能会采用 Unicode 组织关于标识符的建议中的一些想法 。无论发生什么,都必须兼容地完成,同时保留(或者扩展)字母大小写决定标识符可见性的方式,这仍然是我们最喜欢的 Go 功能之一。

目前,我们有一个简单的规则,以后可以在不破坏程序的情况下进行扩展,该规则可以避免由于允许不明确标识符的规则而肯定会出现的错误。

为什么 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 的并发原语源自家族树的不同部分,其主要贡献是将通道作为第一类对象的强大概念。几种早期语言的经验表明,CSP 模型非常适合过程语言框架。

为什么使用 goroutine 而不是线程?

Goroutine 是让并发变得易于使用的一部分。这个想法已经存在了一段时间,即将独立执行的函数(协程)复用到一组线程上。当协程阻塞时,例如通过调用阻塞系统调用,运行时会自动将同一操作系统线程上的其他协程移动到不同的可运行线程,这样它们就不会被阻塞。程序员看不到这些,这就是重点。我们称之为 goroutine 的结果可能非常便宜:除了堆栈内存(只有几千字节)之外,它们几乎没有任何开销。

为了使堆栈变小,Go 的运行时使用可调整大小的有界堆栈。新创建的 Goroutine 被赋予几千字节,这几乎总是足够的。如果不是,运行时会自动增加(和缩小)用于存储堆栈的内存,从而允许许多 goroutine 驻留在适量的内存中。每个函数调用的 CPU 开销平均约为 3 个廉价指令。在同一地址空间中创建数十万个 goroutine 是很实用的。如果 goroutine 只是线程,那么系统资源就会以更少的数量耗尽。

为什么映射操作没有定义为原子操作?

经过长时间的讨论,我们决定映射的典型使用不需要从多个 goroutine 进行安全访问,并且在需要安全访问的情况下,映射可能是已经同步的某些较大数据结构或计算的一部分。因此,要求所有映射操作获取互斥锁会减慢大多数程序的速度,并增加少数程序的安全性。然而,这不是一个容易的决定,因为这意味着不受控制的地图访问可能会使程序崩溃。

该语言并不排除原子地图更新。当需要时,例如托管不受信任的程序时,该实现可以互锁映射访问。

仅当发生更新时地图访问才是不安全的。只要所有 goroutine 都只是读取(在映射中查找元素,包括使用循环迭代它 for range),而不是通过分配元素或执行删除来更改映射,那么它们在不同步的情况下并发访问映射是安全的。

作为正确映射使用的辅助手段,该语言的某些实现包含特殊检查,当并发执行不安全地修改映射时,该检查会在运行时自动报告。同步库中还有一种名为 的类型, sync.Map它适用于某些使用模式(例如静态缓存),尽管它不适合作为内置映射类型的一般替代品。

您能接受我的语言更改吗?

人们经常建议改进该语言—— 邮件列表 包含此类讨论的丰富历史——但这些更改很少被接受。

尽管 Go 是一个开源项目,但该语言和库受到兼容性承诺的保护,该承诺可以防止破坏现有程序的更改,至少在源代码级别如此(程序可能需要偶尔重新编译以保持最新状态)。如果您的提案违反了 Go 1 规范,我们甚至无法接受这个想法,无论其优点如何。 Go 未来的主要版本可能与 Go 1 不兼容,但关于该主题的讨论才刚刚开始,有一点是肯定的:在此过程中引入的此类不兼容性很少。此外,兼容性承诺鼓励我们为旧程序提供一条自动前进路径,以便在出现这种情况时进行适应。

即使您的提案与 Go 1 规范兼容,它也可能不符合 Go 的设计目标精神。 《 Google 的 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是否实现了接口(视情况而定):  

 
var _ I = T{} // 验证 T 是否实现了 I。
var _ I = (*T)(nil) // 验证 *T 实现了 I。

如果 

为什么类型T不满足Equal接口?

考虑这个简单的接口来表示一个可以将自身与另一个值进行比较的对象:

 

虽然在这种情况下,Go 的类型系统确实为程序员做的事情较少,但缺乏子类型使得关于接口满足的规则非常容易声明:函数的名称和签名是否与接口的名称和签名完全相同? Go 的规则也很容易高效地实现。我们认为这些好处弥补了自动类型升级的不足。

我可以将 []T 转换为 []interface{} 吗?

不直接。语言规范不允许这样做,因为这两种类型在内存中没有相同的表示形式。需要将元素单独复制到目标切片。此示例将 的 切片 转换int为 的 切片 interface{}

t := []int{1, 2, 3, 4}
s := make([]接口{}, len(t))
对于 i, v := range t {
    s[i] = v
}

如果 T1 和 T2 具有相同的基础类型,我可以将 []T1 转换为 []T2 吗?

该代码示例的最后一行无法编译。

 

在 Go 中,类型与方法紧密相关,因为每个命名类型都有一个(可能为空)方法集。一般规则是,您可以更改要转换的类型的名称(因此可能更改其方法集),但不能更改复合类型元素的名称(和方法集)。 Go 要求您明确类型转换。

为什么我的 nil 错误值不等于 nil?

在幕后,接口被实现为两个元素:类型T 和值V。 V是一个具体值,例如int、 struct或指针,而不是接口本身,并且具有类型T。例如,如果我们将int值 3 存储在接口中,则结果接口值示意性地为 ( T=intV=3)。该值V也称为接口的 动态值,因为给定的接口变量在程序执行期间 可能保存不同的值V (以及相应的类型)。T

nil仅当VT 均未设置(T=nilV未设置)时 ,接口值才是有效的,特别是,nil接口将始终保存nil类型。如果我们将nil类型指针存储*int在接口值内,则内部类型将*int与指针的值无关:( T=*intV=nil)。因此,即使内部的nil 指针值为 ,V 这样的接口值也将为非 - nil

这种情况可能会令人困惑,当nil值存储在接口值(例如error返回值)内时,就会出现这种情况:

 

如果一切顺利,该函数将返回 a nil p,因此返回值是一个持有 ( , )error的接口值。这意味着如果调用者将返回的错误与 进行比较,即使没有发生任何不好的事情,它也总是看起来好像存在错误。要向调用者返回正确的值,该函数必须返回一个显式的: T=*MyErrorV=nilnilnil errornil

 

对于返回错误的函数来说,最好始终使用error其签名中的类型(如我们上面所做的那样)而不是具体类型(例如 )*MyError,以帮助保证正确创建错误。例如, os.Open 返回一个errorEven,但如果不是nil,它始终是具体类型 *os.PathError

每当使用接口时,都会出现与此处描述的类似情况。请记住,如果接口中存储了任何具体值,则该接口将不会被存储nil。有关更多信息,请参阅 反射定律

为什么没有像 C 中那样的未标记联合?

未标记的联合会违反 Go 的内存安全保证。

为什么 Go 没有变体类型?

变体类型(也称为代数类型)提供了一种方法来指定值可以采用一组其他类型中的一种,但只能采用这些类型。系统编程中的一个常见示例会指定错误是网络错误、安全错误或应用程序错误,并允许调用者通过检查错误类型来区分问题的根源。另一个例子是语法树,其中每个节点可以是不同的类型:声明、语句、赋值等等。

我们考虑过向 Go 添加变体类型,但经过讨论后决定将它们排除在外,因为它们与接口以令人困惑的方式重叠。如果变体类型的元素本身就是接口,会发生什么?

此外,该语言已经涵盖了一些变体类型所解决的问题。使用接口值来保存错误并使用类型开关来区分情况,错误示例很容易表达。语法树示例也是可行的,尽管不是那么优雅。

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

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

 

该方法将满足

 

因为Value实现了空接口。在 Go 中方法类型必须完全匹配,因此Value没有实现Copyable. Go 将类型的功能(其方法)的概念与类型的实现分开。如果两个方法返回不同的类型,则它们没有做相同的事情。想要协变结果类型的程序员通常尝试通过接口来表达类型层次结构。在 Go 中,接口和实现之间的清晰分离更为自然。

价值观

为什么 Go 不提供隐式数值转换?

C 中数字类型之间自动转换的便利性被它引起的混乱所抵消。表达式何时无符号?价值有多大?会溢出吗?结果是否可移植,独立于执行它的机器?它还使编译器变得复杂; C 的“常规算术转换”不容易实现,并且在不同体系结构之间不一致。出于可移植性的原因,我们决定让事情变得清晰和直接,但代价是在代码中进行一些显式转换。不过,Go 中常量的定义——不受符号和大小注释影响的任意精度值——大大改善了问题。

一个相关的细节是,与 C 不同, 即使是 64 位类型,int和也是不同的类型。类型 是通用的;如果你关心一个整数包含多少位,Go 鼓励你明确地表达出来。 int64intint

常量在 Go 中如何工作?

尽管 Go 对不同数值类型的变量之间的转换有严格要求,但该语言中的常量要灵活得多。诸如233.14159 和之math.Pi 类的文字常量占据一种理想的数字空间,具有任意精度并且没有上溢或下溢。例如,math.Pi源代码中 的值被指定为 63 位十进制数字,涉及该值的常量表达式的精度超出了 a 所能float64容纳的范围。只有当常量或常量表达式被分配给变量(程序中的内存位置)时,它才会成为具有通常的浮点属性和精度的“计算机”数字。

此外,由于它们只是数字,而不是键入的值,因此 Go 中的常量比变量可以更自由地使用,从而缓解了严格转换规则带来的一些尴尬。人们可以写出诸如这样的表达式

 

编译器不会抱怨,因为理想的数字2 可以安全准确地转换为 afloat64以便调用math.Sqrt.

题为“常量” 的博客文章 更详细地探讨了这个主题。

为什么要内置地图?

字符串的原因相同:它们是一种非常强大且重要的数据结构,提供具有语法支持的出色实现使编程变得更加愉快。我们相信 Go 的地图实现足够强大,足以满足绝大多数用途。如果特定的应用程序可以从自定义实现中受益,则可以编写一个自定义实现,但在语法上不会那么方便;这似乎是一个合理的权衡。

为什么映射不允许切片作为键?

映射查找需要一个相等运算符,而切片不实现该运算符。它们没有实现相等性,因为此类类型没有很好地定义相等性;有多种考虑因素,涉及浅比较与深比较、指针比较与值比较、如何处理递归类型等等。我们可能会重新讨论这个问题——实现切片相等不会使任何现有程序无效——但如果不清楚切片相等的含义,那么暂时忽略它会更简单。

结构体和数组定义了相等性,因此它们可以用作映射键。

为什么映射、切片和通道是引用,而数组是值?

关于这个话题有很多历史。早期,地图和通道在语法上是指针,不可能声明或使用非指针实例。此外,我们还纠结于数组应该如何工作。最终我们认为指针和值的严格分离使得该语言更难使用。更改这些类型以充当对关联的共享数据结构的引用解决了这些问题。这一变化给语言增加了一些令人遗憾的复杂性,但对可用性产生了很大影响:Go 被引入后成为一种更高效、更舒适的语言。

编写代码

图书馆是如何记录的?

为了从命令行访问文档, go工具有一个 doc 子命令,它提供了声明、文件、包等文档的文本界面。

全局包发现页面 pkg.go.dev/pkg/。运行一个服务器,从网络上任何地方的 Go 源代码中提取包文档,并将其作为 HTML 提供,并包含指向声明和相关元素的链接。这是了解现有 Go 库的最简单方法。

在项目的早期,有一个类似的程序,godoc也可以运行它来提取本地计算机上文件的文档; pkg.go.dev/pkg/本质上是一个后代。另一个后代是 可以在本地运行pkgsite 的命令,例如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) 和 TCP 端口 22 (SSH)。当使用 HTTPS 而不是 HTTP 时,git默认强制执行证书验证,从而防止中间人、窃听和篡改攻击。因此,为了安全起见,该go get命令使用 HTTPS。

Git可以配置为通过 HTTPS 进行身份验证或使用 SSH 代替 HTTPS。要通过 HTTPS 进行身份验证,您可以$HOME/.netrc向 git 查阅的文件 添加一行:

机器 github.com 登录USERNAME密码APIKEY

对于 GitHub 帐户,密码可以是 个人访问令牌

Git还可以配置为使用 SSH 代替 HTTPS 来匹配给定前缀的 URL。例如,要使用 SSH 进行所有 GitHub 访问,请将这些行添加到您的~/.gitconfig

[网址“ssh://git@github.com/”]
	相反= https://github.com/

我应该如何使用“go get”管理软件包版本?

Go 工具链有一个内置系统,用于管理相关包的版本集,称为模块。模块在Go 1.11中引入,并自1.14起就已准备好用于生产使用。

要使用模块创建项目,请运行go mod init.此命令创建一个go.mod跟踪依赖项版本的文件。

go mod init 示例/项目

要添加、升级或降级依赖项,请运行go get

去获取 golang.org/x/text@v0.3.5

有关入门的更多信息, 请参阅教程:创建模块。

有关管理模块依赖关系的指南, 请参阅开发模块。

模块内的包应在发展过程中保持向后兼容性,遵循导入兼容性规则

如果旧包和新包具有相同的导入路径,则
新包必须向后兼容旧包。

Go 1 兼容性指南 是一个很好的参考:不要删除导出的名称,鼓励标记复合文字,等等。如果需要不同的功能,请添加新名称而不是更改旧名称。

模块通过语义版本控制和语义导入版本控制对此进行编码。如果需要中断兼容性,请以新的主要版本发布模块。主要版本 2 及更高版本的模块需要主要版本后缀作为其路径的一部分(例如/v2)。这保留了导入兼容性规则:模块的不同主要版本中的包具有不同的路径。

指针和分配

函数参数什么时候按值传递?

与 C 家族中的所有语言一样,Go 中的所有内容都是按值传递的。也就是说,函数总是获取所传递内容的副本,就好像有一个赋值语句将值分配给参数一样。例如,将int值传递给函数会生成 的副本int,而传递指针值会生成指针的副本,但不会复制它所指向的数据。 (有关这如何影响方法接收者的讨论, 请参阅后面的部分。)

映射和切片值的行为类似于指针:它们是包含指向底层映射或切片数据的指针的描述符。复制地图或切片值不会复制它指向的数据。复制接口值会复制存储在接口值中的内容。如果接口值包含一个结构,则复制接口值会生成该结构的副本。如果接口值包含一个指针,则复制接口值会复制该指针,但不会复制它所指向的数据。

请注意,此讨论是关于操作的语义。实际实现可以应用优化来避免复制,只要优化不改变语义即可。

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

几乎从不。指向接口值的指针仅在罕见且棘手的情况下出现,涉及隐藏接口值的类型以延迟求值。

将指向接口值的指针传递给需要接口的函数是一个常见的错误。编译器会抱怨这个错误,但情况仍然会令人困惑,因为有时 需要一个指针来满足接口。我们的见解是,尽管指向具体类型的指针可以满足接口,但除了一个例外,指向接口的指针永远无法满足接口

考虑变量声明,

var w io.Writer

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

fmt.Fprintf(w, "你好,世界\n")

然而,如果我们传递 的地址w,程序将无法编译。

fmt.Fprintf(&w, "hello, world\n") // 编译时错误。

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

我应该在值或指针上定义方法吗?

func (s *MyStruct)pointerMethod() { } // 指针上的方法
func (s MyStruct) valueMethod() { } // 值方法

对于不习惯指针的程序员来说,这两个例子之间的区别可能会令人困惑,但情况实际上非常简单。在类型上定义方法时,接收者(s在上面的示例中)的行为与该方法的参数完全相同。将接收者定义为值还是指针是同一个问题,就像函数参数应该是值还是指针一样。有几个考虑因素。

首先,也是最重要的,该方法是否需要修改接收者?如果是,则接收者必须是一个指针。 (切片和映射充当引用,因此它们的故事更加微妙,但例如要更改方法中切片的长度,接收者仍然必须是指针。)在上面的示例中,如果pointerMethod修改 的字段s,调用者将看到这些更改,但valueMethod 使用调用者参数的副本进行调用(这是传递值的定义),因此它所做的更改对调用者来说是不可见的。

顺便说一句,在 Java 中,方法接收器一直是指针,尽管它们的指针本质有些伪装(最近的发展正在将值接收器引入 Java)。 Go 中的值接收者是不寻常的。

其次是效率的考虑。如果接收器很大,struct例如很大,那么使用指针接收器可能会更便宜。

接下来是一致性。如果该类型的某些方法必须具有指针接收器,则其余方法也应该具有指针接收器,因此无论如何使用该类型,方法集都是一致的。有关详细信息,请参阅有关方法集的部分 。

对于基本类型、切片和小型等类型structs,值接收器非常便宜,因此除非方法的语义需要指针,否则值接收器是高效且清晰的。

新的和制造的有什么区别?

简而言之:new分配内存,同时make初始化切片、映射和通道类型。

有关更多详细信息, 请参阅《Effective Go》的相关部分。

int64 位机器上的 的大小是多少?

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

另一方面,浮点标量和复杂类型始终具有大小(没有floatcomplex基本类型),因为程序员在使用浮点数时应该注意精度。用于(无类型)浮点常量的默认类型是float64。这样就foo := 3.0声明了一个foo 类型为 的变量float64。对于float32由(无类型)常量初始化的变量,必须在变量声明中显式指定变量类型:

var foo float32 = 3.0

或者,必须为常量指定一个带有转换的类型,如 中所示 foo := float32(3.0)

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

从正确性的角度来看,您不需要知道。 Go 中的每个变量只要有引用就存在。实现选择的存储位置与语言的语义无关。

存储位置确实对编写高效程序有影响。如果可能,Go 编译器将在该函数的堆栈帧中分配该函数的本地变量。但是,如果编译器无法证明函数返回后该变量未被引用,则编译器必须在垃圾收集堆上分配该变量以避免悬空指针错误。此外,如果局部变量非常大,将其存储在堆上而不是堆栈上可能更有意义。

在当前的编译器中,如果一个变量的地址被获取,那么该变量就是在堆上分配的候选者。然而,基本的逃逸分析可以识别某些情况,即此类变量不会在函数返回之后继续存在,而是可以驻留在堆栈上。

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

Go 内存分配器保留了一大块虚拟内存作为分配的场所。该虚拟内存是特定 Go 进程的本地内存;保留不会剥夺其他进程的内存。

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

并发性

哪些操作是原子的?那么互斥体呢?

Go 中操作原子性的描述可以在Go 内存模型文档中找到。

低级同步和原子原语在 sync和 sync/atomic 包中可用。这些包适用于简单的任务,例如增加引用计数或保证小规模互斥。

对于更高级别的操作,例如并发服务器之间的协调,更高级别的技术可以产生更好的程序,Go 通过其 goroutine 和通道支持这种方法。例如,您可以构建您的程序,以便一次只有一个 goroutine 负责特定的数据。最初的Go 谚语总结了这种方法 ,

不要通过共享内存进行通信。相反,通过通信来共享内存。

有关此概念的详细讨论, 请参阅通过通信共享内存代码演练及其 相关文章。

大型并发程序可能会借鉴这两个工具包。

为什么我的程序在 CPU 数量增多时运行速度不快?

程序在 CPU 数量越多时运行速度是否更快取决于它要解决的问题。 Go 语言提供了并发原语,例如 goroutine 和通道,但只有当底层问题本质上是并行时,并发才可以实现并行性。本质上是顺序的问题不能通过添加更多的 CPU 来加速,而那些可以分解为可以并行执行的部分的问题可以加速,有时甚至是显着的加速。

有时添加更多 CPU 会减慢程序速度。实际上,在使用多个操作系统线程时,花费更多时间进行同步或通信而不是进行有用计算的程序可能会遇到性能下降。这是因为在线程之间传递数据涉及到切换上下文,这会产生巨大的成本,并且随着 CPU 的增加,成本会增加。例如, Go 规范中的素数筛示例 虽然启动了许多 goroutine,但没有显着的并行性;增加线程 (CPU) 数量更有可能减慢速度而不是加快速度。

有关此主题的更多详细信息,请参阅题为“并发不是并行” 的演讲 。

如何控制CPU的数量?

同时执行 goroutine 的可用 CPU 数量由GOMAXPROCSshell 环境变量控制,其默认值为可用的 CPU 核心数。因此,具有并行执行潜力的程序应该默认在多 CPU 计算机上实现它。要更改要使用的并行 CPU 的数量,请设置环境变量或使用运行时包的类似名称的 函数 来配置运行时支持以利用不同数量的线程。将其设置为 1 消除了真正并行的可能性,强制独立的 goroutine 轮流执行。

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

Go 的 goroutine 调度程序在平衡 goroutine 和线程方面做得很好,甚至可以抢占 goroutine 的执行,以确保同一线程上的其他 goroutine 不会挨饿。然而,它并不完美。如果您发现性能问题,GOMAXPROCS针对每个应用程序进行设置可能会有所帮助。

为什么没有goroutine ID?

Goroutines 没有名字;他们只是无名工人。它们不向程序员公开唯一的标识符、名称或数据结构。有些人对此感到惊讶,期望该go 语句返回一些可用于稍后访问和控制 goroutine 的项目。

Goroutine 是匿名的根本原因是为了在编写并发代码时可以使用完整的 Go 语言。相比之下,命名线程和 goroutine 时形成的使用模式可能会限制使用它们的库可以执行的操作。

这是困难的例证。一旦人们命名了一个 goroutine 并围绕它构建了一个模型,它就会变得特别,并且人们会试图将所有计算与该 goroutine 相关联,而忽略使用多个可能共享的 goroutine 进行处理的可能性。如果net/http包将每个请求状态与一个 goroutine 相关联,那么客户端在处理请求时将无法使用更多 goroutine。

此外,使用库(例如要求所有处理都在“主线程”上进行的图形系统的库)的经验表明,当以并发语言部署时,该方法会变得多么笨拙和限制。特殊线程或 goroutine 的存在迫使程序员扭曲程序,以避免因无意中操作错误的线程而导致崩溃和其他问题。

对于那些特定 goroutine 真正特殊的情况,该语言提供了诸如通道之类的功能,可以以灵活的方式与其交互。

函数和方法

为什么 T 和 *T 有不同的方法集?

正如Go规范所说,一个类型的方法T集由所有接收者类型为 的方法组成T,而对应的指针类型的*T方法集由所有接收者为*T或 的 方法组成T。这意味着 的方法集*T 包括 的方法集T,但反之则不然。

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

即使编译器可以将值的地址传递给方法,如果方法修改了值,则调用者中的更改也会丢失。例如,如果Write方法 bytes.Buffer 使用值接收器而不是指针,则以下代码:

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

会将标准输入复制到的副本buf中,而不是复制到buf其自身中。这几乎从来都不是理想的行为。

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

由于循环变量的工作方式,在 Go 版本 1.22 之前(请参阅本节末尾的更新),在使用并发闭包时可能会出现一些混乱。考虑以下程序:

函数主() {
    完成 := make(chan bool)

    值 := []字符串{"a", "b", "c"}
    对于 _, v := 范围值 {
        去函数(){
            fmt.Println(v)
            完成 <- 正确
        }()
    }

    // 等待所有 goroutine 完成后再退出
    for _ = 范围值 {
        <-完成
    }
}

人们可能会错误地期望将其a, b, c视为输出。您可能会看到的是c, c, c.这是因为循环的每次迭代都使用变量的相同实例v,因此每个闭包共享该单个变量。当闭包运行时,它会打印执行v时的值,但自 goroutine 启动以来可能已被修改。为了帮助在发生此问题和其他问题之前检测到它们,请运行 . fmt.Printlnvgo vet

v要在启动时将 的当前值绑定到每个闭包,必须修改内部循环以在每次迭代时创建一个新变量。一种方法是将变量作为参数传递给闭包:

    对于 _, v := 范围值 {
        去功能(u字符串){
            fmt.Println( u )
            完成 <- 正确
        }()
    }

在此示例中, 的值v作为参数传递给匿名函数。然后该值可以在函数内部作为变量访问u

更简单的是创建一个新变量,使用看似奇怪但在 Go 中运行良好的声明样式:

    for _, v := 范围值 {
         v := v // 创建一个新的 'v'。
        去函数(){
            fmt.Println( v )
            完成 <- 正确
        }()
    }

回想起来,该语言的这种行为(没有为每次迭代定义一个新变量)被认为是一个错误,并且已在Go 1.22中得到解决,它确实为每次迭代创建了一个新变量,从而消除了这个问题。

控制流

为什么 Go 没有?:运算符?

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

如果表达式 {
    n = 真值
} 别的 {
    n = 假值
}

Go 中没有出现的原因?:是该语言的设计者发现该操作过于频繁地用于创建难以理解的复杂表达式。表格if-else虽然更长,但无疑更清晰。一种语言只需要一个条件控制流结构。

类型参数

为什么 Go 有类型参数?

类型参数允许所谓的泛型编程,其中函数和数据结构是根据稍后在使用这些函数和数据结构时指定的类型来定义的。例如,它们使得编写一个返回任何有序类型的两个值中的最小值的函数成为可能,而不必为每种可能的类型编写单独的版本。有关示例的更深入解释,请参阅博客文章 为什么泛型?

Go 中的泛型是如何实现的?

编译器可以选择是否单独编译每个实例化,或者是否将类似的实例化编译为单个实现。单一实现方法类似于带有接口参数的函数。不同的编译器会针对不同的情况做出不同的选择。标准 Go 编译器通常为具有相同形状的每个类型参数发出一个实例化,其中形状由类型的属性确定,例如它包含的指针的大小和位置。未来的版本可能会尝试在编译时间、运行时效率和代码大小之间进行权衡。

Go 中的泛型与其他语言中的泛型相比如何?

所有语言的基本功能都是相似的:可以使用稍后指定的类型来编写类型和函数。也就是说,存在一些差异。

爪哇

在 Java 中,编译器在编译时检查泛型类型,但在运行时删除这些类型。这称为 类型擦除。例如,在编译时称为的 Java 类型将在运行时List<Integer>变为非泛型类型。List这意味着,例如,当使用 Java 形式的类型反射时,无法区分 type 的值List<Integer>和 type 的值List<Float>。在 Go 中,泛型类型的反射信息包括完整的编译时类型信息。

Java 使用类型通配符(例如List<? extends Number> 或 )List<? super Number>来实现通用协变和逆变。 Go 没有这些概念,这使得 Go 中的泛型类型变得简单得多。

C++

传统上,C++ 模板不会对类型参数强制任何约束,尽管 C++20 通过 概念支持可选约束。在 Go 中,约束对于所有类型参数都是强制的。 C++20 概念表示为必须使用类型参数进行编译的小代码片段。 Go 约束是定义所有允许的类型参数集的接口类型。

C++支持模板元编程; Go 则不然。实际上,所有 C++ 编译器都会在实例化每个模板时对其进行编译;如上所述,Go 可以并且确实对不同的实例化使用不同的方法。

Rust 版本的约束称为特征边界。在 Rust 中,特征边界和类型之间的关联必须显式定义,无论是在定义特征边界的 crate 中还是在定义类型的 crate 中。在 Go 中,类型参数隐式满足约束,就像 Go 类型隐式实现接口类型一样。 Rust 标准库定义了比较或加法等操作的标准特征; Go 标准库没有,因为这些可以通过接口类型在用户代码中表达。一个例外是 Go 的comparable预定义接口,它捕获类型系统中无法表达的属性。

Python

Python 不是一种静态类型语言,因此可以合理地说,所有 Python 函数在默认情况下始终是通用的:它们始终可以使用任何类型的值进行调用,并且在运行时会检测到任何类型错误。

为什么 Go 对类型参数列表使用方括号?

Java 和 C++ 对类型参数列表使用尖括号,就像 JavaList<Integer>和 C++ 中一样std::vector<int>。然而,该选项不适用于 Go,因为它会导致语法问题:在解析函数内的代码时,例如v := F<T>,在看到 时,<我们看到的是实例化还是使用运算符的表达式是不明确的<。如果没有类型信息,这个问题很难解决。

例如,考虑这样的语句

    a、b = w < x、y > (z)

如果没有类型信息,则无法确定赋值的右侧是否是一对表达式(w < x 和y > z),或者是否是返回两个结果值的泛型函数实例化和调用((w<x, y>)(z))。

Go 的一个关键设计决策是在没有类型信息的情况下也可以进行解析,这在使用尖括号进行泛型时似乎是不可能的。

Go 在使用方括号方面并不是唯一的或原创的;还有其他语言(例如 Scala)也使用方括号表示通用代码。

为什么 Go 不支持带类型参数的方法?

Go 允许泛型类型拥有方法,但是除了接收者之外,这些方法的参数不能使用参数化类型。类型的方法决定了该类型实现的接口,但尚不清楚这如何与泛型类型方法的参数化参数一起使用。它需要在运行时实例化函数或为每个可能的类型参数实例化每个泛型函数。这两种方法似乎都不可行。有关更多详细信息(包括示例),请参阅 提案。使用带有类型参数的顶级函数,或者将类型参数添加到接收者类型,而不是带有类型参数的方法。

为什么我不能对参数化类型的接收者使用更具体的类型?

泛型类型的方法声明是使用包含类型参数名称的接收器编写的。也许由于在调用站点指定类型的语法相似,有些人认为这提供了一种机制,通过在接收器中命名特定类型来生成为某些类型参数定制的方法,例如string

类型 S[T 任意] 结构 { f T }

func (s S[字符串]) Add(t 字符串) 字符串 {
    返回 sf + t
}

这会失败,因为编译器将该单词string视为方法中类型参数的名称。编译器错误消息将类似于“ operator + not defined on s.f (variable of type string)”。这可能会令人困惑,因为该+运算符在预声明的类型上工作正常string,但对于该方法,声明已覆盖 的定义string,并且该运算符不适用于该不相关的版本string。像这样覆盖预先声明的名称是有效的,但这样做是一件奇怪的事情,而且经常是一个错误。

为什么编译器无法推断程序中的类型参数?

在很多情况下,程序员可以轻松地看到泛型类型或函数的类型参数必须是什么,但语言不允许编译器推断它。有意限制类型推断,以确保推断出的类型不会出现任何混淆。其他语言的经验表明,在阅读和调试程序时,意外的类型推断可能会导致相当大的混乱。始终可以指定要在调用中使用的显式类型参数。未来,只要规则保持简单明了,就可能支持新的推理形式。

封装和测试

如何创建多文件包?

将包的所有源文件单独放在一个目录中。源文件可以随意引用不同文件中的项目;不需要前向声明或头文件。

除了被分割成多个文件之外,该包将像单文件包一样进行编译和测试。

如何编写单元测试?

_test.go创建一个以与包源相同的目录 结尾的新文件。在该文件内,import "testing" 编写以下形式的函数

func TestFoo(t *testing.T) {
    ...
}

go test在该目录中 运行。该脚本查找Test函数、构建测试二进制文件并运行它。

有关更多详细信息, 请参阅如何编写 Go 代码文档、testing包和子命令。go test

我最喜欢的测试辅助函数在哪里?

Go的标准testing包使得编写单元测试变得很容易,但它缺乏其他语言的测试框架提供的功能,例如断言函数。本文档的前面部分解释了为什么 Go 没有断言,并且相同的论点也适用于assert测试中的使用。正确的错误处理意味着在一个测试失败后让其他测试运行,以便调试失败的人能够全面了解问题所在。报告isPrime给出 2、3、5 和 7(或 2、4、8 和 16)错误答案的测试比报告isPrime给出 2 错误答案因此不再进行测试更有用 被运行。触发测试失败的程序员可能不熟悉失败的代码。现在,在测试中断时,编写良好的错误消息所投入的时间会得到回报。

一个相关的一点是,测试框架往往会发展成自己的迷你语言,带有条件、控制和打印机制,但 Go 已经具备了所有这些功能;为什么要重新创建它们?我们宁愿用 Go 编写测试;它减少了一种需要学习的语言,并且该方法使测试简单易懂。

如果编写良好错误所需的额外代码量似乎重复且令人难以承受,那么如果采用表驱动,迭代数据结构中定义的输入和输出列表(Go 对数据结构文字具有出色的支持),测试可能会效果更好。编写好的测试和好的错误消息的工作将被分摊到许多测试用例上。标准 Go 库充满了说明性示例,例如 的格式化测试fmt

为什么X 不在标准库中?

标准库的目的是支持运行时库、连接到操作系统并提供许多 Go 程序所需的关键功能,例如格式化 I/O 和网络。它还包含对 Web 编程很重要的元素,包括加密以及对 HTTP、JSON 和 XML 等标准的支持。

没有明确的标准来定义包含的内容,因为很长一段时间以来,这是唯一的Go 库。然而,有一些标准定义了今天要添加的内容。

标准库中新增的内容很少,而且纳入的门槛很高。标准库中包含的代码需要承担大量的持续维护成本(通常由原作者以外的人承担),受到Go 1 兼容性承诺的约束 (阻止修复 API 中的任何缺陷),并且受到 Go 版本的约束安排,防止错误修复快速提供给用户。

大多数新代码应该位于标准库之外,并且可以通过go工具的 go get命令进行访问。这样的代码可以有自己的维护者、发布周期和兼容性保证。用户可以在pkg.go.dev找到软件包并阅读其文档 。

尽管标准库中有一些并不真正属于的部分,例如log/syslog,但由于 Go 1 兼容性承诺,我们继续维护库中的所有内容。但我们鼓励大多数新代码驻留在其他地方。

执行

使用什么编译器技术来构建编译器?

Go 有多种生产编译器,还有许多其他针对不同平台的编译器正在开发中。

默认编译器gc包含在 Go 发行版中,作为命令支持的一部分go 。 Gc最初是用 C 编写的,因为引导的困难 - 你需要一个 Go 编译器来设置 Go 环境。但事情已经取得了进展,自从 Go 1.5 版本发布以来,编译器已经是 Go 程序了。使用自动翻译工具将编译器从 C 转换为 Go,如本设计文档 和演讲中所述。因此,编译器现在是“自托管”的,这意味着我们需要面对引导问题。解决方案是安装一个可用的 Go,就像通常安装一个可用的 C 一样。这里和 这里描述了如何从源代码创建新的 Go 环境的故事。

Gc是用 Go 编写的,带有递归下降解析器,并使用自定义加载器(也是用 Go 编写但基于 Plan 9 加载器)来生成 ELF/Mach-O/PE 二进制文件。

编译Gccgo器是用 C++ 编写的前端,带有与标准 GCC 后端耦合的递归下降解析器。实验性 LLVM 后端使用相同的前端。

在项目开始时,我们考虑使用 LLVM, gc但认为它太大且速度太慢,无法满足我们的性能目标。回想起来更重要的是,从 LLVM 开始会使引入一些 ABI 和相关更改变得更加困难,例如 Go 需要但不属于标准 C 设置的一部分的堆栈管理。

事实证明,Go 是一种很好的语言,可以用来实现 Go 编译器,尽管这不是它最初的目标。从一开始就不是自托管,这使得 Go 的设计能够专注于其最初的用例,即网络服务器。如果我们决定 Go 应该尽​​早编译自己,我们最终可能会得到一种更适合编译器构建的语言,这是一个有价值的目标,但不是我们最初的目标。

尽管gc有自己的实现,但go/parser包中提供了本机词法分析器和解析器,并且还有一个本机类型检查器。编译gc器使用这些库的变体。

运行时支持是如何实现的?

同样,由于引导问题,运行时代码最初主要是用 C 编写的(带有少量汇编程序),但后来被翻译为 Go(除了一些汇编程序位)。 Gccgo的运行时支持使用glibc.编译gccgo器使用一种称为分段堆栈的技术来实现 goroutine,该技术受到最近对 gold 链接器的修改的支持。 Gollvm同样是建立在相应的LLVM基础设施之上。

为什么我的小程序是这么大的二进制文件?

工具链中的链接器gc默认创建静态链接的二进制文件。因此,所有 Go 二进制文件都包含 Go 运行时,以及支持动态类型检查、反射甚至恐慌时堆栈跟踪所需的运行时类型信息。

在 Linux 上使用 gcc 静态编译和链接的简单 C“hello, world”程序约为 750 kB,包括 printf.一个等效的 Go 程序 fmt.Printf重达几兆字节,但包含更强大的运行时支持以及类型和调试信息。

编译的 Go 程序gc可以与标志链接-ldflags=-w以禁用 DWARF 生成,从二进制文件中删除调试信息,但不会损失其他功能。这可以大大减少二进制文件的大小。

我可以停止这些关于我未使用的变量/导入的抱怨吗?

未使用的变量的存在可能表明存在错误,而未使用的导入只会减慢编译速度,随着程序随着时间的推移积累代码和程序员,这种影响可能会变得很大。由于这些原因,Go 拒绝编译带有未使用的变量或导入的程序,用短期的便利性来换取长期的构建速度和程序的清晰度。

不过,在开发代码时,临时创建这些情况是很常见的,并且在程序编译之前必须将它们编辑掉可能会很烦人。

有些人要求编译器选项来关闭这些检查或至少将其减少为警告。不过,这样的选项尚未添加,因为编译器选项不应影响语言的语义,并且 Go 编译器不会报告警告,只会报告阻止编译的错误。

没有警告有两个原因。首先,如果值得抱怨,就值得在代码中修复。 (相反,如果不值得修复,那就不值得一提。)其次,让编译器生成警告会鼓励实现对可能使编译产生噪音的弱情况发出警告,从而掩盖 应该修复的真正错误。

不过,解决这种情况很容易。使用空白标识符可以让未使用的东西在开发过程中保留下来。

导入“未使用”

// 该声明将导入标记为通过引用来使用
// 包裹中的物品。
var _ =used.Item // TODO:提交前删除!

函数主() {
    debugData := debug.Profile()
    _ = debugData // 仅在调试期间使用。
    ....
}

如今,大多数 Go 程序员都使用goimports 工具 ,它会自动重写 Go 源文件以获得正确的导入,从而消除了实践中未使用的导入问题。该程序可以轻松连接到大多数编辑器和 IDE,以便在编写 Go 源文件时自动运行。gopls如上所述, 功能也是内置的。

为什么我的病毒扫描软件认为我的 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 如此不同?

除了声明语法之外,差异并不大,并且源于两个愿望。首先,语法应该很轻松,没有太多强制性关键字、重复或奥秘。其次,该语言被设计为易于分析,无需符号表即可解析。这使得构建调试器、依赖分析器、自动文档提取器、IDE 插件等工具变得更加容易。 C 及其后代在这方面是出了名的困难。

为什么声明是倒退的?

如果您习惯使用 C,它们只是倒退。在 C 中,概念是变量被声明为表示其类型的表达式,这是一个好主意,但是类型和表达式语法不能很好地混合,并且结果可能会令人困惑;考虑函数指针。 Go 主要分离表达式和类型语法,这简化了事情(使用*指针前缀是证明规则的例外)。在C中,声明

    整数*a,b;

声明a为指针但不是b;在围棋中

    var a, b *int

声明两者都是指针。这样就更清晰、更规律了。此外,:=简短的声明形式认为完整的变量声明应该呈现相同的:=顺序

    var a uint64 = 1

具有相同的效果

    一个 := uint64(1)

通过为类型提供独特的语法(不仅仅是表达式语法),还可以简化解析。诸如func 和 之类的关键字chan让事情变得清晰。

有关更多详细信息, 请参阅有关Go 声明语法的 文章 。

为什么没有指针运算?

安全。如果没有指针算术,就可以创建一种永远无法派生出错误成功的非法地址的语言。编译器和硬件技术已经发展到使用数组索引的循环可以与使用指针算术的循环一样高效。此外,缺少指针运算可以简化垃圾收集器的实现。

为什么是++and--语句而不是表达式?为什么是后缀而不是前缀?

如果没有指针运算,前置和后置增量运算符的便利性就会下降。通过将它们从表达式层次结构中完全删除,表达式语法得到了简化,并且围绕++and的求值顺序-- (考虑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 采用了更传统的方法,通过垃圾收集来解决对象生命周期,并且仅通过垃圾收集来解决。

当前的实现是标记和清除收集器。如果机器是多处理器,则收集器在单独的 CPU 内核上与主程序并行运行。近年来,收集器方面的主要工作已将暂停时间缩短至亚毫秒范围,即使对于大型堆也是如此,几乎消除了网络服务器中垃圾收集的主要反对意见之一。工作仍在继续完善算法,进一步减少开销和延迟,并探索新方法。 Go 团队的 Rick Hudson 在2018 年 ISMM 主题演讲 中描述了迄今为止的进展并提出了一些未来的方法。

关于性能主题,请记住,Go 为程序员提供了对内存布局和分配的相当大的控制权,这比垃圾收集语言中的典型控制要多得多。细心的程序员可以通过良好地使用语言来显着减少垃圾收集开销;请参阅有关分析 Go 程序的文章 以获取有效示例,其中包括 Go 分析工具的演示。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值