Go语言注释:提升代码可读性与维护性的最佳实践
引言
几乎所有的编程语言都有一种语法来添加代码注释,Go语言也不例外。注释是程序中用人类语言解释代码如何工作或为何如此编写的行。编译器会忽略它们,但细心的程序员不会。注释提供了宝贵的背景信息,帮助你的合作者和未来的自己避免陷阱,并编写更易于维护的代码。
在任何程序包中,普通注释解释了代码执行的原因。它们是针对程序包开发人员的注解和警告。文档注释总结了程序包中每个组件的功能和工作原理,并提供示例代码和命令用法。它们是给用户使用的官方程序包文档。
在本文中,我们将看一些来自几个Go软件包的真实注释,以说明Go语言中注释的样式以及它们应该传达的内容。
普通注释
在Go语言中,注释以两个斜杠(//
)开始,紧跟一个空格(不是必需的,但习惯如此),然后是注释的内容。注释可以出现在代码的上方或右侧。当在上方时,注释会缩进以与代码对齐。
这个Hello World程序在自己的一行上包含了一个单独的注释。
package main
import "fmt"
func main() {
// 通过控制台打招呼
fmt.Println("Hello, World!")
}
注意:如果您添加的注释与代码不一致,gofmt
工具将会修复它。这个工具是您Go安装中提供的,它会将Go代码(包括注释)格式化为通用格式,以便所有地方的Go代码看起来相同,程序员们不用争论制表符和空格的问题。作为Go的爱好者(也称为Gopher),您应该在编写代码时不断地对其进行格式化,并在提交到版本控制之前进行格式化。您可以手动运行gofmt
(例如:gofmt -w hello.go
),但更方便的做法是配置您的文本编辑器或集成开发环境,在保存文件时自动运行它。
由于这个注释非常简短,它可以作为行内注释出现在代码的右侧。
. . .
fmt.Println("Hello, World!") // 通过控制台打招呼
. . .
除非注释非常简洁,否则大多数注释都会单独显示在一行上。
较长的注释会跨越多行。Go语言使用/*
和*/
标记支持C语言风格的块注释来打开和关闭非常长的注释,但这仅在特殊情况下使用(稍后详述)。普通的多行注释以//
开头的方式,而不是使用块注释标记。
下面是一些带有许多注释的代码,每个注释都被正确缩进。其中一个多行注释被突出显示:
这是文章《Go中如何编写注释》的第2部分(共6部分)。
package main
import "fmt"
const favColor string = "blue" // 可以选择任何颜色
func main() {
var guess string
// 创建一个输入循环
for {
// 询问用户猜测我最喜欢的颜色
fmt.Println("猜猜我最喜欢的颜色:")
// 尝试从用户读取一行输入。
// 如果有错误,则打印错误并退出。
if _, err := fmt.Scanln(&guess); err != nil {
fmt.Printf("%s\n", err)
return
}
// 他们猜对颜色了吗?
if favColor == guess {
// 他们猜对了!
fmt.Printf("%q 是我最喜欢的颜色!\n", favColor)
return
}
// 错了!让他们再猜一次。
fmt.Printf("抱歉,%q 不是我最喜欢的颜色。请再猜一次。\n", guess)
}
}
这些注释大部分都是多余的。这样一个小型简单的程序不应该包含如此多的注释,而且大多数注释都说明了代码本身已经显而易见的内容。你可以相信其他的Go程序员已经了解Go语法、控制流、数据类型等基础知识。你不需要写一个注释来声明代码将要对切片进行迭代或者对两个浮点数进行乘法运算。
然而,其中一条注释却是有用的。
好的注释是解释“为什么”的。
任何程序中最有用的注释不是解释代码“做什么”或“如何实现”,而是解释“为什么”这样做。有时候并没有“为什么”,甚至这一点也可以通过内联注释指出并提供有价值的提示。
const favColor string = "blue" // 可以选择任何颜色
这条注释表达了一些代码无法说明的事情:程序员是随机选择了“蓝色”这个值。换句话说,// 随意修改。
大多数代码都有其背后的原因。这里是一个在Go标准库的net/http
包中的一个函数,它包含了两个非常有帮助的注释。
(此处应为原文中“客户·前往”所指的Go标准库代码示例,但原文未提供具体代码,故此处仅作说明。)
. . .
// refererForURL 返回一个不带任何认证信息的引用者,如果 lastReq 的 scheme 是 https 而 newReq 的 scheme 是 http,则返回空字符串。
func refererForURL(lastReq, newReq *url.URL) string {
// https://tools.ietf.org/html/rfc7231#section-5.5.2
// “如果引用页面是通过安全协议传输的,客户端不应在(非安全)HTTP 请求中包含 Referer 头字段。”
if lastReq.Scheme == "https" && newReq.Scheme == "http" {
return ""
}
referer := lastReq.String()
if lastReq.User != nil {
// 这种方式效率不高,但我们只能这样做,除非:
// - 在 URL 上引入一个新方法
// - 产生竞态条件
// - 手动复制 URL 结构体,这会给后续维护带来问题
auth := lastReq.User.String() + "@"
referer = strings.Replace(referer, auth, "", 1)
}
return referer
}
. . .
第一个突出显示的注释警告维护者不要更改下面的代码,因为它是有意按照 HTTP 协议的 RFC(官方规范)编写的。第二个突出显示的注释承认下面的代码并不理想,暗示维护者可以尝试改进并警告他们这样做的危险性。
像这样的注释是必不可少的。它们可以防止维护者无意中引入错误和其他问题,同时也可能鼓励他们谨慎实施新的想法。
上面函数声明前的注释也很有帮助,只是以不同的方式。让我们接下来探索这种类型的注释。
文档注释
在包、函数、常量、变量和类型等最高级别(非缩进)声明的正上方出现的注释被称为文档注释。它们之所以被称为文档注释,是因为它们代表了一个包及其全部导出名称的官方文档。
注意:在 Go 语言中,公开(exported)与其他一些语言中的公共(public)意思相同:一个公开的组件表示其他包可以在导入你的包时使用它。要将包中的任何顶层名称公开,只需要将它首字母大写即可。
文档注释阐明了什么和如何
与我们刚才看到的普通注释不同,文档注释通常解释了代码的功能或实现方式。这是因为它们并不是给包的维护者而是给最终用户看的,而大多数用户通常不想阅读或贡献代码。
用户通常会在三个地方阅读您的文档注释:
- 在本地终端上,通过在单个源文件或目录上运行
go doc
命令。 - 在 pkg.go.dev 上,这是任何公共 Go 包的官方文档中心。
- 在由您的团队使用
godoc
工具托管的私人 Web 服务器上。此工具允许您的团队为私有 Go 包创建自己的参考门户。
在开发 Go 软件包时,您应该为每个被导出的名称编写一个文档注释(偶尔也需要为未导出的名称编写)。这是 godo 的一行文档注释,godo 是 Silicon Cloud API 的 Go 客户端库。
// Client manages communication with Silicon Cloud V2 API.
type Client struct {
像这样的简单文档注释可能看起来不必要,但请记住它将与您的所有其他文档注释一起显示,以全面记录包中的每个可用组件。
以下是包的详细文档注释。
这是文章《Go中如何编写评论》的第4部分(共6部分)。
// Do发送API请求并返回API响应。API响应会被JSON解码并存储在v指向的值中,
// 如果发生API错误,则返回错误。如果v实现了io.Writer接口,
// 原始响应将写入v,而不尝试解码。
func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
. . .
}
函数的文档注释应明确指定参数的预期格式(当不明显时),以及函数返回数据的格式。还可以概述函数的工作原理。
将Do函数的文档注释与函数内部的注释进行比较。
// 确保响应体在重新连接之前被完全读取和关闭,
// 以便我们重用相同的TCP连接。
// 关闭前一个响应的body。但至少读取部分body,
// 这样如果它很小,底层的TCP连接将被重用。
// 无需检查错误:如果失败,Transport无论如何都不会重用它。
这就像我们在net/http
包中看到的注释一样。一个维护者在阅读这条注释下面的代码时可能会想:“为什么不检查错误?”,然后添加错误检查。但是注释解释了为什么他们不需要这样做。它不是高级的“做什么”或“如何做”,而是文档注释。
非常高级别的文档注释是包注释。每个包都应包含一个高级概述,说明该包的用途以及如何使用它,其中包括代码和/或命令示例。包注释可以出现在任何源文件中,但仅限于在package <名称>
声明之前的那个文件中。通常一个包注释会出现在名为doc.go
的独立文件中。
与我们查看的其他注释不同,包注释通常使用/*
和*/
,因为它们可能会很长。这是gofmt
的包注释的开头。
/*
Gofmt格式化Go程序。
它使用制表符进行缩进,使用空格进行对齐。
对齐假定编辑器使用等宽字体。
如果没有显式路径,它会处理标准输入。给定一个文件,
它会操作该文件;给定一个目录,它会递归操作该目录中所有.go文件。
(以句点开头的文件将被忽略。)
默认情况下,gofmt将重新格式化的源代码打印到标准输出。
用法:
gofmt [flags] [path ...]
标志包括:
-d
不打印重新格式化的源代码到标准输出。
如果文件的格式与gofmt的不同,则将差异打印到标准输出。
. . .
*/
package main
那么文档注释的格式是怎样的呢?它们可以有什么样的结构(或者必须有什么样的结构)?
文档注释有一个格式。根据Go语言的创始人在一篇古老的博客文章中所述:
Godoc在概念上与Python的Docstring和Java的Javadoc有关,但其设计更简单。Godoc读取的注释不是语言结构(如Docstring),也不必具有自己的可机器读取的语法(如Javadoc)。Godoc注释只是好的注释,即使没有godoc,您也希望阅读的注释。
虽然文档注释没有必须的格式要求,但可以选择使用一种“简化的 Markdown 子集”格式,该格式在 Go 文档中有详细的描述。在你的文档注释中,你可以使用段落和列表,展示缩进的代码块或命令,提供引用链接等。当文档注释按照这种格式良好结构化时,它们可以呈现为漂亮的网页页面。
以下是一些添加到扩展版“Hello World”程序greeting.go
的注释:《如何用Go编写你的第一个程序》
这是文章《Go中如何编写注释》的第5部分(共6部分)。
// 这是 greeting.go 文件的文档注释。
// - 提示用户输入姓名。
// - 等待姓名输入
// - 打印姓名。
// 这是此文档注释的第二段。
// `gofmt` (和 `go doc`) 将在其之前插入一个空行。
package main
import (
"fmt"
"strings"
)
func main() {
// 这不是文档注释。Gofmt 不会对其进行格式化。
// - 提示用户输入姓名
// - 等待姓名输入
// - 打印姓名
// 这不是“第二段”,因为它不是文档注释。
// 它只是这个非文档注释的更多行。
fmt.Println("请输入您的姓名。")
var name string
fmt.Scanln(&name)
name = strings.TrimSpace(name)
fmt.Printf("你好,%s!我是 Go!", name)
}
上面 package main
的注释是一个文档注释。它试图使用文档注释格式中的段落和列表概念,但还不太正确。gofmt
工具将会对其进行格式化处理。运行 gofmt greeting.go
将打印出如下结果:
// 这是 greeting.go 文件的文档注释。
// - 提示用户输入姓名。
// - 等待姓名输入。
// - 打印姓名。
//
// 这是此文档注释的第二段。
// `gofmt` (和 `go doc`) 将在其之前插入一个空行。
package main
import (
"fmt"
"strings"
)
func main() {
// 这不是文档注释。`gofmt` 不会对其进行格式化。
// - 提示用户输入姓名
// - 等待姓名输入
// - 打印姓名
// 这不是“第二段”,因为它不是文档注释。
// 它只是这个非文档注释的更多行。
fmt.Println("请输入您的姓名。")
var name string
fmt.Scanln(&name)
name = strings.TrimSpace(name)
fmt.Printf("你好,%s!我是 Go!", name)
}
请注意:
- 在文档注释的第一段中列出的项目现在已经对齐。
- 第一段和第二段之间现在有一个空行。
在 main()
函数内的注释没有被格式化,因为 gofmt
识别出它不是一个文档注释。(但正如前面提到的,gofmt
会将所有注释与代码对齐到相同的缩进位置。)
运行 go doc greeting.go
将会格式化并打印文档注释,但不会打印 main()
函数内的注释。
这是一个用于greeting.go的文档注释。
- 提示用户输入姓名。
- 等待输入姓名。
- 打印姓名。
这是此文档注释的第二段。`gofmt`(和`go doc`)会在其前面插入一个空行。
如果您始终并恰当地使用这种文档注释格式,您的软件包用户将感谢您提供易于阅读的文档。
请阅读官方参考页面上的文档注释,学习如何写好它们的全部知识。
快速禁用代码:你是否曾编写新代码导致应用程序变慢,甚至更糟,导致应用程序崩溃?
这就是使用C语言风格的`/*`和`*/`标记的另一个场合。你可以通过在代码前添加`/*`,并在代码后添加`*/`来快速禁用有问题的代码。将这些标记包裹在有问题的代码周围,将其转换为块注释。然后,当你修复了引起问题的代码后,可以通过删除这两个标记来重新启用该代码。
. . .
func main() {
x := initializeStuff()
/* 这段代码导致了问题,所以我们暂时将其注释掉
someProblematicCode(x)
*/
fmt.Println("这段代码仍在运行!")
}
对于较长的代码段来说,使用这些标签比在问题代码的每一行开头添加`//`要方便得多。按照惯例,使用`//`来进行普通注释和将在代码中永久存在的文档注释。只在测试期间(或用于包注释,如前所述)临时使用`/*`和`*/`标签。不要长时间保留程序中被注释掉的代码片段。
结论
通过在所有的Go程序中编写富有表达力的注释,你会:
- 防止合作伙伴出错。
- 帮助自己将来,有时候会忘记为什么代码最初是这样编写的。
- 为包的用户提供可阅读的参考,而无需深入研究代码。