Go文档注释

本文翻译自《Go Doc Comments》。

“文档注释(Doc Comments)”是指直接出现在顶级(译者注:包级作用域(全局作用域),非块级作用域(非局部作用域))的package、const、func、type和var声明之前的注释,其中没有换行符。每个导出的(大写字母开头的,公有的)名称都应该有一个文档注释。

go/docgo/doc/comment包提供了从go源代码中提取文档的能力,各种工具都利用了这一功能。go doc命令查找并打印给定包或符号(symbol)的文档注释。(符号是顶级的const、func、type或var。)web服务器pkg.go.dev显示公开的go包的文档(当其许可证允许时)。为该网站提供服务的程序是golang.org/x/pkgsite/cmd/pkgsite,它也可以在本地运行以查看私有模块的文档,也可以在没有互联网连接的情况下运行。语言服务器gols能在IDE中编辑Go源文件时提供文档。

本页的其余部分介绍了如何编写Go文档注释。

包(Packages)

每个包都应该有一个介绍自己的包注释。它提供了与整个包相关的信息,并通常设定了对这个包的期望。特别是在大型软件包中,包注释可以简要概述API的最重要部分,并根据需要链接到其他文档注释。

如果包本身很简单,那么包注释可以很简短。例如:

// Package path implements utility routines for manipulating slash-separated
// paths.
//
// The path package should only be used for paths separated by forward
// slashes, such as the paths in URLs. This package does not deal with
// Windows paths with drive letters or backslashes; to manipulate
// operating system paths, use the [path/filepath] package.
package path

[path/filepath]中的方括号创建了一个文档链接

从这个例子中可以看出,Go文档注释使用了完整的句子。对于包注释,这意味着第一句话以“Package”开头。

对于多文件包,包注释应该仅放在一个源文件中。如果多个文件都具有包注释,那么会将它们连接起来,形成整个包的一个大注释。

命令(Commands)

命令的包注释类似,但它描述的是程序的行为,而不是包中的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.
    -w
        Do not print reformatted sources to standard output.
        If a file's formatting is different from gofmt's, overwrite it
        with gofmt's version. If an error occurred during overwriting,
        the original file is restored from an automatic backup.

When gofmt reads from standard input, it accepts either a full Go program
or a program fragment. A program fragment must be a syntactically
valid declaration list, statement list, or expression. When formatting
such a fragment, gofmt preserves leading indentation as well as leading
and trailing spaces, so that individual sections of a Go program can be
formatted by piping them through gofmt.
*/
package main

注释的开头一段是使用有语义的换行符(译者注:对go doc和pkgsite等从注释生成文档的工具来说,这种换行符具有一定的语义)编写的,其中每个新句子或长短语都在一个新行上,这可以使差异随着代码和注释的发展而更容易阅读。后面的段落恰好没有遵循这一惯例,不必须一个句子占一行,可以从任何地方换行,只要满足可读性。无论哪种方式,go doc和pkgsite都可以在打印输出时重写文档注释的文本。例如:

$ go doc 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.
...

带缩进的行被视为预格式化的文本:它们不会被重写,而是在HTML和Markdown演示中以代码字体打印。(详见下文的“语法(Syntax)”小节。)

类型(Types)

类型的文档注释应该解释该类型的每个实例代表或提供了什么。如果API很简单,文档注释可以很短。例如:

package zip

// A Reader serves content from a ZIP archive.
type Reader struct {
    ...
}

默认情况下,程序员应该期望一个类型是并发安全的,即一次只能由单个goroutine使用。如果一个类型提供了更强的保证,文档注释应该说明这些保证。例如:

package regexp

// Regexp is the representation of a compiled regular expression.
// A Regexp is safe for concurrent use by multiple goroutines,
// except for configuration methods, such as Longest.
type Regexp struct {
    ...
}

Go类型还应该让零值具有特定意思。如果不显而易见,那么应该在文档注释里记录零值的含义。例如:

package bytes

// A Buffer is a variable-sized buffer of bytes with Read and Write methods.
// The zero value for Buffer is an empty buffer ready to use.
type Buffer struct {
    ...
}

