Effective Go(有效的 GO)[0]
🥈

Effective Go(有效的 GO)[0]

引言

Go 是一种新语言。尽管它借鉴了现有语言的思想,但它具有独特的特性,使得有效的 Go 程序在特性上与其亲属语言编写的程序有所不同。将 C++ 或 Java 程序直接翻译成 Go 不太可能产生令人满意的结果——Java 程序是用 Java 编写的,而不是 Go。另一方面,从 Go 的角度思考问题可能会产生成功但截然不同的程序。换句话说,要写好 Go,理解其特性和习惯用法非常重要。同时,了解 Go 编程的既定规范,如命名、格式、程序构造等,也很重要,这样你编写的程序才能让其他 Go 程序员容易理解。
本文档提供了编写清晰、符合习惯的 Go 代码的建议。它补充了语言规范、Go 语言之旅和如何编写 Go 代码的内容,建议你先阅读这些材料。
2022 年 1 月补充说明:本文档是为 2009 年 Go 发布时撰写的,自那以后没有进行显著更新。尽管它是理解如何使用该语言的良好指南,但由于语言的稳定性,它对库的内容几乎没有涉及,也没有谈及自撰写以来 Go 生态系统的重大变化,如构建系统、测试、模块和多态性等。没有计划对其进行更新,因为发生了很多事情,越来越多的文档、博客和书籍很好地描述了现代 Go 的使用。尽管《有效的 Go》仍然有用,但读者应理解它远非完整指南。有关背景信息,请参见问题 28782。

示例

Go 包源代码不仅作为核心库,还作为使用该语言的示例。此外,许多包中包含可直接从 go.dev 网站运行的自包含可执行示例,例如这个示例(如有必要,请点击“示例”一词以打开)。如果你对如何解决某个问题或某个实现方式有疑问,库中的文档、代码和示例可以提供答案、想法和背景。

格式化

