译文 100 Go mistakes 之 Go 易学难精

yudotyang · 2021年07月20日 · 464 次阅读
本帖已被设为精华帖!

本文是对《100 Go Mistackes:How to Avoid Them》一书的翻译。因翻译水平有限,难免存在翻译准确性问题,敬请谅解。

本文首发于微信公众号 “Go 学堂”,想获取更多信息,请关注该公众号。

本章涵盖内容:

  • 提醒我们是什么让 Go 成为了一种高效、可扩展的和多产的语言
  • 探究 Go 为什么易学难精

在过去的几十年里,编程有了很大的发展。大多数现代计算机系统已经不再是由一个人编写了,而是由很多程序员组成的组织,甚至是由数千人来完成的。我们的代码必须要具备可读性,表达性以及可维护性,以保证系统经久耐用。同时,在当今快速发展的世界中,最大限度的提高敏捷行并缩短上市时间对大多数组织来说是至关重要的。编程也应该顺应这种趋势,以确保软件工程师在阅读、编写和维护代码时尽可能高效。

为了应对这些挑战,谷歌在 2007 年构思了 Go 编程语言。从那时起,很多组织采用该语言来支持各种场景:API,自动化,数据库,CLI(命令行接口)等等。Go 至今被很多人认为是云语言。Go 成功的一个关键因素是因为它是一门简单的编程语言。一个新手可以在不到一天的时间内就能学习该语言的所有主要功能。然而,正如我们在这章看到的,简单易学并不一定意味着容易掌握。

这个想法引导我写了一本书,帮助研发人员最有效的使用 Go 编程语言。然而,有一个问题:你为什么要读一本关于 Go 常见错误的书?为什么不通过一本可以深入研究不同主题的书来加深你的知识呢?神经科学家证明当我们面对错误时,正是大脑成长的最佳时期。难道你没有经历过从错误中学习并在数月甚至数年后回忆起相关场景的过程吗?就像珍妮特·梅特卡夫(Janet Metcalfe)在《从错误中学习》一书中所说,这一特点是因为错误具有促进作用。其主要思想是我们不仅能记住错误,而且还能记住错误发生的场景。这就是为什么从错误中学习是如此有效的原因之一。

遵循这些原则,本书将包含开发人员在该语言的关键领域所犯的 100 个常见错误。同时,为了加强我们提到的促进作用,每个错误都会尽可能的由真实世界发生的例子。该书不仅是关于理论的。它的主要目标是帮助你走上精通 Go 的道路。

1.1 Go 概述

让我们重新思考是什么让 Go 成为一种在现代系统中如此流行和高效的语言。

1.1.1 特点

在特性方面,Go 没有类型继承,没有异常,没有宏,没有偏函数,不支持惰性求值或不变性,没有运算法重载,没有模式匹配,没有隐式类型转换等。

这些特性为什么在 Go 语言中不支持呢?官方 Go FAQ 给我们提供了见解: 为什么 Go 没有特征 X?你最喜欢的功能可能会丢失,是因为它不合适,因为>它影响了编译速度或设计的清晰度,或者因为它会使基础系统模型变得太困> 难。 --- Go FAQ

因此,一门编程语言的特性数量不应该成为我们关注的主要方面。至少,这不是 Go 语言所提倡的。

类型继承就是一个非常好的例子。类型继承的问题在于它在大型代码库中代码的路径会更复杂,以及难以理解。事实上,如果大多数交互都基于继承,那么开发人员维护的心智模型会很快变的复杂。长期以来,人们一直建议程序员应该更喜欢组合而非继承。因此,继承没有被包含在 Go 语言中。这是众多例子中的一个,在这些例子中,Go 设计者有意地倾向于语言的其他方面,而不是添加尽可能多的特性。

另一个例子是关于数据结构的。Go 只有三种标准的数据结构类型:

  • array。可以存储固定数量的且类型相同的元素。
  • slice。数组的动态版本,提供了一种更强大、更方便的方式来存储元素集合。
  • map。Go 中用于存储键值对的哈希表的实现。

因此,没有链表、没有二叉树、没有二叉堆等。这种标准数据结构的缺乏可能让新手感觉有些惊讶。然而,这是 Go 设计者有意做出的。例如,大多数情况,切片是 CPU 访问动态元素列表的最有效的方式。它涉及可预测访问模式并依赖数据局部性,这使得它在大多数请情况下比链表更有效。这三种基本类型可以处理我们使用 Go 开发时遇到的大多数场景。

稳定性也是 Go 的一个基本特征。尽管 Go 收到频繁的更新(性能改进,安全补丁等),但在过去的很多年 Go 仍然是一门非常稳定的语言。稳定性是在组织规模上采用一种语言的一个重要方面。人们甚至认为它是语言的最佳特征。