对于具有导出的(公有的)字段的结构体,文档注释或每个字段的注释都应解释每个公有的字段的含义。例如,以下类型的文档注释解释了它的字段的含义:

package io

// 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.
type LimitedReader struct {
    R   Reader // underlying reader
    N   int64  // max bytes remaining
}

相反,以下类型的文档注释将解释留给每个字段的注释:

package comment

// A Printer is a doc comment printer.
// The fields in the struct can be filled in before calling
// any of the printing methods
// in order to customize the details of the printing process.
type Printer struct {
    // HeadingLevel is the nesting level used for
    // HTML and Markdown headings.
    // If HeadingLevel is zero, it defaults to level 3,
    // meaning to use <h3> and ###.
    HeadingLevel int
    ...
}

与包(上节)和函数(下节)一样,类型的文档注释以类型标识符的一个完整句子开头。明确的主题通常会使措辞更清晰,也会使文本更容易搜索,无论是在网页上还是在命令行上。例如:

$ go doc -all regexp | grep pairs
pairs within the input string: result[2*n:2*n+2] identifies the indexes
    FindReaderSubmatchIndex returns a slice holding the index pairs identifying
    FindStringSubmatchIndex returns a slice holding the index pairs identifying
    FindSubmatchIndex returns a slice holding the index pairs identifying the
$

函数(Funcs)

一个函数的文档注释应该解释它返回的内容,如果被调用会产生副作用,也应该解释它的作用。参数的名称或结果的名称可以在注释中直接引用,而无需任何特殊语法(例如反引号)。(这种惯例的一个后果是,我们通常要避免函数名、参数名和结果名使用像a这样可能被误认为是普通单词的名称。)例如:

package strconv

// Quote returns a double-quoted Go string literal representing s.
// The returned string uses Go escape sequences (\t, \n, \xFF, \u0100)
// for control characters and non-printable characters as defined by IsPrint.
func Quote(s string) string {
    ...
}

package os

// Exit causes the current program to exit with the given status code.
// Conventionally, code zero indicates success, non-zero an error.
// The program terminates immediately; deferred functions are not run.
//
// For portability, the status code should be in the range [0, 125].
func Exit(code int) {
    ...
}

如果一个文档注释需要解释多个返回结果,那么给每个返回结果取一个名字可以使文档注释更容易理解,即使函数体中没有用到这些名字。例如:

package io

// Copy copies from src to dst until either EOF is reached
// on src or an error occurs. It returns the total number of bytes
// written and the first error encountered while copying, if any.
//
// A successful Copy returns err == nil, not err == EOF.
// Because Copy is defined to read from src until EOF, it does
// not treat an EOF from Read as an error to be reported.
func Copy(dst Writer, src Reader) (n int64, err error) {
    ...
}

相反,当返回结果无需在文档注释中给出一个名字进行描述时,通常也不会在代码中给出一个名字,就像上面的Quote示例一样,以避免造成混淆。

这些规则都适用于普通函数和方法。对于方法,在列出一个类型的所有方法时,使用相同的接收者名称可以避免不必要的变化:

$ go doc bytes.Buffer
package bytes // import "bytes"

type Buffer struct {
    // Has unexported fields.
}
    A Buffer is a variable-sized buffer of bytes with Read and Write methods.
    The zero value for Buffer is an empty buffer ready to use.

func NewBuffer(buf []byte) *Buffer
func NewBufferString(s string) *Buffer
func (b *Buffer) Bytes() []byte
func (b *Buffer) Cap() int
func (b *Buffer) Grow(n int)
func (b *Buffer) Len() int
func (b *Buffer) Next(n int) []byte
func (b *Buffer) Read(p []byte) (n int, err error)
func (b *Buffer) ReadByte() (byte, error)
...

这个例子还显示了返回类型T或指针*T的公有的函数,可能带有额外的error返回结果,在假设它们是T的构造函数的情况下,与类型T及其方法一起展示。

