引言
几乎所有的编程语言都有一种向代码添加注释的语法,Go也不例外。注释(comment)是程序中使用人类语言解释代码如何工作或为什么要这样写的行。编译器会忽略它们,但细心的程序员不会。注释添加了宝贵的上下文,可以帮助您的合作者(以及您未来的自己)避免陷阱并编写更可维护的代码。
任何包中的普通注释都解释了该代码为什么做它所做的事情。它们是针对包开发人员的注意事项和警告。文档注释总结了包中每个组件的功能以及工作原理,并提供了示例代码和命令用法。它们是用户的官方包文档。
在本文中,我们将从几个Go包中查看一些真实的注释,不仅说明注释在Go中是什么样子的,还说明它们应该传达什么。
普通的评论
Go中的注释以两个斜杠(//
)开始,然后是一个空格(不是必需的,但习惯用法),然后是注释。它可能出现在所涉及代码的上方或右侧。在上面,它会缩进以与代码对齐。
这个Hello World程序在它自己的一行中只包含一个注释:
hello.go
package mainimport "fmt"func main() {// 通过控制台打招呼fmt.Println("Hello, World!")
}
**注意:**如果你添加了与代码不一致的注释,gofmt
工具会解决这个问题。该工具随您的Go安装一起提供,将Go代码(包括注释)格式化为通用格式,以便任何地方的Go代码看起来都是相同的,程序员不会因为制表符和空格而争论。作为一名Gopher (Go爱好者的称呼),您应该在编写Go代码时不断格式化代码,并且在将其提交到版本控制系统之前。你可以手动运行gofmt
(gofmt -w hello.go
),但更方便的是配置你的文本编辑器或IDE,使其在每次保存文件时运行。
由于这段注释很短,它可以作为行内注释出现在代码的右侧:
hello.go
. . .fmt.Println("Hello, World!") // 通过控制台打招呼
. . .
大多数注释都单独出现在一行中,除非它们非常简短。
较长的注释跨越多行。Go支持c风格的块注释,使用/*
和*/
标签来打开和关闭非常长的注释,但这些仅用于特殊情况。(稍后会详细介绍。)普通的多行注释以//
开头,而不是使用块注释标签。
下面是一些带有许多注释的代码,每个注释都正确缩进。其中一个多行注释被突出显示:
color.go
package mainimport "fmt"const favColor string = "blue" // Could have chosen any colorfunc main() {var guess string// Create an input loopfor {// Ask the user to guess my favorite colorfmt.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语法、控制流、数据类型等基础知识。你不需要写注释来宣布代码将要遍历一个切片或将两个浮点数相乘。
然而,其中有一条注释是有用的。
好的评论可以解释为什么
在任何程序中,最有用的注释都不是解释代码做了什么或如何做,而是解释为什么这样做。有时没有为什么,即使这样也可以指出来,就像下面这段行内注释所做的那样:
color.go
const favColor string = "blue" // 可以选择任何颜色吗
这段注释说明了代码中没有的东西:值“blue”
是程序员任意选择的。换句话说,//可以随意更改
。
然而,大多数代码都有一个为什么。这是Go标准库中的net/http
包中的一个函数,其中包含两个非常有用的注释:
client.go
. . .
// 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 lineauth := lastReq.User.String() + "@"referer = strings.Replace(referer, auth, "", 1)}return referer
}
. . .
第一个突出显示的注释警告维护者不要更改下面的代码,因为它是为了符合HTTP协议的RFC(官方规范)而编写的。第二个高亮的注释承认下面的代码并不理想,暗示了维护者可能会如何尝试改进它,并警告他们这样做的危险。
这样的注释是必不可少的。它们防止维护者在不知情的情况下引入bug和其他问题,同时也可能邀请他们实现新的想法,但要谨慎。
func
声明上面的注释也很有用,但方式不同。让我们接下来探讨这种评论。
文档注释
出现在顶级(非缩进)声明之上的注释,如package
、func
、const
、var
和type
,被称为文档注释。之所以这样命名,是因为它们代表了包及其所有导出名称的官方文档。
注意:在Go中,exported的意思与public在某些语言中的意思相同:导出的组件是其他包在导入你的包时可能会使用的组件。要导出包中的任何顶级名称,只需将其大写即可。
文档注释解释了做什么和怎么做
与我们刚才看到的普通注释不同,文档注释通常会解释代码做什么或如何做。这是因为它们不是为包的维护者准备的,而是为它的用户准备的,这些用户通常不想阅读或贡献代码。
用户通常会在以下三个地方阅读文档注释:
-
在他们的本地终端中,通过在单个源文件或目录上运行
go doc
。 -
在pkg.go.dev上,任何公开的Go包的官方文档中心。
-
在您的团队使用
godoc
工具托管的私人运行的web服务器上。此工具可让您的团队为私有Go包创建自己的参考门户。
在开发Go包时,您应该为每个导出的名称编写文档注释。(CodeReviewComments。)这是godo
(DigitalOcean API的Go客户端库)中的一行文档注释:
godo.go
// Client manages communication with DigitalOcean V2 API.
type Client struct {
像这样简单的文档注释似乎没有必要,但请记住,它将与所有其他文档注释一起出现,以全面记录包的每个可用组件。
下面是这个包的一段较长的文档注释:
godo.go
// 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
函数的文档注释与函数内部的注释进行比较:
godo.go
// 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
包中看到的评论。阅读这段代码的维护者可能会想,“为什么这段代码不检查错误呢?,然后添加错误检查。但是这条评论解释了为什么他们不需要这样做。它不像文档注释那样是高级的** what 或 how **。
最高级别的文档注释是包注释。每个包都应该只包含一个包注释,概述包是什么以及如何使用它,包括代码和/或命令示例。包注释可以出现在任何源文件中,而且只能出现在package <name>
声明之上的那个文件中。包注释通常出现在名为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的文档字符串和Java的Javadoc有关,但它的设计更简单。godoc读取的注释不是语言结构(像Docstring那样),也必须有自己的机器可读的语法(像Javadoc那样)。Godoc注释只是好的注释,即使Godoc不存在,你也会想要阅读的那种注释。
虽然文档注释没有必须的格式,但它们可以选择使用Go文档中完全描述的“Markdown的简化子集”格式。在文档注释中,要以段落和列表的形式书写,以缩进的形式显示示例代码或命令,提供引用的链接等。当文档注释按照这种格式结构良好时,它们就可以呈现为漂亮的网页。
以下是添加到[如何用Go编写第一个程序]中的扩展Hello World程序greeting.go
中的一些注释。
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 mainimport ("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 stringfmt.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 mainimport ("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 stringfmt.Scanln(&name)name = strings.TrimSpace(name)fmt.Printf("Hi, %s! I'm Go!", name)
}
注意:
-
文档注释第一段中列出的项目现在对齐了。
-
第一段和第二段之间现在有一个空行。
3.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风格的/*
和*/
标签。你可以通过在代码前面加一个/*
、后面加一个*/
来快速禁用有问题的代码。用这些标签把有问题的代码包裹起来,把它变成一个块注释。然后,当您修复了它导致的任何问题后,您可以通过删除这两个标签重新启用代码。
problematic.go
. . .
func main() {x := initializeStuff()/* This code is causing problems, so we're going to comment it out for nowsomeProblematicCode(x)*/fmt.Println("This code is still running!")
}
对于较长的代码块,使用这些标签比在每行有问题的代码的开头添加//
要方便得多。作为一种约定,使用//
表示普通注释和文档注释,它们将无限期地存在于代码中。仅在测试期间临时使用/*
和*/
标签(或如前所述用于包注释)。不要在程序中长时间地留下注释代码片段。
总结
通过在你所有的Go程序中编写富有表现力的注释,你可以:
-
防止你的合作者破坏东西。
-
帮助未来的自己,他有时已经忘记代码最初为什么要这样写。
-
给包的用户一个参考,他们可以在不深入代码的情况下阅读。