Go中如何编写评论
引言
几乎所有的编程语言都有一种语法来添加代码注释,Go语言也不例外。注释是程序中用人类语言解释代码如何工作或为何如此编写的行。编译器会忽略它们,但仔细的程序员不会。注释提供了宝贵的背景信息,帮助你的合作者和未来的自己避免陷阱,并编写更易于维护的代码。
在任何程序包中,普通的注释解释了代码执行的原因。它们是针对程序包开发人员的注解和警告。文档注释总结了程序包中每个组件的功能和工作原理,并提供示例代码和命令用法。它们是给用户使用的官方程序包文档。
在本文中,我们将看一些来自几个Go软件包的真实评论,以说明Go语言中的评论的样式以及它们应该传达的内容。
普通评论
在Go语言中,注释以两个斜杠(//)开始,紧跟一个空格(不是必需的,但习惯如此),然后是注释的内容。注释可以出现在代码的上方或右侧。当在上方时,注释会缩进以与代码对齐。
这个Hello World程序在自己的一行上包含了一个单独的注释。
package main
import "fmt"
func main() {
// Say hi via the console
fmt.Println("Hello, World!")
}
Note
由于这个评论非常简短,它可以作为行内评论出现在代码的右侧。
. . .
fmt.Println("Hello, World!") // Say hi via the console
. . .
除非评论非常简洁,否则大多数评论都会单独显示在一行上。
较长的评论跨越多行。Go语言使用/*和*/标记支持C语言风格的块注释来打开和关闭非常长的注释,但这仅在特殊情况下使用(稍后详述)。普通的多行注释以//开头的方式,而不是使用块注释标记。
下面是一些带有许多注释的代码,每个注释都被正确缩进。其中一个多行注释被突出显示:
颜色。去。
(Note: “color.go” is a very short and vague phrase in English, and it is not clear what the intended meaning is. The provided paraphrase is a literal translation of the phrase, but it may not convey a clear message in Chinese.)
package main
import "fmt"
const favColor string = "blue" // Could have chosen any color
func main() {
var guess string
// Create an input loop
for {
// Ask the user to guess my favorite color
fmt.Println("Guess my favorite color:")
// Try to read a line of input from the user.
// Print out an error and exit, if there is one.
if _, err := fmt.Scanln(&guess); err != nil {
fmt.Printf("%s\n", err)
return
}
// Did they guess the correct color?
if favColor == guess {
// They guessed it!
fmt.Printf("%q is my favorite color!\n", favColor)
return
}
// Wrong! Have them guess again.
fmt.Printf("Sorry, %q is not my favorite color. Guess again.\n", guess)
}
}
这些评论大部分都是杂乱的。这样一个小而简单的程序不应该包含这么多评论,而且大多数评论都说出了代码本身已经显而易见的内容。你可以相信其他的Go程序员已经了解Go语法、控制流、数据类型等基础知识。你不需要写一个评论来宣告代码将要对切片进行迭代或者对两个浮点数进行乘法运算。
然而,其中一条评论却是有用的。
好的评论是解释为什么的。
任何程序中最有用的评论不是解释代码做什么或是如何实现的,而是解释为什么这样做。有时候并没有为什么,甚至这一点也可以通过内联注释指出并提供有价值的提示。
const favColor string = "blue" // Could have chosen any color
这条评论表达了一些代码无法说明的事情:程序员是随机选择了“蓝色”这个值。换句话说,// 随意修改。
大多数代码都有它的原因。这里是一个在Go标准库的net/http包中的一个函数,它包含了两个非常有帮助的注释。
. . .
// refererForURL returns a referer without any authentication info or
// an empty string if lastReq scheme is https and newReq scheme is http.
func refererForURL(lastReq, newReq *url.URL) string {
// https://tools.ietf.org/html/rfc7231#section-5.5.2
// "Clients SHOULD NOT include a Referer header field in a
// (non-secure) HTTP request if the referring page was
// transferred with a secure protocol."
if lastReq.Scheme == "https" && newReq.Scheme == "http" {
return ""
}
referer := lastReq.String()
if lastReq.User != nil {
// This is not very efficient, but is the best we can
// do without:
// - introducing a new method on URL
// - creating a race condition
// - copying the URL struct manually, which would cause
// maintenance problems down the line
auth := lastReq.User.String() + "@"
referer = strings.Replace(referer, auth, "", 1)
}
return referer
}
. . .
第一个突出显示的评论警告维护者不要更改下面的代码,因为它是有意按照HTTP协议的RFC(官方规范)编写的。第二个突出显示的评论承认下面的代码并不理想,暗示维护者可以尝试改进并警告他们这样做的危险性。
像这样的评论是必不可少的。它们可以防止维护者无意中引入错误和其他问题,同时也可能鼓励他们谨慎实施新的想法。
上面函数声明前的注释也很有帮助,只是以不同的方式。让我们接下来探索这种类型的注释。
文档注释
在包、函数、常量、变量和类型等最高级别(非缩进)声明的正上方出现的注释被称为文档注释。它们之所以被称为文档注释,是因为它们代表了一个包及其全部导出名称的官方文档。
Note
文档评论阐明了什么和如何
与我们刚才看到的普通评论不同,文档注释通常解释了代码的功能或实现方式。这是因为它们并不是给包的维护者而是给最终用户看的,而大多数用户通常不想阅读或贡献代码。
用户通常会在三个地方阅读您的文档注释:
-
- 在本地终端上,通过在单个源文件或目录上运行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 {
像这样的简单文档注释可能看起来不必要,但请记住它将与您的所有其他文档注释一起显示,以全面记录包中的每个可用组件。
以下是包的详细文档注释。
// Do sends an API request and returns the API response. The API response is JSON decoded and stored in the value
// pointed to by v, or returned as an error if an API error has occurred. If v implements the io.Writer interface,
// the raw response will be written to v, without attempting to decode it.
func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
. . .
}
函数的文档注释应明确指定参数的预期格式(当不明显时),以及函数返回数据的格式。还可以概述函数的工作原理。
将Do函数的文档注释与函数内的注释进行比较。
// Ensure the response body is fully read and closed
// before we reconnect, so that we reuse the same TCPConnection.
// Close the previous response's body. But read at least some of
// the body so if it's small the underlying TCP connection will be
// re-used. No need to check for errors: if it fails, the Transport
// won't reuse it anyway.
这就像我们在net/http包中看到的注释一样。一个维护者在阅读这条注释下面的代码时可能会想:“为什么不检查错误?”,然后添加错误检查。但是注释解释了为什么他们不需要这样做。它不是高级的“做什么”或“如何做”,而是文档注释。
非常高级别的文档注释是包注释。每个包都应包含一个高级别概述该包的用途以及如何使用它的包注释,其中包括代码和/或命令示例。包注释可以出现在任何源文件中,但仅限于在包<名称>声明之前的那个文件中。通常一个包注释会出现在名为doc.go的独立文件中。
与我们查看的其他评论不同,包注释通常使用/*和*/,因为它们可能会很长。这是gofmt的包注释的开头。
/*
Gofmt formats Go programs.
It uses tabs for indentation and blanks for alignment.
Alignment assumes that an editor is using a fixed-width font.
Without an explicit path, it processes the standard input. Given a file,
it operates on that file; given a directory, it operates on all .go files in
that directory, recursively. (Files starting with a period are ignored.)
By default, gofmt prints the reformatted sources to standard output.
Usage:
gofmt [flags] [path ...]
The flags are:
-d
Do not print reformatted sources to standard output.
If a file's formatting is different than gofmt's, print diffs
to standard output.
. . .
*/
package main
那么文档注释的格式是怎样的呢?它们可以有什么样的结构(或者必须有什么样的结构)?
文档注释有一个格式。
根据Go语言的创始人在一篇古老的博客文章中所述:
Godoc在概念上与Python的Docstring和Java的Javadoc有关,但其设计更简单。Godoc读取的注释不是语言结构(如Docstring),也不必具有自己的可机器读取的语法(如Javadoc)。Godoc注释只是好的注释,即使没有godoc,您也希望阅读的注释。
虽然文档注释没有必须的格式要求,但可以选择使用一种“简化的 Markdown 子集”格式,该格式在 Go 文档中有详细的描述。在你的文档注释中,你可以使用段落和列表,展示缩进的代码块或命令,提供引用链接等。当文档注释按照这种格式良好结构化时,它们可以呈现为漂亮的网页页面。
以下是一些添加到extended Hello World程序greeting.go的评论:《如何用Go编写你的第一个程序》
// This is a doc comment for greeting.go.
// - prompt user for name.
// - wait for name
// - print name.
// This is the second paragraph of this doc comment.
// `gofmt` (and `go doc`) will insert a blank line before it.
package main
import (
"fmt"
"strings"
)
func main() {
// This is not a doc comment. Gofmt will NOT format it.
// - prompt user for name
// - wait for name
// - print name
// This is not a "second paragraph" because this is not a doc comment.
// It's just more lines to this non-doc comment.
fmt.Println("Please enter your name.")
var name string
fmt.Scanln(&name)
name = strings.TrimSpace(name)
fmt.Printf("Hi, %s! I'm Go!", name)
}
上面 package main 的注释是一个文档注释。它试图使用文档注释格式中的段落和列表的概念,但还不太正确。gofmt 工具将会对其进行格式化处理。运行 gofmt greeting.go 将打印出如下结果:
// This is a doc comment for greeting.go.
// - prompt user for name.
// - wait for name.
// - print name.
//
// This is the second paragraph of this doc comment.
// `gofmt` (and `go doc`) will insert a blank line before it.
package main
import (
"fmt"
"strings"
)
func main() {
// This is not a doc comment. `gofmt` will NOT format it.
// - prompt user for name
// - wait for name
// - print name
// This is not a "second paragraph" because this is not a doc comment.
// It's just more lines to this non-doc comment.
fmt.Println("Please enter your name.")
var name string
fmt.Scanln(&name)
name = strings.TrimSpace(name)
fmt.Printf("Hi, %s! I'm Go!", name)
}
请注意:
-
- 在文档注释的第一段中列出的项目现在已经对齐。
-
- 第一段和第二段之间现在有一个空行。
- 在main()内的注释没有被格式化,因为gofmt识别出它不是一个文档注释。(但正如前面提到的,gofmt会将所有注释与代码对齐到相同的缩进位置。)
运行 go doc greeting.go 将会格式化并打印文档注释,但不会打印 main() 函数内的注释。
This is a doc comment for greeting.go.
- prompt user for name.
- wait for name.
- print name.
This is the second paragraph of this doc comment. `gofmt` (and `go doc`) will
insert a blank line before it.
如果您始终并恰当地使用这种文档注释格式,您的软件包的用户将感谢您提供易于阅读的文档。
阅读官方参考页面上的文档评论,学习如何写好它们的全部知识。
迅速禁用代码
你是否有过编写新代码而导致应用程序变慢或者更糟糕的情况,导致应用程序崩溃?这就是使用C语言风格的/* 和 */标记的另一个场合。你可以通过在代码前加一个/*,并在代码后加一个*/来快速禁用有问题的代码。将这些标记包裹在有问题的代码周围,将其转换为块注释。然后,当你修复了引起问题的代码后,可以通过删除这两个标记来重新启用该代码。
. . .
func main() {
x := initializeStuff()
/* This code is causing problems, so we're going to comment it out for now
someProblematicCode(x)
*/
fmt.Println("This code is still running!")
}
对于较长的代码段来说,使用这些标签比在问题代码的每一行开头添加//要方便得多。按照惯例,使用//来进行普通评论和将在代码中永久存在的文档评论。只在测试期间(或用于包注释,如前所述)临时使用/*和*/标签。不要长时间保留程序中被注释掉的代码片段。
结论
通过在所有的Go程序中编写富有表达力的注释,你会:
-
- 防止合作伙伴出错。
-
- 帮助自己将来,有时候会忘记为什么代码最初是这样编写的。
- 为包的用户提供可阅读的参考,而无需深入研究代码。