默认情况下,程序员可以认为同时从多个goroutine里调用公有的函数是安全的;这一事实无需明确说明。

另一方面,如前一节所述,以任何方式使用类型的实例,包括调用方法,通常被认为一次只能在一个goroutine里使用。如果没有在类型的文档注释中写明其方法是并发安全的,那么应该在每个方法的注释中说明。例如:

package sql

// Close returns the connection to the connection pool.
// All operations after a Close will return with ErrConnDone.
// Close is safe to call concurrently with other operations and will
// block until all other operations finish. It may be useful to first
// cancel any used context and then call Close directly after.
func (c *Conn) Close() error {
    ...
}

请注意,函数和方法的注释主要关注操作返回什么结果或执行什么内容,应该详细说明调用方需要知道的内容。特殊案例对文档可能特别重要。例如:

package math

// Sqrt returns the square root of x.
//
// Special cases are:
//
//  Sqrt(+Inf) = +Inf
//  Sqrt(±0) = ±0
//  Sqrt(x < 0) = NaN
//  Sqrt(NaN) = NaN
func Sqrt(x float64) float64 {
    ...
}

文档注释不应解释内部细节,例如当前实现中使用的算法。这些最好写在函数体内部的注释里。当细节对调用方特别重要时,那么应该给出时间或空间的边界值。例如:

package sort

// Sort sorts data in ascending order as determined by the Less method.
// It makes one call to data.Len to determine n and O(n*log(n)) calls to
// data.Less and data.Swap. The sort is not guaranteed to be stable.
func Sort(data Interface) {
    ...
}

因为这个文档注释没有提到具体使用了哪种排序算法,所以将来容易使用不同的算法来实现。

常量(Consts)

Go的语法允许对声明进行分组,在这种情况下,一个文档注释可以解释说明一组相关的常量,而单个常量只能通过单行注释进行记录。例如:

package scanner // import "text/scanner"

// The result of Scan is one of these tokens or a Unicode character.
const (
    EOF = -(iota + 1)
    Ident
    Int
    Float
    Char
    ...
)

有时一组常量根本不需要文档注释。例如:

package unicode // import "unicode"

const (
    MaxRune         = '\U0010FFFF' // maximum valid Unicode code point.
    ReplacementChar = '\uFFFD'     // represents invalid code points.
    MaxASCII        = '\u007F'     // maximum ASCII value.
    MaxLatin1       = '\u00FF'     // maximum Latin-1 value.
)

另一方面,未加入到组里的常量通常使用以一个完整的句子开头的文档注释。例如:

package unicode

// Version is the Unicode edition from which the tables are derived.
const Version = "13.0.0"

有类型的常量显示在其类型的声明的下边,因此通常会省略它们的文档注释,而使用其类型的文档注释。例如:

package syntax

// An Op is a single regular expression operator.
type Op uint8

const (
    OpNoMatch        Op = 1 + iota // matches no strings
    OpEmptyMatch                   // matches empty string
    OpLiteral                      // matches Runes sequence
    OpCharClass                    // matches Runes interpreted as range pair list
    OpAnyCharNotNL                 // matches any character except newline
    ...
)

