styleguide

Go 风格指南

https://ggdocs.cn/styleguide/go/guide

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

注意: 这是概述 Google Go 风格的一系列文档的一部分。 本文档是规范性的权威性的。 有关更多信息,请参阅 概述

风格原则

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

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

清晰性

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

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

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

代码实际上在做什么?

Go 的设计方式应该能够相对直接地看到代码在做什么。 如果存在不确定性,或者读者可能需要先前的知识才能理解代码,那么值得花时间使代码的目的对于未来的读者来说更加清晰。 例如,它可能有助于

这里没有万能的方法,但在开发 Go 代码时,优先考虑清晰性非常重要。

代码为什么要这样做?

代码的原理通常可以通过变量、函数、方法或包的名称来充分传达。 如果没有,添加注释很重要。 当代码包含读者可能不熟悉的细微差别时,“为什么?”尤其重要,例如

API 可能需要小心使用才能正确使用。 例如,由于性能原因,一段代码可能很复杂且难以遵循,或者一系列复杂的数学运算可能以意想不到的方式使用类型转换。 在这些情况下以及更多情况下,伴随的注释和文档解释这些方面非常重要,以便未来的维护人员不会犯错误,并且读者可以理解代码而无需进行逆向工程。

同样重要的是要注意,某些提供清晰度的尝试(例如添加额外的注释)实际上可能会通过添加混乱、重复代码已有的内容、与代码冲突或增加维护负担以保持注释最新,从而模糊代码的目的。 让代码自己说话(例如,通过使符号名称本身具有自我描述性),而不是添加冗余注释。 通常,注释最好解释为什么要这样做,而不是代码在做什么。

Google 代码库在很大程度上是统一且一致的。 通常情况下,突出显示的代码(例如,通过使用不熟悉的模式)这样做是有充分理由的,通常是为了提高性能。 保持此属性很重要,以便让读者清楚地知道他们在阅读新代码时应该关注哪里。

标准库包含许多此类原则的示例。 其中

简洁性

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

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

代码简洁性和 API 使用简洁性之间可能会出现权衡。 例如,使代码更复杂可能是值得的,这样 API 的最终用户可以更轻松地正确调用 API。 相反,将一些额外的工作留给 API 的最终用户也可能是值得的,以便代码保持简单易懂。

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

此原则并不意味着不能或不应该用 Go 编写复杂的代码,或者不允许 Go 代码复杂。 我们力求建立一个避免不必要复杂性的代码库,以便当复杂性确实出现时,它表明有问题的代码需要小心理解和维护。 理想情况下,应该有伴随的注释来解释原理并识别应采取的护理措施。 这通常发生在优化代码以提高性能时;这样做通常需要更复杂的方法,例如预分配缓冲区并在整个 goroutine 生命周期中重复使用它。 当维护人员看到这一点时,它应该是一个线索,表明有问题的代码对性能至关重要,并且应该影响在进行未来更改时采取的护理。 另一方面,如果不必要地使用,这种复杂性会给将来需要阅读或更改代码的人带来负担。

如果代码的目的应该很简单,但结果却非常复杂,这通常表明应该重新审视实现,看看是否有更简单的方法来实现相同的目标。

最少机制

如果有多种表达相同想法的方式,请首选使用最标准工具的方式。 通常存在复杂的机制,但不应在没有理由的情况下使用。 根据需要向代码添加复杂性很容易,而一旦发现不必要,删除现有的复杂性则要困难得多。

  1. 如果足以满足您的用例,请尽量使用核心语言结构(例如通道、切片、映射、循环或结构)。
  2. 如果没有,请在标准库中查找工具(如 HTTP 客户端或模板引擎)。
  3. 最后,在引入新依赖项或创建自己的依赖项之前,请考虑 Google 代码库中是否有一个核心库足以满足需求。

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

同样,如果一段代码需要进行集合成员检查,则通常可以使用布尔值映射(例如,map[string]bool)。 仅当需要更复杂的操作时,才应使用提供类似集合的类型和功能的库,这些操作使用映射无法实现或过于复杂。

