Skip to Content
Go风格指南

Go 风格指南

https://google.github.io/styleguide/go/guide 

概述 | 指南 | 决策 | 最佳实践

注意: 这是概述 Google Go 风格的系列文档之一。本文档是**规范性的权威性的**。更多信息请参见概述

风格原则

有几个总体原则概括了如何思考编写可读的 Go 代码。以下是可读代码的属性,按重要性排序:

  1. 清晰性:代码的目的和原理对读者来说是清楚的。
  2. 简单性:代码以最简单的方式实现其目标。
  3. 简洁性:代码具有高信噪比。
  4. 可维护性:代码的编写方式使其易于维护。
  5. 一致性:代码与更广泛的 Google 代码库保持一致。

清晰性(Clarity)

可读性的核心目标是产生对读者来说清晰的代码。

清晰性主要通过有效的命名、有用的注释和高效的代码组织来实现。

清晰性应从读者而非代码作者的角度来看待。代码易于阅读比易于编写更重要。代码的清晰性有两个不同的方面:

代码实际上在做什么?

Go 的设计使得人们可以比较直观地看到代码在做什么。在不确定的情况下,或者读者可能需要先验知识才能理解代码时,值得投入时间使代码的目的对未来的读者更加清晰。例如,以下做法可能有帮助:

  • 使用更具描述性的变量名
  • 添加额外的注释
  • 用空白和注释分隔代码
  • 将代码重构为独立的函数/方法以使其更加模块化

这里没有放之四海而皆准的方法,但在开发 Go 代码时优先考虑清晰性是很重要的。

代码为什么要这样做?

代码的原理通常可以通过变量、函数、方法或包的名称充分传达。当名称不够说明时,添加注释就很重要。“为什么?“在代码包含读者可能不熟悉的细微之处时尤为重要,例如:

  • 语言层面的细微之处,例如,一个闭包将捕获循环变量,但闭包在很多行之后
  • 业务逻辑层面的细微之处,例如,一个访问控制检查需要区分实际用户和冒充用户的人

API 可能需要谨慎使用。例如,一段代码可能因为性能原因而错综复杂且难以跟踪,或者复杂的数学运算序列可能以意想不到的方式使用类型转换。在这些以及更多情况下,随附的注释和文档应该解释这些方面,以便未来的维护者不会犯错误,读者能够在不需要逆向工程的情况下理解代码。

同样重要的是要意识到,某些旨在提供清晰性的尝试(如添加额外注释)实际上可能因增加杂乱、重申代码已经表达的内容、与代码矛盾或增加维护负担来保持注释更新而模糊代码的目的。让代码自己说话(例如,通过使符号名称本身具有自描述性)而不是添加冗余注释。注释通常更好地解释为什么要这样做,而不是代码在做什么。

Google 代码库在很大程度上是统一和一致的。代码中突出的部分(例如,使用不熟悉的模式)通常是有充分理由的,通常是为了性能。保持这一特性对于让读者在阅读新代码时知道应该把注意力集中在哪里非常重要。

标准库中包含许多这一原则的实际示例。其中包括:

简单性(Simplicity)

你的 Go 代码应该对使用、阅读和维护它的人来说是简单的。

Go 代码应以最简单的方式编写来实现其目标,无论是在行为还是性能方面。在 Google Go 代码库中,简单的代码:

  • 从上到下易于阅读
  • 不假设你已经知道它在做什么
  • 不假设你能记住所有前面的代码
  • 没有不必要的抽象层次
  • 名称不会引起对平凡事物的注意
  • 使值和决策的传播对读者来说是清晰的
  • 注释解释为什么而不是什么,以避免未来的偏离
  • 文档自成体系
  • 有有用的错误信息和有用的测试失败信息
  • 通常与”巧妙”的代码互斥

代码简单性和 API 使用简单性之间可能存在权衡。例如,让代码更复杂可能是值得的,以便 API 的最终用户可以更容易地正确调用 API。相反,给 API 的最终用户留一点额外工作也可能是值得的,以保持代码简单和易于理解。

当代码需要复杂性时,复杂性应该被有意地添加。这通常在需要额外性能或特定库或服务有多个不同的客户时是必要的。复杂性可能是合理的,但应该附带文档,以便客户和未来的维护者能够理解和驾驭复杂性。这应该辅以展示其正确用法的测试和示例,特别是在同时存在”简单”和”复杂”使用方式的情况下。

