res lots of comments. — Dave Thomas and Andrew Hunt, The Pragmatic Programmer 好的代码中附带有大量的注释,坏的代码缺少大量的注释。
代码注释对 Go 程序的可读性极为重要。一个注释应该做到如下三个方面的至少一个:
- 注释应该解释“做什么”。
- 注释应该解释“怎么做的”。
- 注释应该解释“为什么这么做”。
第一种形式适合公开的符号:
// Open opens the named file for reading.
// If successful, methods on the returned file can be used for reading.
第二种形式适合方法内的注释:
// queue all dependant actions
var results []chan error
for _, dep := range a.Deps {
results = append(results, execute(seen, dep))
}
第三种形式,“为什么这么做”,这是独一无二的,无法被前两种取代,也无法取代前两种。第三种形式的注释用于解释更多的状况,而这些状况往往难以脱离上下文,否则将没有意义,这些注释就是用来阐述上下文的。
return &v2.Cluster_CommonLbConfig{
// Disable HealthyPanicThreshold
HealthyPanicThreshold: &envoy_type.Percent{
Value: 0,
},
}
在这个示例中,很难立即弄清楚把HealthyPanicThreshold
的百分比设置为零会产生什么影响。注释就用来明确将值设置为0
实际上是禁用了panic
阈值的这种行为。
变量和常量上的注释应当描述它的内容,而非目的
我之前谈过,变量或常量的名称应描述其目的。向变量或常量添加注释时,应该描述变量的内容,而不是定义它的目的。
const randomNumber = 6 // determined from an unbiased die
这个示例的注释描述了“为什么”randomNumber
被赋值为 6,也说明了 6 这个值是从何而来的。但它没有描述randomNumber
会被用到什么地方。下面是更多的例子:
const (
StatusContinue = 100 // RFC 7231, 6.2.1
StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2
StatusProcessing = 102 // RFC 2518, 10.1
StatusOK = 200 // RFC 7231, 6.3.1
如在 RFC 7231 的第 6.2.1 节中定义的那样,在 HTTP 语境中 100 被当做StatusContinue
。
小窍门:对于那些没有初始值的变量,注释应当描述谁将负责初始化它们 // sizeCalculationDisabled indicates whether it is safe // to calculate Types' widths and alignments. See dowidth. var sizeCalculationDisabled bool 这里,通过注释让读者清楚函数dowidth
在负责维护sizeCalculationDisabled
的状态。 小窍门:隐藏一目了然的东西 Kate Gregory 提到一点^3,有时一个好的命名,可以省略不必要的注释。 // registry of SQL drivers var registry = make(mapstringsql.Driver) 注释是源码作者加的,因为registry
没能解释清楚定义它的目的——它是个注册表,但是什么的注册表? 通过重命名变量名为sqlDrivers
,现在我们很清楚这个变量的目的是存储 SQL 驱动。 var sqlDrivers = make(mapstringsql.Driver) 现在注释已经多余了,可以移除。
总是为公开符号写文档说明
因为 godoc 将作为您的包的文档,您应该总是为每个公开的符号写好注释说明——包括变量、常量、函数和方法——所有定义在您包内的公开符号。
这里是 Go 风格指南的两条规则:
- 任何既不明显也不简短的公共功能必须加以注释。
- 无论长度或复杂程度如何,都必须对库中的任何函数进行注释。
package ioutil
// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
func ReadAll(r io.Reader) ([]byte, error)
对这个规则有一个例外:您不需要为实现接口的方法进行文档说明,特别是不要这样:
// Read implements the io.Reader interface
func (r *FileReader) Read(buf []byte) (int, error)
这个注释等于说明都没说,它没有告诉您这个方法做了什么,实际上更糟的是,它让您去找别的地方的文档。在这种情况我建议将注释整个去掉。
这里有一个来自io
这个包的示例:
// LimitReader returns a Reader that reads from r
// but stops with EOF after n bytes.
// The underlying implementation is a *LimitedReader.
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }
// A LimitedReader reads from R but limits the amount of
// data returned to just N bytes. Each call to Read
// updates N to reflect the new amount remaining.
// Read returns EOF when N <= 0 or when the underlying R returns EOF.
type LimitedReader struct {
R Reader // underlying reader
N int64 // max bytes remaining
}
func (l *LimitedReader) Read(p []byte) (n int, err error) {
if l.N <= 0 {
return 0, EOF
}
if int64(len(p)) > l.N {
p = p[0:l.N]
}
n, err = l.R.Read(p)
l.N -= int64(n)
return
}
请注意,LimitedReader
的声明紧接在使用它的函数之后,并且LimitedReader.Read
又紧接着定义在LimitedReader
之后,即便LimitedReader.Read
本身没有文档注释,那和很清楚它是io.Reader
的一种实现。
小窍门:在您编写函数之前先写描述这个函数的注