格式化问题是最具争议但影响最小的。人们可以适应不同的格式化风格,但如果不必适应不同风格,那就更好。如果每个人都遵循相同的风格,就能减少在这个话题上花费的时间。问题在于如何在没有冗长规定的风格指南的情况下实现这一理想。
在 Go 中,我们采用了一种不寻常的方法,让机器处理大多数格式化问题。gofmt 程序(也可以作为 go fmt 使用,后者在包级别而非源文件级别操作)读取 Go 程序,并以标准的缩进和垂直对齐风格输出源代码,同时保留并在必要时重新格式化注释。如果你想知道如何处理某种新的布局情况,可以运行 gofmt;如果结果看起来不太对,就重新调整你的程序(或对 gofmt 提交一个 bug),而不是绕过它。
举个例子,完全没有必要花时间对齐结构体字段上的注释。gofmt 会为你处理这些。给定以下声明:
type T struct { name string // name of the object value int // its value }
gofmt 会对齐列:
type T struct { name string // name of the object value int // its value }
所有标准包中的 Go 代码都已使用 gofmt 格式化。
一些格式化细节仍然存在。简要说明如下:
缩进
我们使用制表符进行缩进,gofmt 默认会输出制表符。仅在必要时使用空格。
行长度
Go 没有行长度限制。不要担心超出打孔卡片的长度。如果一行感觉太长,可以换行并多缩进一个制表符。
括号
Go 需要的括号比 C 和 Java 少,控制结构(如 if、for、switch)的语法中没有括号。此外,运算符优先级层次更短且更清晰,因此
x<<8 + y<<16
这意味着空格所表示的含义,与其他语言不同。

注释

Go 提供了 C 风格的 /* */ 块注释和 C++ 风格的 // 行注释。行注释是常见的用法;块注释主要用于包注释,但在表达式内或禁用大段代码时也很有用。
出现在顶级声明之前且没有中间换行的注释被视为对该声明的文档。这些“文档注释”是特定 Go 包或命令的主要文档。有关文档注释的更多信息,请参见“Go 文档注释”。

名称

在 Go 中,名称与其他语言一样重要。它们甚至具有语义效果:名称在包外的可见性取决于其首字符是否为大写。因此,花一点时间讨论 Go 程序中的命名约定是值得的。

包名称

当一个包被导入时,包名成为访问其内容的方式。
import "bytes"
导入的包可以使用 bytes.Buffer。确保使用相同名称来引用包的内容是有帮助的,这意味着包名应该简短、简洁且富有表现力。根据约定,包名应为小写单词,不应使用下划线或混合大小写。尽量简短,因为使用你包的每个人都会输入这个名称。无需担心名称冲突,因为包名只是导入的默认名称;它不需要在所有源代码中唯一,在出现冲突的罕见情况下,导入包可以选择不同的本地名称。无论如何,由于导入的文件名决定了使用的是哪个包,因此混淆很少发生。
另一项约定是包名是其源目录的基本名称;例如,src/encoding/base64 中的包被导入为 "encoding/base64",但其名称为 base64,而不是 encoding_base64 或 encodingBase64
【译者注: src/encoding/base64 这种已经被 go.mod 替换,如果你学的是高版本 go,请直接忽略此部分】

导入者的命名

包的导入者将使用名称来引用其内容,因此包中的导出名称可以利用这一点以避免重复。请勿使用 import . 语法,虽然它可以简化必须在测试包外运行的测试,但通常应避免使用。例如,bufio 包中的缓冲读取器类型称为 Reader,而不是 BufReader,因为用户看到的是 bufio.Reader,这是一个清晰简洁的名称。此外,由于导入的实体始终通过其包名进行访问,bufio.Reader 不会与 io.Reader 冲突。同样,创建 ring.Ring 新实例的函数,通常会称为 NewRing,但由于 Ring 是包中唯一导出的类型,且包名为 ring,因此它被称为 New,客户端看到的是 ring.New。利用包结构来帮助你选择好的名称。
一个简短的例子是 once.Doonce.Do(setup) 读起来很好,而写成 once.DoOrWaitUntilDone(setup) 并不会改善可读性。长名称并不总是能提高可读性;一个有帮助的文档注释往往比额外的长名称更有价值。

Getter

Go 不提供自动支持的 getter 和 setter。自己提供 getter 和 setter 是没有问题的,通常也是合适的,但在命名时不需要在 getter 的名称中加上 Get。如果你有一个名为 owner(小写,未导出)的字段,则 getter 方法应称为 Owner(大写,导出),而不是 GetOwner。使用大写名称进行导出提供了区分字段和方法的依据。如果需要 setter 函数,通常会称为 SetOwner。这两个名称在实践中读起来都很好。
owner := obj.Owner() if owner != user { obj.SetOwner(user) }
【译者注: 对于常见的面向对象开发语言,如 JAVA、C++ 等,一般都会含有类的属性操作方法。这里的 Getter 和 Setter 指的就是这个写方法。由于 go 即使面向对象的开发语言也是面向对象的语言,它在此方面的限制很宽松,或者你可以认为它没有这个类似的方法,它只有私有和公开的区分,也就是函数名称首字母是否大写】

接口名称

根据约定,单方法接口的命名方式是将方法名加上后缀 -er 或类似的修改,以构造出代理名词,例如 ReaderWriterFormatterCloseNotifier 等。
有许多这样的名称,遵循这些名称及其所捕获的函数名称是很有益的。ReadWriteCloseFlushString 等都有标准的签名和含义。为了避免混淆,除非你的方法具有相同的签名和含义,否则不要给你的方法使用这些名称。相反,如果你的类型实现了与已知类型中的方法相同含义的方法,应该使用相同的名称和签名;例如,将字符串转换的方法命名为 String 而不是 ToString

MixedCaps(大小写混合方式命名)

最后,Go 中的约定是使用 MixedCaps 或 mixedCaps 来书写多词名称,而不是使用下划线。
【译者注: 此部分主要是命名规范, go 约定使用驼峰命名法,例如: getName、 SetName 等,不建议使用 get_name 或者 get-name 等】

分号

与 C 语言类似,Go 的正式语法使用分号来终止语句,但与 C 不同的是,这些分号在源代码中并不出现。相反,词法分析器使用简单规则在扫描时自动插入分号,因此输入文本大多不包含分号。
规则如下:如果在换行符之前的最后一个令牌是标识符(包括像 int 和 float64 这样的词)、基本字面量(如数字或字符串常量),或某些特定的令牌,那么在该位置将自动插入一个分号。
break continue fallthrough return ++ -- ) }
词法分析器总是在令牌后插入分号。这可以总结为:“如果换行符出现在一个可能结束语句的令牌后,则插入一个分号。”
分号在闭合大括号前也可以省略,因此像这样的语句:
go func() { for { dst <- <-src } }()
不需要分号。惯用的 Go 程序中,分号只出现在 for 循环的条件部分,用于分隔初始化器、条件和继续元素。如果你在一行中写多个语句,分号也是必要的。
分号插入规则的一个结果是,你不能将控制结构(如 ifforswitch 或 select)的开括号放在下一行。如果这样做,括号前会插入一个分号,这可能导致意想不到的效果。应这样书写:
if i < f() { g() }
不应这样书写:
if i < f() // wrong! { // wrong! g() }
【译者注:这部分介绍了 go 为了减少开发者输入,将分号进行了自动添加,如果你 C 或者其它编程语言经验,这部分需要注意一下。】