总而言之,Go 不是包含最多特性的语言。然而,一切都是为了考虑编程语言的所有方面,并为开发人员提供尽可能好的平衡。

1.1.2 开发者生产力

今天,我们比以往任何时候都更快的构建,测试和部署。软件编程必须顺应这一趋势。Go 被认为是开发人员最具有生产力的语言。让我们看看为什么这么说。

简洁性

首先,我们提到的是 Go 是一种简洁的语言:它只有 25 个关键字。如果与其他语言相比,Java 和 Rust 有 50 多个,C++ 有 100 多个,等等。

例如,由于错误管理(errors 处理),人们可以能会争论 Go 应用程序是否是简洁的。然而,一般来说,Go 的简洁性体现在对于新手来说 Go 的学习曲线很浅。在 Go 中,开发人员可以通过注入 tour.golang.org 之类的资源来快速学习 Go。

富有表现力

我们可以强调 Go 是富有表现力的。在编程语言中的表现力意味着我们可以自然的和直观的编写和阅读代码。正如 Robert C.Martin 在《整洁代码》一书中所写的那样,阅读与写作所花费的时间比远远超过 10:1。因此,使用富有表现力的语言工作是至关重要的,尤其是在大型组织中。此外,与其他语言相比,解决常见问题的方法数量减少也使得大型 Go 代码库通常更容易处理。

快速编译

开发人员生产力的另一个重要方面是编译时间。例如,作为开发人员,还有什么比必须等待构建完成才能执行单元测试更令人烦恼的吗?

以快速编译为目标一直是 Go 设计者有意而为的。首先,Go 的设计目的是为软件构建提供一个模型,简化依赖性分析,避免 C 风格的 include 文件和库的大量开销。因此,为开发人员编译节省了大量时间。

总之,Go 被认为是一种高效的语言,主要有三个原因:简洁性,表达性和高效性。然而,正如您想象的那样,生产力并不是语言中唯一需要考虑的方面。让我们看看使 Go 语言如此流行的其他方面。

1.1.3 安全性

Go 是一门静态类型的语言。因此,类型检查是在编译阶段而非运行时进行的。这样就保证了我们编写的代码在大多数情况下是类型安全的。

此外,Go 具有垃圾收集器来帮助开发者处理内存管理。直接管理内存不是开发人员的责任。垃圾回收器负责跟踪内存分配并在不需要的时候释放内存。但在执行期间也增加了一点开销。出于这个原因,Go 不打算用于实时应用程序,因为通常不可能对执行时间做出严格的保证。然而,这是一种假设平衡,因为它显著减少了开发工作并降低了应用程序崩溃或内存泄露的风险。

对于开发人员来说,另一个令人害怕的方面是指针。指针是一个包含另一个变量地址的变量。指针是 Go 语言的一个核心方面。然而,Go 中的指针处理起来并不复杂,因为他们是显式的(与引用不同),并且没有指针运算之类的东西。这是什么原因呢?再次是为了降低编写不安全应用程序的风险。

由于这些特性,Go 是一种非常安全的语言,这对 Go 应用程序的总体可靠性产生了积极的影响。

1.1.4 并发

2005 年,注明的 C++ 专家 Herb Sutter 写了一篇名为 免费的午餐结束了 的博客文章。他提到,在过去的 30 年里,CPU 设计者主要在三个领域取得了显著的进步:

  • 时钟速度
  • 执行优化
  • 缓存

多年来,通过改进这三个领域导致顺序应用程序的性能(非并行,单线程,单进程)的改进。然而,根据 Herb Sutter 的说法,现在是时候停止期望 CPU 持续变得更快了。这一假设在过去几年得到了验证。如图 1.1 所示,从 2004 年左右开始,单线程执行的速度提升不再是线性的。更糟糕的是,它已经趋于达到上限。

图 1.1

Herb Sutter 接着提到现在是改变我们开发应用程序方式的正确时机。同时,CPU 设计人员不再只关注时钟速度和优化。相反,他们开始考虑其他方法,例如多核和吵线程(同一物理核上的多个逻辑核)。并发性将成为软件开发人员的下一个重大革命,而不是编写顺序应用程序并期望 CPU 总是变的更快。

Go 编程语言在设计时就考虑到了并发性。它的并发模型基于通信顺序进程(CSP)。我们将在下一节查看此模型。

CSP 模型

CSP 模型是一种依赖于消息传递的并发范式。进程不必共享内存,而是通过通道交换消息来进行通信。如图 1.2 所示:

图 1.2

在图 1.2 中,我们可以看到基于 CSP 的两个进程之间的交互。每一个进程都是顺序执行的。没有回调使整个交互更加复杂。第一个进程发送一个消息 A,同时在某个时刻等待响应。第二个进程等待消息 A,执行一些工作,并作为响应,发送一个消息 B。

通过内存共享促进消息传递的基本原理是什么呢?