简洁

简洁的 Go 代码具有很高的信噪比。 很容易辨别相关的细节,并且命名和结构引导读者了解这些细节。

有很多事情会妨碍在任何给定时间显示最突出的细节

重复的代码尤其会模糊每个几乎相同的部分之间的差异,并且需要读者在视觉上比较相似的代码行以找到更改。 表格驱动测试 是一个很好的示例,说明了一种机制,该机制可以简洁地从每次重复的重要细节中分解出公共代码,但是选择将哪些部分包含在表格中将影响表格的易于理解程度。

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

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

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

如果代码看起来与此非常相似但略有不同,则读者可能不会注意到更改。 在这种情况下,值得有意地“提升”错误检查的信号,方法是添加注释以引起注意。

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

可维护性

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

可维护的代码

当使用像接口和类型这样的抽象时,它们在定义上会从它们被使用的上下文中移除信息,因此确保它们提供足够的好处非常重要。编辑器和 IDE 可以直接连接到方法定义并在使用具体类型时显示相应的文档,但否则只能引用接口定义。接口是一种强大的工具,但也有成本,因为维护者可能需要了解底层实现的细节才能正确使用接口,这必须在接口文档或调用点中解释。

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

// Bad:
// The use of = instead of := can change this line completely.
if user, err = db.UserByID(userID); err != nil {
    // ...
}
// Bad:
// The ! in the middle of this line is very easy to miss.
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:
// Gregorian leap years aren't just year%4 == 0.
// See 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)

同样,隐藏关键逻辑或重要边缘情况的辅助函数可能会使未来的更改很容易无法正确地考虑它。

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

可维护的代码会最大程度地减少其依赖项(包括隐式和显式)。 依赖更少的包意味着可以影响行为的代码行数更少。 避免依赖内部或未记录的行为可以减少代码在未来这些行为发生变化时带来的维护负担。

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

一致性

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

一致性问题不会覆盖上述任何原则,但如果必须打破僵局,通常最好打破它以支持一致性。

包内的一致性通常是最重要的一致性级别。 如果在整个包中以多种方式处理相同的问题,或者如果同一个概念在文件中具有多个名称,则可能会非常令人不快。 但是,即使这样也不应覆盖已记录的样式原则或全局一致性。

核心指南

这些指南收集了所有 Go 代码都应遵循的 Go 样式的最重要方面。 我们期望在授予可读性时学习和遵循这些原则。 这些原则预计不会经常更改,并且新添加的内容必须清除很高的标准。

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

格式化

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

MixedCaps

Go 源代码使用 MixedCapsmixedCaps(驼峰式大小写)而不是下划线(蛇形大小写)来编写多字名称。

即使它打破了其他语言的约定也是如此。 例如,如果导出常量,则它是 MaxLength(不是 MAX_LENGTH),如果未导出,则是 maxLength(不是 max_length)。

局部变量被认为是 未导出的,目的是选择初始大写字母。

行长度

Go 源代码没有固定的行长度。 如果一行感觉太长,请首选重构而不是拆分它。 如果它已经尽可能短了,则应允许该行保持较长。

不要拆分行

命名

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

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

局部一致性

如果样式指南对特定的样式点没有任何说明,则作者可以自由选择他们喜欢的样式,除非在附近的代码(通常在同一个文件或包中,但有时在团队或项目目录中)对该问题采取了一致的立场。

有效的局部样式考虑因素示例

无效的局部样式考虑因素示例

如果局部样式与样式指南不一致,但可读性影响仅限于一个文件,则通常会在代码审查中发现该问题,在这种情况下,一致的修复将超出 CL 的范围。 此时,可以提交一个错误来跟踪修复。

如果更改会使现有的样式偏差恶化,在更多 API 表面中暴露它,扩大偏差存在的文件数量,或者引入实际的错误,那么局部一致性不再是违反新代码样式指南的有效理由。 在这些情况下,作者可以适当地清理同一 CL 中的现有代码库,在当前 CL 之前执行重构,或者找到至少不会使局部问题恶化的替代方案。