引言
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.Do
;once.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
或类似的修改,以构造出代理名词,例如 Reader
、Writer
、Formatter
、CloseNotifier
等。有许多这样的名称,遵循这些名称及其所捕获的函数名称是很有益的。
Read
、Write
、Close
、Flush
、String
等都有标准的签名和含义。为了避免混淆,除非你的方法具有相同的签名和含义,否则不要给你的方法使用这些名称。相反,如果你的类型实现了与已知类型中的方法相同含义的方法,应该使用相同的名称和签名;例如,将字符串转换的方法命名为 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
循环的条件部分,用于分隔初始化器、条件和继续元素。如果你在一行中写多个语句,分号也是必要的。分号插入规则的一个结果是,你不能将控制结构(如
if
、for
、switch
或 select
)的开括号放在下一行。如果这样做,括号前会插入一个分号,这可能导致意想不到的效果。应这样书写:if i < f() { g() }
不应这样书写:
if i < f() // wrong! { // wrong! g() }
【译者注:这部分介绍了 go 为了减少开发者输入,将分号进行了自动添加,如果你 C 或者其它编程语言经验,这部分需要注意一下。】