#0. Go:易学难精

223 阅读12分钟

本章内容包括:

  • Go语言的高效、可扩展和高产出的原因
  • 探讨Go语言易学难精的原因
  • 描述开发者常见的错误类型

做错误是每个人生活的一部分。正如阿尔伯特·爱因斯坦曾说过的:

“一个从未犯过错误的人,从未尝试过新事物。”

最后重要的不是我们犯了多少错误,而是我们从错误中学习的能力。这种说法也适用于编程。我们在一种语言中获得的高级技能不是神奇的过程;它涉及到做出许多错误并从中吸取教训。这本书的目的就建立在这个思想之上。通过查看和学习人们在语言的许多领域中常犯的100个错误,它将帮助您成为更熟练的Go开发者。

本章将快速回顾为什么Go在这些年逐渐成为主流。我们将讨论尽管Go被认为易于学习,但是掌握其细微差别可能具有挑战性的原因。最后,我们将介绍本书涵盖的概念。

1.1 Go 概述

如果你正在阅读此书,那么你可能已经对Go语言深信不疑了。因此,本节简要回顾了Go语言如此强大的原因。

在过去几十年中,软件工程获得了长足的发展。大多数现代系统不再由单个人编写,而是由多个程序员组成的团队编写的 - 有时甚至达到数百甚至数千人。如今,代码必须可读、表达明确且可维护,以保证系统多年来的持久性。与此同时,在我们这个快节奏的世界中,最大限度地提高敏捷性和缩短上市时间对大多数组织来说至关重要。编程也应该追随这一趋势,公司努力确保软件工程师在阅读、编写和维护代码时尽可能高效。

为了应对这些挑战,谷歌在2007年创建了Go编程语言。从那时起,许多组织已经采用该语言来支持各种用例:API、自动化、数据库、CLI(命令行接口)等等。如今,许多人认为Go是云的语言。

从特性上看,Go没有类型继承、异常、宏、部分函数、对懒变量计算或不变性的支持、运算符重载、模式匹配等等。为什么这些特性在语言中缺失? 官方的 Go FAQ(go.dev/doc/faq)给了我们一些启发:

为什么Go语言没有X特性?你最喜欢的特性可能因为不合适而缺失,或者因为它影响编译速度或者设计的清晰性,亦或因为它会使基本系统模型过于复杂而被忽略。

通过语言的特性数量来判断其质量可能不是一个准确的指标。至少,这不是Go的目标。相反,Go在组织规模采用语言时利用了一些基本特性。这些包括:

  • 稳定性(Stability)——尽管 Go 经常进行更新(包括改进和安全补丁),但它仍然是一种稳定的语言。有些人甚至认为这是该语言的最佳特性之一。
  • 表达能力(Expressivity)——我们可以通过代码的编写和阅读的自然程度和直观程度来定义编程语言的表达能力。关键字数量的减少和解决常见问题的有限方式使得 Go 成为用于大型代码库的高表达语言。
  • 编译(Compilation)——作为开发者,什么会比等待构建来测试应用程序更令人沮丧?使编译时间尽可能短一直是语言设计者有意识的目标。这反过来又提高了生产力。
  • 安全性(Safety)——Go 是一种强类型的静态语言。因此,它有严格的编译期规则,在大多数情况下确保代码是类型安全的。

Go从底层开始就具有坚实的特性,比如利用goroutine和channel提供出色的并发原语。构建高效的并发应用程序不需要强烈依赖外部库。观察并发在当前的重要性也展示了为什么Go是当下以及可预见的未来非常合适的语言。

有些人也认为Go是一种简单的语言。从某种意义上说,这不一定是错误的。例如,新手可以在不到一天的时间内学习该语言的主要特性。那么,如果Go很简单,为什么要阅读以错误为中心的书呢?

1.2 简单不等于容易

简单和容易之间存在微妙的区别。简单,应用于技术,意味着不难学习或理解。然而,容易意味着我们可以毫不费力地实现任何事情。Go易于学习但不一定易于掌握。

举个例子,让我们来看看并发。2019年,一项关注并发错误的研究被发表:“理解Go语言中的真实世界并发错误”。这项研究是对并发错误进行的首次系统分析。它关注了多个流行的Go仓库,如Docker、gRPC和Kubernetes。这项研究最重要的启示之一是,尽管人们认为基于消息传递比共享内存更易于处理和不易出错,但大多数阻塞错误都是由于通过channel不准确使用消息传递范式造成的。

对这样的结论应该作出什么适当的反应?我们应该认为语言设计者对消息传递的看法是错误的吗?我们应该重新考虑在项目中如何处理并发吗?当然不是。

这并不是对抗消息传递和共享内存,并确定胜出者的问题。但是,作为Go开发者,我们有责任透彻地理解如何使用并发,它对现代处理器的影响,在什么情况下应该优先使用一种方法而不是另一种方法,以及如何避免常见的陷阱。这个例子强调了尽管channels和goroutines等概念很简单,但在实践中它并不是一个简单的话题。

“简单并不意味着容易”这个主题,不仅适用于并发,还可以推广到Go的许多方面。因此,要成为熟练的Go开发者,我们必须对语言的许多方面有透彻的理解,这需要时间、努力和错误的积累。

这本书旨在通过深入探讨100个Go错误来帮助加速我们向专业化发展的过程。

1.3 100个Go错误

为什么我们应该阅读一本关于常见Go语言错误的书籍?为什么不通过一本普通的书来深入我们的知识,探索不同的主题呢?