(有关HTML演示文稿,请参阅pkg.go.dev/regexp/syntax#Op。)

变量(Vars)

对变量的约定与对常量的约定相同。例如,这里有一组变量:

package fs

// Generic file system errors.
// Errors returned by file systems can be tested against these errors
// using errors.Is.
var (
    ErrInvalid    = errInvalid()    // "invalid argument"
    ErrPermission = errPermission() // "permission denied"
    ErrExist      = errExist()      // "file already exists"
    ErrNotExist   = errNotExist()   // "file does not exist"
    ErrClosed     = errClosed()     // "file already closed"
)

单个变量:

package unicode

// Scripts is the set of Unicode script tables.
var Scripts = map[string]*RangeTable{
    "Adlam":                  Adlam,
    "Ahom":                   Ahom,
    "Anatolian_Hieroglyphs":  Anatolian_Hieroglyphs,
    "Arabic":                 Arabic,
    "Armenian":               Armenian,
    ...
}

语法(Syntax)

Go文档注释有简单的语法,支持段落、标题、链接、列表和预格式化的代码块。为了在源代码文件中保持注释的轻量和可读,不支持更改字体或原始HTML标签等复杂语法。Markdown爱好者可以将Go文档注释语法视为Markdown语法的一个简化的子集。

可以使用代码格式化的官方程序gofmt重新格式化文档注释,以便使用规范的格式。Gofmt的目标是可读性和用户对源代码中注释编写方式的控制,但会调整表现形式,使特定注释的语义更加清晰,类似于在普通源代码中将1+2 * 3重新格式化为1 + 2*3

诸如//go:generate之类的指令注释不被视为文档注释的一部分,并且在提供的文档中被省略。Gofmt将指令注释移动到文档注释的末尾,前面有一行空行。例如:

package regexp

// An Op is a single regular expression operator.
//
//go:generate stringer -type Op -trimprefix Op
type Op uint8

指令注释是与正则表达式//(line |extern |export |[a-z0-9]+:[a-z0-9])。定义自己指令的工具应该使用//toolname:directive的形式。

Gofmt会删除文档注释的前导的空行和尾随的空行。

段落(Paragraphs)

段落是一段没有缩进的非空白行。我们已经看到了许多段落的例子。

一对连续的后引号(` U+0060)将被解释为Unicode左引号( U+201C),一对连续单引号(' U+0027)被理解为Unicode右引号( U+201D)。

Gofmt会在段落文本中保留换行符。这允许使用语义换行符,如上文所述。Gofmt用一个空行替换段落之间重复的空行。Gofmt还将连续的反引号或单引号重新格式化为Unicode版本的反引号或单引号。

标题(Headings)

标题是以数字符号(U+0023)开头的一行,然后是空格和标题文本。要被识别为标题,该行不能缩进,并用空行与相邻的段落文本隔开。

例如:

// Package strconv implements conversions to and from string representations
// of basic data types.
//
// # Numeric Conversions
//
// The most common numeric conversions are [Atoi] (string to int) and [Itoa] (int to string).
...
package strconv

更多说明:

// #This is not a heading, because there is no space.
//
// # This is not a heading,
// # because it is multiple lines.
//
// # This is not a heading,
// because it is also multiple lines.
//
// The next paragraph is not a heading, because there is no additional text:
//
// #
//
// In the middle of a span of non-blank lines,
// # this is not a heading either.
//
//     # This is not a heading, because it is indented.

#语法是在Go 1.19中添加的。在Go 1.19之前,标题是由满足某些条件的单行段落隐含地标识的,最显著的条件是没有任何终止标点符号的单行。

Gofmt将早期Go文档注释里的被视为隐式标题的行重新格式化为使用#开头的标题。如果重新格式化得不合适,也就是说,如果这一行本不是标题,那么使其成为普通段落的最简单方法是使用句点或冒号等终止符,或者将其分成两行。

文档连接(Doc links)

当一行的形式为“[Text]: URL”,并且是不缩进的非空白的行时,定义了一个链接目标。在同一文档注释中的其他文本中的“[Text]”表示对连接目标进行引用,类似于HTML中的<a href=“URL”>Text</a>。例如:

// Package json implements encoding and decoding of JSON as defined in
// [RFC 7159]. The mapping between JSON and Go values is described
// in the documentation for the Marshal and Unmarshal functions.
//
// For an introduction to this package, see the article
// “[JSON and Go].”
//
// [RFC 7159]: https://tools.ietf.org/html/rfc7159
// [JSON and Go]: https://golang.org/doc/articles/json_and_go.html
package json

通过将URL的声明保留在一个单独的小节中,这种格式只会在最小程度上中断实际文本流。它类似于Markdown的引用超链接的快捷方式的格式,但没有可选的标题文本。

如果没有相应的URL声明,那么(除了文档链接,将在下一节中介绍)“[Text]”不是超链接,并且在显示时保留方括号。每个文档注释都是独立考虑的:一个注释中的链接目标的定义不会影响其他注释。

尽管声明链接的块可能与普通段落交错,gofmt会将所有链接声明块移动到文档注释的末尾,最多分为两个块:第一块包含在注释中引用的所有链接,第二块包含注释中未引用的所有连接。第二块中的链接易于注意和修复(当链接声明有拼写错误时)或删除(但连接不再需要时)。

被识别为URL的纯文本会在HTML网页中自动显示为链接。

文档连接(Doc links)

文档链接的形式为“[Name1]”或“[Name1.Name2]”,用于指代当前软件包中导出的标识符,或“[pkg]”、“[pkg.Name1]”、“[pkg.Name1.Name1]”,用于表示其他软件包中的标识符。

例如:

package bytes

// ReadFrom reads data from r until EOF and appends it to the buffer, growing
// the buffer as needed. The return value n is the number of bytes read. Any
// error except [io.EOF] encountered during the read is also returned. If the
// buffer becomes too large, ReadFrom will panic with [ErrTooLarge].
func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
    ...
}

符号链接的方括号文本可以包括一个可选的前导星号,从而可以很容易地引用指针类型,例如[*bytes.Buffer]

在引用其他软件包时,“pkg”可以是完整的导入路径,也可以是现有导入的软件包的假定名称。假定的包名要么是重命名导入中的标识符,要么是goimports假定的名称。(当这个假定不正确时,Goimports会插入重命名,所以这个规则应该适用于几乎所有的Go代码。)例如,如果当前包导入encoding/json,则可以编写“[json.Decoder]”代替“[encoding/json.Decoder]”以链接到encoding/jsonDecoder的文档。如果一个包中的不同源文件使用相同的名称导入不同的包,那么就无法使用这种简写语法。

仅当“pkg”以域名(带点的路径元素)开头或者是标准库中的包之一(“[os]”、“[encoding/json]”等)时,才假定“pkg”是完整的导入路径。例如,[os.File][example.com/sys.File]是文档链接(后者将是一个损坏的链接),但[os/sys.File]不是,因为标准库中没有os/sys包。

为了避免与映射、泛型和数组类型在语法上出现冲突,文档链接的前后必须有标点符号、空格、制表符或行的开头或结尾。例如,文本“map[ast.Expr]TypeAndValue”就不包含文档链接。

列表(Lists)

列表是一段缩进或空行,其中每一行缩进以着重符号(着重号,表示强调)列表标记或数字列表标记开头。

着重号列表标记是星形、加号、短划线或Unicode项目符号(*、+、-、•;U+002A、U+002B、U+000D、U+2022),后跟空格或制表符,然后是文本。在着重号列表中,以着重号开头的每一行都是一个新的列表项。

例如:

package url

// PublicSuffixList provides the public suffix of a domain. For example:
//   - the public suffix of "example.com" is "com",
//   - the public suffix of "foo1.foo2.foo3.co.uk" is "co.uk", and
//   - the public suffix of "bar.pvt.k12.ma.us" is "pvt.k12.ma.us".
//
// Implementations of PublicSuffixList must be safe for concurrent use by
// multiple goroutines.
//
// An implementation that always returns "" is valid and may be useful for
// testing but it is not secure: it means that the HTTP server for foo.com can
// set a cookie for bar.com.
//
// A public suffix list implementation is in the package
// golang.org/x/net/publicsuffix.
type PublicSuffixList interface {
    ...
}

数字列表标记是任意长度的十进制数字,后跟句点或右括号,然后是空格或制表符,然后是文本。在数字编号列表中,以数字开头的每一行都是一个新的列表项。数字编号不会重复。

例如:

package path

// Clean returns the shortest path name equivalent to path
// by purely lexical processing. It applies the following rules
// iteratively until no further processing can be done:
//
//  1. Replace multiple slashes with a single slash.
//  2. Eliminate each . path name element (the current directory).
//  3. Eliminate each inner .. path name element (the parent directory)
//     along with the non-.. element that precedes it.
//  4. Eliminate .. elements that begin a rooted path:
//     that is, replace "/.." by "/" at the beginning of a path.
//
// The returned path ends in a slash only if it is the root "/".
//
// If the result of this process is an empty string, Clean
// returns the string ".".
//
// See also Rob Pike, “[Lexical File Names in Plan 9].”
//
// [Lexical File Names in Plan 9]: https://9p.io/sys/doc/lexnames.html
func Clean(path string) string {
    ...
}

列表项仅包含段落,而不包含代码块或嵌套的列表。这避免了计数空白符的问题,也避免了关于一个水平制表符(Tab)占多少个空白符的问题。

Gofmt重新格式化着重号列表,以使用破折号作为列表项的标记,在破折号之前使用两个空格作为缩进,与列表项连续的下一行使用四个空格作为缩进。

Gofmt重新格式化数字编号列表,在数字前使用一个空格,在数字后使用一个句点,与列表项连续的下一行使用四个空格作为缩进。

在列表和它前面的段落之间,Gofmt会保留一个空行,但并非需要一个空行(没有空行也可以)。Gofmt会在列表和下面的段落或标题之间插入一个空行。

代码块(Code blocks)

代码块是一段缩进或空行,不以着重号列表标记或数字编号列表标记开头。它被呈现为预格式化的文本(HTML中的<pre>块)。

代码块里通常包含Go代码。例如:

package sort

// Search uses binary search...
//
// As a more whimsical example, this program guesses your number:
//
//  func GuessingGame() {
//      var s string
//      fmt.Printf("Pick an integer from 0 to 100.\n")
//      answer := sort.Search(100, func(i int) bool {
//          fmt.Printf("Is your number <= %d? ", i)
//          fmt.Scanf("%s", &s)
//          return s != "" && s[0] == 'y'
//      })
//      fmt.Printf("Your number is %d.\n", answer)
//  }
func Search(n int, f func(int) bool) int {
    ...
}

当然,除了代码之外,代码块通常还包含预格式化的文本。例如:

package path

// Match reports whether name matches the shell pattern.
// The pattern syntax is:
//
//  pattern:
//      { term }
//  term:
//      '*'         matches any sequence of non-/ characters
//      '?'         matches any single non-/ character
//      '[' [ '^' ] { character-range } ']'
//                  character class (must be non-empty)
//      c           matches character c (c != '*', '?', '\\', '[')
//      '\\' c      matches character c
//
//  character-range:
//      c           matches character c (c != '\\', '-', ']')
//      '\\' c      matches character c
//      lo '-' hi   matches character c for lo <= c <= hi
//
// Match requires pattern to match all of name, not just a substring.
// The only possible returned error is [ErrBadPattern], when pattern
// is malformed.
func Match(pattern, name string) (matched bool, err error) {
    ...
}

Gofmt对代码块中的所有行缩进一个Tab,会替换非空白行的任何其他类型的缩进。Gofmt还会在每个代码块前后插入一个空行,将代码块与周围的段落文本清楚地区分开来。

常见错误和缺陷

文档注释中任何一段缩进或空行都显示为代码块,这一规则可以追溯到Go的最早几天。不幸的是,gofmt缺乏对文档注释的支持,导致许多现有注释虽然使用缩进,但gofmt不认为这是在创建代码块。

例如,这个未经修改的列表一直被godoc解释为三行段落后面跟着一行代码块:

package http

// cancelTimerBody is an io.ReadCloser that wraps rc with two features:
// 1) On Read error or close, the stop func is called.
// 2) On Read failure, if reqDidTimeout is true, the error is wrapped and
//    marked as net.Error that hit its timeout.
type cancelTimerBody struct {
    ...
}

会被godoc输出为:

cancelTimerBody is an io.ReadCloser that wraps rc with two features:
1) On Read error or close, the stop func is called. 2) On Read failure,
if reqDidTimeout is true, the error is wrapped and

    marked as net.Error that hit its timeout.

类似地,以下注释中的命令是一行段落,后面跟着一行代码块:

package smtp

// localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls:
//
// go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \
//     --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
var localhostCert = []byte(`...`)

会被godoc输出为:

localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls:

go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \

    --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h

以下注释是一个两行的段落(第二行是“{”),后面是一个缩进六行的代码块和一行的段落:

// On the wire, the JSON will look something like this:
// {
//  "kind":"MyAPIObject",
//  "apiVersion":"v1",
//  "myPlugin": {
//      "kind":"PluginA",
//      "aOption":"foo",
//  },
// }

会被godoc输出为:

On the wire, the JSON will look something like this: {

    "kind":"MyAPIObject",
    "apiVersion":"v1",
    "myPlugin": {
        "kind":"PluginA",
        "aOption":"foo",
    },

}

另一个常见的文档注释编写错误是未缩进的Go函数定义或代码块语句,它们也用“{”和“}”括起来。

Go 1.19的gofmt中引入了对文档注释重新格式化的功能,通过在代码块周围添加空行,使此类文档注释编写错误更加显而易见。

一份2022年的分析发现,公共Go模块中只有3%的文档注释经Go 1.19的gofmt 草案重新格式化。仅限于这些文档注释,大约87%的gofmt重新格式化后的文档注释保留了人们通过阅读文档注释推断出的godoc的输出结构;大约6%的人被未缩进的列表、未缩进的多行shell命令和未缩进的大括号分隔的代码块所困扰。

基于这一分析,Go 1.19的gofmt使用了一些启发式算法,将未缩进的行合并到相邻的缩进列表或代码块中。经过这些调整,Go 1.19的gofmt将上述示例重新格式化为:

// cancelTimerBody is an io.ReadCloser that wraps rc with two features:
//  1. On Read error or close, the stop func is called.
//  2. On Read failure, if reqDidTimeout is true, the error is wrapped and
//     marked as net.Error that hit its timeout.

// localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls:
//
//  go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \
//      --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h

// On the wire, the JSON will look something like this:
//
//  {
//      "kind":"MyAPIObject",
//      "apiVersion":"v1",
//      "myPlugin": {
//          "kind":"PluginA",
//          "aOption":"foo",
//      },
//  }

这种重新格式化使含义更加清晰,并使文档注释在早期版本的Go中正确呈现。如果启发式算法做出了错误的决定,可以通过插入空行来重写它,以清楚地将段落文本与非段落文本分开。

即使有了这些启发算法,其他现有的注释也需要手动调整来纠正它们的呈现。最常见的错误是缩进了一行换行的文本。例如:

// TODO Revisit this design. It may make sense to walk those nodes
//      only once.

// According to the document:
// "The alignment factor (in bytes) that is used to align the raw data of sections in
//  the image file. The value should be a power of 2 between 512 and 64 K, inclusive."

在以上两种情况下,最后一行的缩进会使其成为一个代码块。解决办法是取消缩进。

另一个常见的错误是没有缩进列表或代码块里的换行。例如:

// Uses of this error model include:
//
//   - Partial errors. If a service needs to return partial errors to the
// client,
//     it may embed the `Status` in the normal response to indicate the
// partial
//     errors.
//
//   - Workflow errors. A typical workflow has multiple steps. Each step
// may
//     have a `Status` message for error reporting.

修复这个错误的方法就是缩进列表或代码块里的换行。

gofmt不支持嵌套列表语法,因此会把

// Here is a list:
//
//  - Item 1.
//    * Subitem 1.
//    * Subitem 2.
//  - Item 2.
//  - Item 3.

格式化为

// Here is a list:
//
//  - Item 1.
//  - Subitem 1.
//  - Subitem 2.
//  - Item 2.
//  - Item 3.

最佳解决方法是避免使用嵌套列表。另一个潜在的解决方法是混合使用两种列表标记,因为着重号标记不会在数字编号列表中引入数字列表项,反之亦然。例如:

// Here is a list:
//
//  1. Item 1.
//
//     - Subitem 1.
//
//     - Subitem 2.
//
//  2. Item 2.
//
//  3. Item 3.

Godoc:为Go代码生成文档

本文翻译自《Godoc: documenting Go code》。

Andrew Gerrand

2011/03/31

[注,20226:有关Go代码文档的更新指南,请参阅“Go文档注释”]

Go项目非常重视文档。文档是使软件可访问和可维护的重要组成部分。当然,文档必须写得又好又准确,但也必须易于书写和维护。理想情况下,它应该与代码本身耦合,这样文档就可以随着代码一起进化。程序员越容易制作出好的文档,就越能方便每个人。

为此,我们开发了godoc文档工具。本文描述了使用godoc制作文档方法,并介绍了如何按照我们的约定为自己的项目编写好的文档。

Godoc解析Go源代码(包括注释),并以HTML或纯文本形式生成文档。最终的结果是文档与它所记录的代码紧密结合。例如,通过godoc的web界面,你可以一键从函数的文档导航到其实现

Godoc在概念上与Python的Docstring和Java的Javadoc相似,但其设计得更简单。godoc读取的注释不是语言构造(不与Docstring一样),也不必须有机器可读的语法(不与Javadoc一样)。Godoc需要的只是好的注释,即使Godoc不存在,你也会想读这种注释。

约定很简单:要为一个类型、变量、常量、函数,甚至是一个包生成文档,请在其声明之前直接写一个常规注释,中间不能有空行。Godoc随后会将该注释生成文档。例如,以下是fmt包的Fprint函数的文档:

// Fprint formats using the default formats for its operands and writes to w.
// Spaces are added between operands when neither is a string.
// It returns the number of bytes written and any write error encountered.
func Fprint(w io.Writer, a ...interface{}) (n int, err error) {

请注意,这个注释是一个完整的句子,以它所描述的Go语言语法元素的名称开头。这一重要的约定使我们能够生成各种格式的文档,从纯文本到HTML再到UNIX手册页,并且当工具为了简洁而截断文档时,例如当它们提取第一行或第一个句子时,可以更好地阅读文档。

package的声明之上的注释会生成这个包的文档。这些注释可以很短,就像sort包的简短描述一样:

// Package sort provides primitives for sorting slices and user-defined
// collections.
package sort

注释也可以像gob包的概述一样详细。对于需要大量介绍性文档的包,该包使用了另一种约定:包注释放在自己的doc.go文件中,该文件只包含这些注释和一个package gob包声明语句。

在编写任何大小的包注释时,请记住它的第一句话将出现在godoc的包列表中。

与顶层声明语句不相邻的注释在godoc的输出中会被省略,除了一个明显的例外:以短语“BUG(who)”开头的顶层注释被识别为已知Bug,并在godoc的输出中包含在包文档的“Bugs”小节。“who”应该是可以提供更多信息的人的用户名。例如,这是字节包中的一个已知Bug:

// BUG(r): The rule Title uses for word boundaries does not handle Unicode punctuation properly.

有时,一个结构体的字段、一个函数、一个类型,甚至整个包都会变得多余或不再需要,但必须保留以与现有程序兼容。要发出不应使用某个标识符的信号,请在其文档注释中添加一段以“Deprecated:”开头的段落,后跟一些有关弃用的信息。

Godoc在将注释文本转换为HTML时使用了一些格式规则:

  • 随后的几行文本被视为同一段落的一部分;你必须使用一个空白行来分隔段落。
  • 预格式化的文本必须相对于周围的注释文本进行缩进(有关示例,请参阅gob的doc.go)。
  • URL将被转换为HTML链接;不需要特殊标记。

请注意,这些规则都不要求你做任何不同寻常的事情。

事实上,godoc最棒的地方在于它的易用性。因此,许多Go代码,包括所有的标准库,都已经遵循了这些约定。

你自己的代码只要有如上所述的注释就可以提供良好的文档。安装在$GOROOT/src/pkg中的任何Go软件包和任何GOPATH工作空间都可以通过godoc的命令行和HTTP接口进行访问,你可以通过-path标志或在源代码目录中运行“godoc.”来指定其他索引路径。有关更多详细信息,请参阅godoc文档