Go 文件註解

「文件註解」是出現在頂層套件、常數、函式、類型和變數宣告之前,且沒有換行的註解。每個匯出的(大寫)名稱都應該有文件註解。

go/docgo/doc/comment 套件提供從 Go 原始程式碼中萃取文件的功能,且多種工具會使用此功能。 go doc 指令會查詢並列印給定套件或符號的文件註解。(符號是頂層 const、func、type 或 var。)網頁伺服器 pkg.go.dev 會顯示公開 Go 套件的文件(如果其授權條款允許使用)。提供該網站服務的程式是 golang.org/x/pkgsite/cmd/pkgsite,也可以在本地執行,以查看私人模組的文件或在沒有網路連線的情況下查看。語言伺服器 gopls 會在 IDE 中編輯 Go 原始程式碼檔案時提供文件。

本頁面的其餘部分說明如何撰寫 Go 文件註解。

套件

每個套件都應該有一個套件註解來介紹套件。它提供與整個套件相關的資訊,並通常設定對套件的預期。特別是在大型套件中,套件註解可以簡要概述 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 文件註解使用完整的句子。對於套件註解,表示 第一個句子 以「套件 」開頭。

對於多檔案套件,套件註解應該只在一個原始檔中。如果多個檔案有套件註解,它們會串接起來形成整個套件的一個大型註解。

指令

指令的套件註解類似,但它描述的是程式行為,而不是套件中的 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

註解的開頭使用 語義換行 編寫,其中每個新句子或長短語都在一行上,這可以讓 diff 在程式碼和註解演進時更容易閱讀。後面的段落沒有遵循這個慣例,而是手動換行。無論哪種方式最適合你的程式碼庫都可以。無論如何,go docpkgsite 在列印文件註解文字時都會重新換行。例如

$ 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 簡報中的程式碼字型列印。(以下的 語法 部分提供了詳細資訊。)

類型

類型的文件註解應該說明該類型的每個實例代表或提供什麼。如果 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
$

函數

函數的文件註解應說明函數回傳什麼,或者對於因副作用而呼叫的函數,說明函數執行了什麼動作。命名參數和結果可以在註解中直接參照,而無需任何特殊語法,例如反引號。(此慣例的後果是通常會避免使用可能被誤認為一般字詞的名稱,例如 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 strings

// HasPrefix reports whether the string s begins with prefix.
func HasPrefix(s, prefix string) bool

如果文件註解需要說明多個結果,命名結果可以讓文件註解更易於理解,即使這些名稱未用於函數主體中。例如

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 的頂層函式,可能加上額外的錯誤結果,會與型別 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) {
    ...
}

由於此說明文件註解未提及使用哪種排序演算法,因此未來變更實作以使用不同演算法會比較容易。

常數

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"

已鍵入的常數顯示在其類型聲明旁邊,因此通常省略 const 群組文件註解,而採用該類型的文件註解。例如

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
    ...
)

(請參閱 pkg.go.dev/regexp/syntax#Op 以取得 HTML 簡報。)

變數

變數的慣例與常數相同。例如,以下是一組已分組的變數

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,
    ...
}

語法

Go 文件註解以一種簡單的語法撰寫,支援段落、標題、連結、清單和預先格式化的程式碼區塊。為了讓註解在原始檔案中保持輕量且易於閱讀,不支援字型變更或原始 HTML 等複雜功能。Markdown 愛好者可以將語法視為 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 會移除文件註解中的前導和尾隨空白行。如果文件註解中的所有行都以相同的空格和標籤順序開始,gofmt 會移除該前置詞。

段落

段落為範圍未縮排的非空白行。我們已經看過許多段落的範例。