今天,所有的 CPU 都有不同级别的缓存来加速对主内存(RAM)的访问。跨不同线程共享的变量可能会重复多次。因此,共享内存是现代 CPU 提供的一种错觉(我们将在并发章节深入研究这些概念)。

采用消息传递符合现代 cpu 的构建方式,这在大多数情况下对性能有重大影响。此外,她使复杂的交互更容易推理。我们不必处理复杂的回调链:一切都是按顺序编写的。

Go 使用两个原语实现了 CSP 模型:goroutine 和 channel。

goroutine 可以被看成是一个轻量级的线程。与操作系统调度的线程不同,goroutines 是由 Go 运行时调度的。一个 goroutine 同一时间只属于一个线程,同时,一个线程能处理多个 goroutines,如图 1.3 所示:

图 1.3

操作系统负责在 CPU 内核上调度线程。同时,Go 运行时根据工作负载确定最合适的 Go 线程数量,并在这些线程上调度 goroutine。与线程相比,创建 goroutine 的成本在启动时间和内存(只有 2KB 的栈大小)方面更便宜。从一个 goroutine 到另一个 goroutine 的上下文切换操作也比线程的上下文切换更快。因此,看到应用程序同时创建数百个甚至数千个 goroutine 的情况并不少见。

另一方面,channel 是一种允许在不同 goroutine 之间交换数据的数据结构。发送到 channel 的每一条消息最多由一个 goroutine 接收。唯一的广播操作(1 对 N)是一个 channel 闭包,它传播被多个 goroutine 接收的事件。

将这些原语座位核心语言的一部分是一个了不起的特性。无需依赖任何外部库。开发人员可以以整洁的、富有表现力和标准的方式编写并发代码。当然,我们仍然可以使用互斥锁的方式来共享内存。然而,在大多数情况下,我们应该支持消息传递的方法,主要是因为,正如所讨论的,这种方法利用了现代 CPU 的构建方式。

消息传递是一种强大的并发方法,但它不能防止数据竞争。幸运的是,Go 提供了一个强大工具来检测数据竞争。

我们通过计的多个方面展示了 Go 是强大的和简单易学的。那么,你又为什么要阅读一本关于 Go 的书来扩展你的知识呢?

1.2 简单不意味着容易

简单和容易之间存在者细微的差别。简单地应用一项技术意味着学习或理解起来并不复杂。然而,容易意味着我们可以毫不费力的实现一切。Go 则简单易学,但难于精通。

我们以并发为例。2019 年,发表了一项针对并发错误的研究:Understanding Real-World Concurrency Bugs in Go。这项研究是对并发错误的首次系统分析,重点关注六个流行的 Go 代码仓库:Docker,Kubernetes,etcd,CockroachDB,BoltDB 和 gRPC。

这项研究中有许多有趣的结论。在所有这些仓库中,作者表明,尽管在 Go 中达到了培养,但传递消息的放阿飞的使用频率低于共享内存方法。该研究还强调,尽管人们认为传递消息的方法更容易处理切不易出错,但大多数阻塞错误都是由传递消息的不准确使用引起的。

关于这项研究,我们将得出什么结论?我们是否应该害怕在我们的应用程序中使用消息传递方法呢?当然不是。首先,共享内存和传递消息两种范式可以共存。这也意味着,我们,Go 开发人员,需要取得一些进展并彻底理解消息传递方法的含义,以避免重复最常见的并发错误。然而,这也意味着消息传递虽然在理论上易于学习和使用,但在实践中并不容易掌握。

这个观点-- 简单不代表容易 可以推广到 Go 的很多方面,不仅是并发;例如:

  • 什么时候使用接口?
  • 什么时候使用值接收,什么时候使用指针接收?
  • 如何高效处理切片?
  • 如何干净而富有表现力的处理错误管理?
  • 如何避免内存泄露?
  • 如何编写相关测试和基准测试?
  • 如何使用应用程序做好生产准备?

要成为一名熟练的 Go 开发者,我们应该对该语言的许多方面都要有透彻的了解,这需要大量的时间,精力和错误。本书疑意在通过收集和展示 Go 语言各个方面的 100 个常见错误来帮助你成为一名熟练的开发人员:基础知识、代码组织、数据和控制结构、字符串、函数和方法、错误管理、并发、测试、优化和生产。

1.3 总结

  • Go 是一种现代编程语言,它在开发人员的生产力方面投入了大量精力,这对当今大多数公司来说至关重要。
  • Go 简单易学,但难以精通;这就是为什么如果我们想最有效地使用 Go,我们就要加深我们的知识。
  • 通过错误和具体例子学习是一种强大的手段。
更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
Orichalcum GoCN 每日新闻 (2021-07-21) 中提及了此贴 07月21日 09:38
astaxie 将本帖设为了精华贴 07月21日 11:13
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册