这一原则并不意味着不能或不应该用 Go 编写复杂代码,也不意味着 Go 代码不允许复杂。我们追求的代码库避免不必要的复杂性,这样当复杂性确实出现时,它表明相关代码需要小心理解和维护。理想情况下,应该有随附的注释来解释原因并说明应该注意什么。这在优化代码性能时经常出现;这样做通常需要更复杂的方法,比如预分配缓冲区并在整个 goroutine 生命周期中重用它。当维护者看到这一点时,它应该是一个暗示,表明相关代码对性能至关重要,这应该影响未来更改时的谨慎程度。另一方面,如果不必要地使用,这种复杂性对于未来需要阅读或更改代码的人来说是一种负担。

如果代码在其目的应该很简单时却变得非常复杂,这通常是一个信号,表明应该重新审视实现,看看是否有更简单的方式来实现同样的事情。

最小机制原则(Least mechanism)

当有多种方式表达同一想法时,优先选择使用最标准工具的方式。复杂的机制通常存在,但不应无理由地使用。根据需要给代码添加复杂性是容易的,而在发现现有复杂性不必要后再移除它则困难得多。

  1. 当核心语言构造(例如通道、切片、映射、循环或结构体)足以满足你的用例时,优先使用它们。
  2. 如果没有合适的核心语言构造,在标准库中寻找工具(如 HTTP 客户端或模板引擎)。
  3. 最后,在引入新依赖或创建自己的依赖之前,考虑 Google 代码库中是否有足够的核心库。

例如,考虑生产代码包含一个绑定到变量的 flag,该变量有一个必须在测试中覆盖的默认值。除非打算测试程序的命令行接口本身(例如使用 os/exec),否则直接覆盖绑定的值比使用 flag.Set 更简单,因此也更可取。

同样,如果一段代码需要集合成员检查,布尔值映射(例如 map[string]bool)通常就足够了。提供类似集合类型和功能的库只有在需要更复杂的操作且用映射无法实现或过于复杂时才应使用。

简洁性(Concision)

简洁的 Go 代码具有高信噪比。它易于辨别相关细节,命名和结构引导读者通过这些细节。

在任何给定时间,有很多东西可能妨碍最显著细节的呈现:

重复的代码尤其会模糊每个几乎相同部分之间的差异,并要求读者目视比较相似的代码行来发现变化。表驱动测试 是一个很好的机制示例,它可以简洁地从每次重复的重要细节中提取公共代码,但选择在表中包含哪些内容将影响表的易理解程度。

在考虑多种代码结构方式时,值得考虑哪种方式使重要细节最明显。

理解和使用常见的代码构造和惯用法对于保持高信噪比也很重要。例如,以下代码块在错误处理 中非常常见,读者可以快速理解此块的用途。

// Good: if err := doSomething(); err != nil { // ... }

如果代码看起来与此非常相似但有细微不同,读者可能不会注意到变化。在这种情况下,值得有意”增强”错误检查的信号,添加注释以引起注意。

// Good: if err := doSomething(); err == nil { // if NO error // ... }

可维护性(Maintainability)

代码被编辑的次数远多于被编写的次数。可读的代码不仅对试图理解其工作方式的读者有意义,而且对需要更改它的程序员也有意义。清晰性是关键。

可维护的代码:

  • 易于未来的程序员正确修改
  • API 的结构使其能够优雅地增长
  • 对其所做的假设是清晰的,并且选择的抽象映射到问题的结构,而不是代码的结构
  • 避免不必要的耦合,不包含未使用的功能
  • 有全面的测试套件,以确保承诺的行为得以维持且重要逻辑正确,并且测试在失败时提供清晰、可操作的诊断信息

当使用接口(Interface)和类型等抽象时(这些抽象从使用它们的上下文中移除信息),确保它们提供足够的好处是很重要的。编辑器和 IDE 可以在使用具体类型时直接连接到方法定义并显示相应的文档,但在其他情况下只能引用接口定义。接口是强大的工具,但也有成本,因为维护者可能需要了解底层实现的具体细节才能正确使用接口,这必须在接口文档或调用处进行解释。

可维护的代码还避免在容易被忽略的地方隐藏重要细节。例如,在以下每行代码中,单个字符的存在或缺失对于理解该行至关重要:

// Bad: // 使用 = 而不是 := 可能完全改变这行代码。 if user, err = db.UserByID(userID); err != nil { // ... }
// Bad: // 这行代码中间的 ! 非常容易被忽略。 leap := (year%4 == 0) && (!(year%100 == 0) || (year%400 == 0))

这两者都没有错误,但都可以用更明确的方式编写,或者可以有一个随附的注释来引起对重要行为的注意:

// Good: u, err := db.UserByID(userID) if err != nil { return fmt.Errorf("invalid origin user: %s", err) } user = u
// Good: // 格里高利闰年不仅仅是 year%4 == 0。 // 参见 https://en.wikipedia.org/wiki/Leap_year#Algorithm。 var ( leap4 = year%4 == 0 leap100 = year%100 == 0 leap400 = year%400 == 0 ) leap := leap4 && (!leap100 || leap400)

同样,隐藏关键逻辑或重要边界情况的辅助函数可能使未来的更改容易无法正确处理它。

可预测的名称是可维护代码的另一个特征。包的用户或代码的维护者应该能够在给定的上下文中预测变量、方法或函数的名称。相同概念的函数参数和接收器名称通常应共享相同的名称,既为了保持文档的可理解性,也为了以最小的开销促进代码重构。

可维护的代码最小化其依赖(包括隐式和显式的)。依赖更少的包意味着更少的代码行可以影响行为。避免依赖内部或未文档化的行为使代码在这些行为在未来变化时不太可能产生维护负担。

在考虑如何构建或编写代码时,值得花时间思考代码可能随时间如何演变。如果给定的方法更有利于更容易和更安全的未来更改,那通常是一个好的权衡,即使这意味着一个稍微更复杂的设计。

一致性(Consistency)

一致的代码是在更广泛的代码库中、在团队或包的上下文中、甚至在单个文件中,看起来、感觉和行为都像类似代码的代码。

一致性的考虑不会覆盖上述任何原则,但如果必须打破平局,通常有利于支持一致性。

包内的一致性通常是最直接重要的一致性级别。如果同一问题在包中以多种方式处理,或者同一概念在文件中有多个名称,可能会非常令人困惑。然而,即使这也不应覆盖文档化的风格原则或全局一致性。

核心指导原则

这些指导原则收集了所有 Go 代码都应遵循的 Go 风格最重要的方面。我们期望这些原则在获得可读性批准时已被学习和遵循。这些预计不会经常变化,新增的内容将需要达到很高的标准。

以下指导原则扩展了 Effective Go  中的建议,它为整个社区的 Go 代码提供了共同的基准。

格式化

所有 Go 源文件必须符合 gofmt 工具输出的格式。此格式由 Google 代码库中的预提交检查强制执行。生成的代码 通常也应该格式化(例如使用 format.Source),因为它也可以在 Code Search 中浏览。

混合大小写(MixedCaps)

Go 源代码在编写多词名称时使用 MixedCapsmixedCaps(驼峰式大小写),而不是下划线(蛇形命名法)。

即使这打破了其他语言中的惯例,也适用此规则。例如,如果导出则常量为 MaxLength(而非 MAX_LENGTH),如果未导出则为 maxLength(而非 max_length)。

局部变量在选择初始大小写时被视为未导出的 

行长度

Go 源代码没有固定的行长度限制。如果一行感觉太长,优先考虑重构而不是拆分它。如果它已经尽可能短了,该行应该被允许保持长。

不要在以下情况拆分行:

  • 缩进变化之前(例如函数声明、条件语句)
  • 为了使长字符串(例如 URL)适合多个较短的行

命名

命名更多是艺术而非科学。在 Go 中,名称往往比许多其他语言中的要短一些,但适用相同的通用指南 。名称应该:

  • 使用时不感觉重复
  • 考虑上下文
  • 不重复已经清楚的概念

你可以在决策中找到更具体的命名指导。

局部一致性

在风格指南对某个特定风格点没有说明的情况下,作者可以自由选择他们喜欢的风格,除非相近的代码(通常在同一文件或包中,但有时在团队或项目目录中)已经对该问题采取了一致的立场。

有效的局部风格考虑示例:

  • 使用 %s%v 格式化打印错误
  • 使用带缓冲的通道代替互斥锁

无效的局部风格考虑示例:

  • 代码的行长度限制
  • 使用断言测试库

如果局部风格与风格指南不一致,但可读性影响仅限于一个文件,它通常会在代码审查中被发现,而一致性修复将超出相关 CL 的范围。在那时,提交一个 bug 来跟踪修复是合适的。

如果一个更改会恶化现有的风格偏差、在更多的 API 表面暴露它、扩大偏差存在的文件数量或引入实际的 bug,那么对于新代码来说,局部一致性不再是违反风格指南的有效理由。在这些情况下,作者应该在同一个 CL 中清理现有代码库,在当前 CL 之前进行重构,或者至少找到一个不会使局部问题变得更糟的替代方案。

Last updated on