如何在Go中编写注释

引言

几乎所有的编程语言都有一种向代码添加注释的语法,Go也不例外。注释(comment)是程序中使用人类语言解释代码如何工作或为什么要这样写的行。编译器会忽略它们,但细心的程序员不会。注释添加了宝贵的上下文,可以帮助您的合作者(以及您未来的自己)避免陷阱并编写更可维护的代码。

任何包中的普通注释都解释了该代码为什么做它所做的事情。它们是针对包开发人员的注意事项和警告。文档注释总结了包中每个组件的功能以及工作原理,并提供了示例代码和命令用法。它们是用户的官方包文档。

在本文中,我们将从几个Go包中查看一些真实的注释,不仅说明注释在Go中是什么样子的,还说明它们应该传达什么。

普通的评论

Go中的注释以两个斜杠(//)开始,然后是一个空格(不是必需的,但习惯用法),然后是注释。它可能出现在所涉及代码的上方或右侧。在上面,它会缩进以与代码对齐。

这个Hello World程序在它自己的一行中只包含一个注释:

hello.go

package main

import "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 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语法、控制流、数据类型等基础知识。你不需要写注释来宣布代码将要遍历一个切片或将两个浮点数相乘。

然而,其中有一条注释是有用的。

好的评论可以解释为什么

在任何程序中,最有用的注释都不是解释代码做了什么或如何做,而是解释为什么这样做。有时没有为什么,即使这样也可以指出来,就像下面这段行内注释所做的那样:

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 line
		auth := lastReq.User.String() + "@"
		referer = strings.Replace(referer, auth, "", 1)
	}
	return referer
}
. . .

第一个突出显示的注释警告维护者不要更改下面的代码,因为它是为了符合HTTP协议的RFC(官方规范)而编写的。第二个高亮的注释承认下面的代码并不理想,暗示了维护者可能会如何尝试改进它,并警告他们这样做的危险。

这样的注释是必不可少的。它们防止维护者在不知情的情况下引入bug和其他问题,同时也可能邀请他们实现新的想法,但要谨慎。

func声明上面的注释也很有用,但方式不同。让我们接下来探讨这种评论。

文档注释

出现在顶级(非缩进)声明之上的注释,如packagefuncconstvartype,被称为文档注释。之所以这样命名,是因为它们代表了包及其所有导出名称的官方文档。

注意:在Go中,exported的意思与public在某些语言中的意思相同:导出的组件是其他包在导入你的包时可能会使用的组件。要导出包中的任何顶级名称,只需将其大写即可。

文档注释解释了做什么怎么做

与我们刚才看到的普通注释不同,文档注释通常会解释代码做什么或如何做。这是因为它们不是为包的维护者准备的,而是为它的用户准备的,这些用户通常不想阅读或贡献代码。

用户通常会在以下三个地方阅读文档注释:

  1. 在他们的本地终端中,通过在单个源文件或目录上运行go doc

  2. 在pkg.go.dev上,任何公开的Go包的官方文档中心。

  3. 在您的团队使用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 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)
}

注意:

  1. 文档注释第一段中列出的项目现在对齐了。

  2. 第一段和第二段之间现在有一个空行。

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 now
    someProblematicCode(x)
    */
    fmt.Println("This code is still running!")
}

对于较长的代码块,使用这些标签比在每行有问题的代码的开头添加//要方便得多。作为一种约定,使用//表示普通注释和文档注释,它们将无限期地存在于代码中。仅在测试期间临时使用/**/标签(或如前所述用于包注释)。不要在程序中长时间地留下注释代码片段。

总结

通过在你所有的Go程序中编写富有表现力的注释,你可以:

  1. 防止你的合作者破坏东西。

  2. 帮助未来的自己,他有时已经忘记代码最初为什么要这样写。

  3. 给包的用户一个参考,他们可以在不深入代码的情况下阅读。

你可能感兴趣的:(Go,golang,数据库,开发语言)