• 3. 注释
    • 3.1. 关于变量和常量的注释应描述其内容而非其目的
    • 3.2. 公共符号始终要注释
    • 3.2.1. 不要注释不好的代码,将它重写
    • 3.2.2. 与其注释一段代码,不如重构它

    3. 注释

    在我们继续讨论更大的项目之前,我想花几分钟时间谈论一下注释。

    Good code has lots of comments, bad code requires lots of comments. (好的代码有很多注释,坏代码需要很多注释。) — Dave Thomas and Andrew Hunt (The Pragmatic Programmer)

    注释对 Go 语言程序的可读性非常重要。 注释应该做的三件事中的一件:

    1. 注释应该解释其作用。
    2. 注释应该解释其如何做的。
    3. 注释应该解释其原因。

    第一种形式是公共符号注释的理想选择:

    1. // Open opens the named file for reading.
    2. // If successful, methods on the returned file can be used for reading.

    第二种形式非常适合在方法中注释:

    1. // queue all dependant actions
    2. var results []chan error
    3. for _, dep := range a.Deps {
    4. results = append(results, execute(seen, dep))
    5. }

    第三种形式是独一无二的,因为它不会取代前两种形式,但与此同时它并不能代替前两种形式。 此形式的注解用以解释代码的外部因素。 这些因素脱离上下文后通常很难理解,此注释的为了提供这种上下文。

    1. return &v2.Cluster_CommonLbConfig{
    2. // Disable HealthyPanicThreshold
    3. HealthyPanicThreshold: &envoy_type.Percent{
    4. Value: 0,
    5. },
    6. }

    在此示例中,无法清楚地明白 HealthyPanicThreshold 设置为零百分比的效果。 需要注释 0 值将禁用 panic 阀值。

    3.1. 关于变量和常量的注释应描述其内容而非其目的

    我之前谈过,变量或常量的名称应描述其目的。 向变量或常量添加注释时,该注释应描述变量内容,而不是变量目的。

    1. const randomNumber = 6 // determined from an unbiased die

    在此示例中,注释描述了为什么 randomNumber 被赋值为6,以及6来自哪里。 注释没有描述 randomNumber 的使用位置。 还有更多的栗子:

    1. const (
    2. StatusContinue = 100 // RFC 7231, 6.2.1
    3. StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2
    4. StatusProcessing = 102 // RFC 2518, 10.1
    5. StatusOK = 200 // RFC 7231, 6.3.1

    在HTTP的上下文中,数字 100 被称为 StatusContinue,如 RFC 7231 第 6.2.1 节中所定义。

    贴士: 对于没有初始值的变量,注释应描述谁负责初始化此变量。

    1. // sizeCalculationDisabled indicates whether it is safe
    2. // to calculate Types' widths and alignments. See dowidth.
    3. var sizeCalculationDisabled bool

    这里的注释让读者知道 dowidth 函数负责维护 sizeCalculationDisabled 的状态。

    隐藏在众目睽睽下 这个提示来自Kate Gregory[3]。有时你会发现一个更好的变量名称隐藏在注释中。

    1. // registry of SQL drivers
    2. var registry = make(map[string]*sql.Driver)

    注释是由作者添加的,因为 registry 没有充分解释其目的 - 它是一个注册表,但注册的是什么?

    通过将变量重命名为 sqlDrivers,现在可以清楚地知道此变量的目的是保存SQL驱动程序。

    1. var sqlDrivers = make(map[string]*sql.Driver)

    之前的注释就是多余的,可以删除。

    3.2. 公共符号始终要注释

    godoc 是包的文档,所以应该始终为包中声明的每个公共符号 —​ 变量、常量、函数以及方法添加注释。

    以下是 Google Style 指南中的两条规则:

    • 任何既不明显也不简短的公共功能必须予以注释。
    • 无论长度或复杂程度如何,对库中的任何函数都必须进行注释 ```golang 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)

    1. 这条规则有一个例外; 您不需要注释实现接口的方法。 具体不要像下面这样做:
    2. ```golang
    3. // Read implements the io.Reader interface
    4. func (r *FileReader) Read(buf []byte) (int, error)

    这个注释什么也没说。 它没有告诉你这个方法做了什么,更糟糕是它告诉你去看其他地方的文档。 在这种情况下,我建议完全删除该注释。

    这是 io 包中的一个例子

    1. // LimitReader returns a Reader that reads from r
    2. // but stops with EOF after n bytes.
    3. // The underlying implementation is a *LimitedReader.
    4. func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }
    5. // A LimitedReader reads from R but limits the amount of
    6. // data returned to just N bytes. Each call to Read
    7. // updates N to reflect the new amount remaining.
    8. // Read returns EOF when N <= 0 or when the underlying R returns EOF.
    9. type LimitedReader struct {
    10. R Reader // underlying reader
    11. N int64 // max bytes remaining
    12. }
    13. func (l *LimitedReader) Read(p []byte) (n int, err error) {
    14. if l.N <= 0 {
    15. return 0, EOF
    16. }
    17. if int64(len(p)) > l.N {
    18. p = p[0:l.N]
    19. }
    20. n, err = l.R.Read(p)
    21. l.N -= int64(n)
    22. return
    23. }

    请注意,LimitedReader 的声明就在使用它的函数之前,而 LimitedReader.Read 的声明遵循 LimitedReader 本身的声明。 尽管 LimitedReader.Read 本身没有文档,但它清楚地表明它是 io.Reader 的一个实现。

    贴士: 在编写函数之前,请编写描述函数的注释。 如果你发现很难写出注释,那么这就表明你将要编写的代码很难理解。

    3.2.1. 不要注释不好的代码,将它重写

    Don’t comment bad code — rewrite it — Brian Kernighan

    粗劣的代码的注释高亮显示是不够的。 如果你遇到其中一条注释,则应提出问题,以提醒您稍后重构。 只要技术债务数额已知,它是可以忍受的。

    标准库中的惯例是注意到它的人用 TODO(username) 的样式来注释。

    1. // TODO(dfc) this is O(N^2), find a faster way to do this.

    注释 username 不是该人承诺要解决该问题,但在解决问题时他们可能是最好的人选。 其他项目使用 TODO 与日期或问题编号来注释。

    3.2.2. 与其注释一段代码,不如重构它

    Good code is its own best documentation. As you’re about to add a comment, ask yourself, ‘How can I improve the code so that this comment isn’t needed?’ Improve the code and then document it to make it even clearer. 好的代码是最好的文档。 在即将添加注释时,请问下自己,“如何改进代码以便不需要此注释?’ 改进代码使其更清晰。 — Steve McConnell

    函数应该只做一件事。 如果你发现自己在注释一段与函数的其余部分无关的代码,请考虑将其提取到它自己的函数中。

    除了更容易理解之外,较小的函数更易于隔离测试,将代码隔离到函数中,其名称可能是所需的所有文档。