一對連續的反引號 (` U+0060) 會被解譯為 Unicode 左引號(“ U+201C),而一對連續的單引號 (' U+0027) 會被解譯為 Unicode 右引號(” U+201D)。

Gofmt 保留段落文字中的換行符號:它不會重新換行文字。這允許使用 語意換行,如前所見。Gofmt 會將段落之間重複的空白行替換為單一空白行。Gofmt 也會將連續的反引號或單引號重新格式化為其 Unicode 解譯。

標題

標題是一行,以數字符號 (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 視為隱含標題的行,以改用 # 標題。如果重新格式化不適當,也就是說,如果該行不打算作為標題,使其成為段落最簡單的方法是加入終止標點符號,例如句點或冒號,或將其分成兩行。

範圍未縮排的非空白行定義連結目標,當每一行都是 “[文字]: URL” 形式時。在同一文件註解中的其他文字中,“[文字]” 代表使用給定文字連結到 URL,在 HTML 中為 <a href=“URL”>文字</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://go.dev.org.tw/doc/articles/json_and_go.html
package json

透過將 URL 保存在獨立的區段中,此格式僅會對實際文字的流程造成最小的中斷。它也大致符合 Markdown 捷徑參考連結格式,但沒有選用的標題文字。

如果沒有對應的 URL 宣告,則 (除了下一個區段中所述的文件連結之外)「[文字]」並非超連結,且顯示時會保留方括號。每個文件註解都會被獨立考量:一個註解中的連結目標定義不會影響其他註解。

儘管連結目標定義區塊可以與一般段落交錯,但 gofmt 會將所有連結目標定義移至文件註解的結尾,最多分成兩個區塊:首先是包含註解中所有有引用的連結目標的區塊,然後是包含註解中沒有引用的所有目標的區塊。獨立的區塊讓未使用的目標容易被注意到並修正 (如果連結或定義有錯字) 或刪除 (如果不再需要定義)。

被辨識為 URL 的純文字會在 HTML 呈現中自動連結。

文件連結是「[名稱 1]」或「[名稱 1.名稱 2]」形式的連結,用於參考目前套件中的已匯出識別碼,或「[套件]」、「[套件.名稱 1]」或「[套件.名稱 1.名稱 2]」用於參考其他套件中的識別碼。

例如

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/json 的 Decoder 文件。如果套件中的不同原始檔使用相同名稱匯入不同套件,則簡寫會產生歧義,且無法使用。

「pkg」僅在以網域名稱 (帶有句點的路徑元素) 開頭,或為標準函式庫中的套件之一(「[os]」、「[encoding/json]」等)時,才會被假設為完整匯入路徑。例如,[os.File][example.com/sys.File] 是文件連結(後者會是失效連結),但 [os/sys.File] 不是,因為標準函式庫中沒有 os/sys 套件。

為避免地圖、泛型和陣列類型的問題,文件連結必須同時在前後加上標點符號、空格、標籤,或行首或行尾。例如,文字「map[ast.Expr]TypeAndValue」不包含文件連結。

清單

清單是由縮排或空白行組成的範圍(否則會是程式碼區塊,如下一節所述),其中第一個縮排行以項目符號清單標記或編號清單標記開頭。

項目符號清單標記為星號、加號、破折號或 Unicode 項目符號 (*, +, -, •; U+002A, U+002B, U+002D, 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 {
    ...
}

清單項目只包含段落,不包含程式區塊或巢狀清單。這可避免任何間距計算的細微差別,以及關於跳格在不一致縮排中計算為幾個空白的問題。

Gofmt 重新格式化項目符號清單,使用破折號作為項目符號標記,破折號前有兩個空白的縮排,續行則有四個空白的縮排。

Gofmt 重新格式化編號清單,在數字前使用一個空白,數字後使用句點,續行則再次使用四個空白的縮排。

Gofmt 保留清單和前段落之間的空白行,但不要求。它會在清單和後段落或標題之間插入空白行。

程式區塊

程式區塊是由縮排或空白行組成的範圍,不以項目符號標記或編號清單標記開頭。它會呈現為預格式化文字(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 缺乏對文件註解的支持,導致許多現有的註解使用縮排,卻無意建立程式碼區塊。

例如,這個未縮排的清單一直被 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 {
    ...
}

這在 go doc 中總是呈現為

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(`...`)

這在 go doc 中呈現為

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 doc 中呈現為

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 重新格式化保留了人們從閱讀註解中推斷出的結構;約有 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.

解決方法是縮排換行的行。

Go 文件註解不支援巢狀清單,因此 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.