在2011年的一篇文章中,神经科学家们证明了,面对错误时是大脑成长的最佳时机。 难道我们没有经历过从错误中学习并在几个月甚至几年后回忆起那个场合的过程,尤其是在一些与它相关的情境中?正如Janet Metcalfe在另一篇文章中所展示的,这是因为错误具有促进作用。 主要的思想是,我们不仅能记住错误,还能记住错误周围的情境。这是从错误中学习如此高效的原因之一。

为了加强这种促进作用,这本书尽可能地为每个错误配上现实世界的例子。这本书不仅仅是关于理论;它还帮助我们更好地避免错误,做出更明智、更有意识的决策,因为现在我们理解了这些错误背后的理由。

告诉我,我会忘记。教我,我会记住。让我参与,我会学习。

—未知

这本书介绍了七个主要的错误类别。总的来说,这些错误可以分为:

  • 缺陷
  • 不必要的复杂性
  • 较弱的可读性
  • 次优或非惯用法的组织
  • API便利性的缺失
  • 代码优化不足
  • 生产力的缺失

接下来我们将介绍每个错误类别。

1.3.1 Bugs

第一种错误类型,可能也是最显而易见的,就是软件缺陷。2020年,Synopsys公司进行的一项研究估计,仅在美国,软件缺陷造成的成本就超过2万亿美元。

此外,软件缺陷还可能导致悲惨的后果。例如,我们可以提及Therac-25这个案例,这是由加拿大原子能有限公司(AECL)生产的一种放射疗法设备。由于存在竞态条件缺陷,该设备给患者的辐射剂量是预期的数百倍,导致三名患者不幸死亡。因此,软件缺陷问题并非仅仅关乎金钱。作为开发人员,我们应当时刻牢记自己的工作会产生多大的影响力。

本书涵盖了大量可能导致各种软件缺陷的案例,包括数据竞争、内存泄漏、逻辑错误和其他缺陷。尽管准确的测试应该是尽早发现此类缺陷的一种方式,但有时由于时间限制或复杂性等不同因素,我们可能会遗漏某些案例。因此,作为一名Go开发人员,确保我们能够避免常见的缺陷是至关重要的。

1.3.2 不必要的复杂性

下一类错误与不必要的复杂性有关。软件复杂性的一大部分源于这样一个事实:作为开发人员,我们努力思考虚构的未来场景。与其解决当下具体的问题,不如构建一种可以应对任何未来用例的进化软件,这种做法往往更有诱惑力。然而,在大多数情况下,这种做法弊大于利,因为它会使代码库变得更难以理解和推理。

回到Go语言,我们可以想到许多开发人员可能会因为未来的需求而设计抽象(如接口或泛型)的用例。本书讨论了一些主题,在这些主题中,我们应该谨慎,不让不必要的复杂性损害代码库。

1.3.3 较弱的可读性

另一种错误是削弱可读性。正如Robert C. Martin在他的著作《代码整洁之道:敏捷软件手工艺》中所写,我们花在阅读代码上的时间比编写代码的时间要多出10倍以上。大多数人最初都是在单人项目中开始编程,那时代码可读性并不太重要。然而,现代软件工程是一种有时间维度的编程:确保我们几个月、几年甚至几十年后,仍然能够处理和维护某个应用程序。

在Go语言编程时,我们可能会犯许多损害可读性的错误。这些错误可能包括嵌套代码、数据类型表示或在某些情况下不使用命名的返回值参数。通过本书,我们将学习如何编写可读的代码,并为未来的读者(包括未来的自己)着想。

1.3.4 次优或非惯用的组织结构

无论是在从事新项目时,还是因为我们形成了不准确的反射习惯,另一种错误是次优或非惯用地组织代码和项目。这种问题会使项目变得更难推理和维护。本书涵盖了Go语言中一些常见的这类错误。例如,我们将看看如何构建项目结构,以及如何处理实用程序包或init函数。总的来说,了解这些错误应该能帮助我们更高效、更惯用地组织代码和项目。

1.3.5 缺乏API便利性

为客户制作降低API便利性的常见错误是另一种错误类型。如果API不友好,它将不那么具有表达力,因此更难理解,也更容易出错。 我们可以想到许多情况,例如过度使用任何类型,使用错误的创建模式来处理选项,或者盲目应用面向对象编程的标准实践,这些都会影响我们API的可用性。本书涵盖了常见的错误,这些错误阻止我们为用户公开方便使用的API。

1.3.6 代码优化不足

低效代码是开发者可能犯的另一种错误。它可能由多种原因引起,比如不理解语言特性或缺乏基础知识。性能是这种错误最明显的影响之一,但并非唯一。

我们可以思考为其他目标优化代码,例如准确性。例如,本书提供了一些常见的技术,以确保浮点运算的准确性。同时,我们将涵盖许多可能因执行并行化不当、不知道如何减少分配或数据对齐的影响等而对性能代码产生负面影响的案例。我们将通过不同的视角来处理优化问题。

1.3.7 生产力不足

在大多数情况下,当我们开始一个新项目时,最好的语言选择是什么?是我们最有生产力的语言。熟悉一种语言的工作方式并利用它来发挥最大的效用,对于达到熟练程度至关重要。

在这本书中,我们将涵盖许多案例和具体的例子,这将帮助我们在Go语言工作中提高生产力。例如,我们将看到编写高效的测试以确保我们的代码工作,依靠标准库来提高效率,以及充分利用性能分析工具和代码检查工具。现在,是时候深入探讨那100个常见的Go语言错误了。