作為 Gopher,你知道 Go 的註釋即文件應該怎麼寫嗎?

amc發表於2022-03-24

剛入門 Go 開發時,在開源專案的主頁上我們經常可以看到這樣的一個徽章:

image.png

點選徽章,就可以開啟 https://pkg.go.dev/ 的網頁,網頁中給出了這個開源專案所對應的 Go 文件。在剛接觸 Go 的時候,我一度以為,pkg.go.dev 上面的文件是需要開發者上傳並稽核的——要不然那些文件咋都顯得那麼專業呢。

然而當我寫自己的輪子時,慢慢的我就發現並非如此。劃重點:在 pkg.go.dev 上的文件,都是 Go 自動從開源專案的工程程式碼中爬取、格式化後展現出來的。換句話說,每個人都可以寫自己的 GoDoc 並且展示在 pkg.go.dev 上,只需要遵從 GoDoc 的格式標準即可,也不需要任何稽核動作。

本文章的目的是通過例子,簡要說明 GoDoc 的格式,讓讀者也可以自己寫一段高大上的 godoc。以下內容以我自己的 jsonvalue 包為例子。其對應的 GoDoc 在這裡。讀者可以點開,並與程式碼中的內容做參考對比。

什麼是 GoDoc

顧名思義,GoDoc 就是 Go 語言的文件。在實際應用中,godoc 可能可以指以下含義:

  1. 在 2019.11 月之前,表示 https://godoc.org 中的內容
  2. 現在 godoc.org 已經下線,會重定向到 pkg.go.dev,並且其功能也都重新遷移到這上面——下文以 “pkg.go.dev” 指代這個含義
  3. Go 開發工具的一個命令,就叫做 godoc——下文直接以 “godoc” 指代這個工具
  4. pkg.go.dev 的相關命令,被叫做 pkgsite,程式碼託管在 GitHub 上——下文以 “pkgsite” 指代這個工具
  5. Go 工具包的文件以及生成該文件所相關的格式——下文以 “GoDoc” 指代這個含義

目前的 godoc 和 pkgsite 有兩個作用,一個是用來本地除錯自己的 GoDoc 顯示效果;另一個是在無法科學上網的時候,用來本地搭建 GoDoc 伺服器之用。

godoc 命令

我們從工具命令開始講起吧。在 2019 年之前,Go 使用的是 godoc 這個工具來格式化和展示 Go 程式碼中自帶的文件。現在這個命令已經不再包含於 Go 工具鏈中,而需要額外安裝:

go get -v golang.org/x/tools/cmd/godoc

godoc 命令有多種模式和引數,這裡我們列出最常用和最簡便的模式:

cd XXXX; godoc -http=:6060

其中 XXXX 是包含 go.mod 的一個倉庫目錄。假設 XXX 是我的 jsonvalue 庫的本地目錄,根據 go.mod,這個庫的地址是 github.com/Andrew-M-C/go.jsonvalue,那麼我就可以在瀏覽器中開啟 http://${IP}:${PORT}/pkg/github.com/Andrew-M-C/go.jsonvalue/,就可以訪問我的 jsonvalue 庫的 GoDoc 頁面了,如下圖所示:

image.png

pkgsite 命令

正如前文所說,現在 Go 官方維護和使用的是 pkg.go.dev,因此本文主要說明 pkgsite 的用法。

當前的 pkgsite 要求 Go 1.18 版,因此請把 Go 版升級到 1.18。然後我們需要安裝 pkgsite:

go install golang.org/x/pkgsite/cmd/pkgsite@latest

然後和 godoc 類似:

cd XXXX; pkgsite -http=:6060

一樣用 jsonvalue 舉例。瀏覽器的地址與 godoc 類似,但是少了 pkg/: http://${IP}:${PORT}/github.com/Andrew-M-C/go.jsonvalue/,頁面如下圖所示:

image.png

pkg.go.dev 內容

總體內容

由於筆者在 jsonvalue 中對 GoDoc 玩得比較多,因此還是以這個庫為例子。我們開啟 pkg.go.dev 中相關包的主頁,可以看到這些內容:

image.png

  • A - 當前 package 的完整路徑
  • B - 當前 package 的名稱,其中的 module 表示這是一個符合 go module 的包
  • C - 當前 package 的一些基礎資訊,包括最新版本、釋出時間、證書、依賴的包數量(包括系統包)、被引用的包數量
  • D - 如果當前 package 包含 README 檔案,則展示 README 檔案的內容
  • E - 當前 package 內的 comment as document 文件內容
  • F - 當前 package 的檔案列表,可以點選快速瀏覽
  • G - 當前 package 的子目錄列表

如果你的 README (markdown 格式) 有子標題,那麼 pkgsite 會生成 README 下的二級目錄索引。Markdown 的格式在本文就不予說明,相信碼農們都耳熟能詳了。

Documentation

讓我們點開 Documentation,一個完整的 package,可能包含以下這些內容:

image.png

小節說明
Overview這是整個 package 的概覽說明,取的是 go 程式碼中的 “包註釋” 部分
Index這是整個 GoDoc 內容的總目錄,包含了所有可匯出的函式、方法、常量、變數和示例程式碼
Variables這裡列出了所有可匯出變數。實際上一個封裝得比較好的 package,這裡點進去之後應該是空的
Functions所有的可匯出函式(返回可匯出型別的函式除外)
Types所有的可匯出型別及其方法,以及能夠生成對應型別的可匯出函式列表(比如各種建構函式)

其實 Documentation 的內容,就是 GoDoc。Go 秉承 “註釋即文件” 的理念,其中 pkg.go.devgodocpkgsite 都使用同一套 GoDoc 格式,三者都按照該格式從文件的註釋中提取,並生成文件。

下面我們具體來說明一下 GoDoc 的語法。

GoDoc 語法

在 GoDoc 中,當前 package 的所有可匯出型別,都會在 pkg.go.dev 頁面中展示出來,即便某個可匯出型別沒有任何的註釋,GoDoc 也會將這個可匯出內容的原型展示出來——當然了,我們應該時時刻刻記住:所有的可匯出內容,都應該寫好註釋。

GoDoc 支援 ///* ... */ 兩種模式的註釋符。但是筆者還是推薦使用 //,這也是目前的註釋符主流,而且大部分 IDE 也都支援一鍵將多行文字直接轉為註釋(比如 Mac 的 VsCode,使用 command + /)。雖然 /* */ 在多行註釋中非常方便,但一旦看到這個,總覺得好像是上古時代的程式碼 (狗頭)。

繫結 GoDoc 與指定型別

對於任意一個可匯出內容,緊跟著程式碼定義上方一行的註釋,都會被視為該內容的 GoDoc,從而被提取出來。比如說:

// 這一行,會被視為 SomeTypeA 的 GoDoc,
// 因為它緊挨著 SomeTypeA 的定義。
type SomeTypeA struct{}

// 這一行與 SomeTypeB 的定義之間隔了一行,
// 所以並不會認為是 SomeTypeB 的 GoDoc。

type SomeTypeB struct{}

/*
使用這種註釋符的註釋也是同理,因為整個註釋塊緊挨著 SomeTypeC 的定義,
因此會被視為 SomeTypeC 的註釋。
*/
type SomeTypeC struct{}

這三個型別在 pkgsite 頁面上的展示效果是這樣的:

image.png

但是,請讀者注意,按照 Go 官方的推薦,程式碼註釋的第一個單詞,應該是被註釋的內容本身。比如前文中,SomeTypeA 的註釋應該是 // SomeTypeA 開頭。下文開始將會統一使用這一規範。

換行(段落)

讀者可以注意到,前文中的所有有效註釋,我都換了一行;但是在 pkgsite 的頁面展示中,並沒有發生換行。

實際上,在註釋中如果只是單純的一個換行另寫註釋的話,在頁面是不會將其當作新的一段來看待的,GoDoc 的邏輯,也僅僅渲染完這一行之後,再加一個空格,然後繼續渲染下一行。

如果要在同一個註釋塊中新加一個段落,那麼我們需要插入一行空註釋,如下:

// SomeNewLine 只是用來展示如何在 GoDoc 中換行。
//
// 你看,這就是新的一行了,耶~✌️
func SomeNewLine() error {
    return nil
}

image.png

內嵌程式碼

如果有需要的話,我們可以在註釋中內嵌一小段程式碼,程式碼會被獨立為一個段落,並且使用等寬字元展示。比如下面的一個例子:

// IntsElem 用於不 panic 地從一個 int 切片中讀取元素,並且返回值和實際在切片中的位置。
//
// 不論是任何情況,如果切片長為0,則 actual Index 返回 -1.
//
// 根據引數 index 可以有幾種情況:
//
// - 零值,則直接取切片的第一個值
//
// - 正值,則從切片0位置開始,如果遇到切片結束了,那麼就迴圈從頭開始數
//
// - 負值,則表示逆序,此時則迴圈從切片的最後一個值開始數
//
// 負值的例子:
//
//    sli := []int{0, -1, -2, -3}
//    val, idx := IntsElem(sli, -2)
//
// 返回得 val = -2, idx = 2
func IntsElem(ints []int, index int) (value, actualIndex int) {
    // ......
}

image.png

總結一下:在註釋塊中,如果部分註釋行符合以下標準之一,則視為程式碼塊:

  • 註釋行以製表符 \t 開頭
  • 註釋行以以多於一個空格(包括製表符)開頭

普通註釋和程式碼塊之間可以不用專門的空註釋行,但個人建議還是加上比較好。

Overview 部分

在 Documentation 中的 Overview 部分,是整個 package 的說明,這種型別的註釋,被稱為 “包註釋”。包註釋是寫在 go 檔案最開始的 package xxx 上面。雖然 GoDoc 沒有限制、但是 Go 官方建議包註釋應當以 // Package xxx 開頭作為文字的主語。

如果在一個 package 中,有多個檔案都包含了包註釋,那麼 GoDoc 會按照檔案的字典序,依次展示這些檔案中的包註釋。但這樣可能會帶來混亂,因此一個 package 我們應當只在一個檔案中寫包註釋。

一般而言,我們可以選擇以下的檔案寫包註釋:

  • 很多 package 下面會有一個與 package 名稱同名的 xxx.go 檔案,那我們可以統一就在這個檔案裡寫包註釋,比如這樣
  • 如果 xxx.go 檔案本身承載了較多程式碼,或者是包註釋比較長,那麼我們可以專門開一個 doc.go 檔案,用來寫包註釋,比如這樣

棄用程式碼宣告

Go 所使用的版本號是 vX.Y.Z 的模式,按照官方的思想,每當 package 升級時,儘量不要升級大版本X值,這也同時代表著,本次升級是完全向前相容的。但是實際上,我們在做一些小版本或中版本升級時,有些函式/型別可能不再推薦使用。此時,GoDoc 提供了一個關鍵字 Deprecated:,作為整個註釋塊的第一個單詞,比如我們可以這麼寫:

// Deprecated: ElemAt 這個函式棄用,後續請遷移到 IntsElem 函式中.
func ElemAt(ints []int, index int) int {
    // ......
}

針對 deprecated 的內容,pkgsite 一方面會在目錄中標識出來:

image.png

此外,在正文中,也會刻意用灰色字型低調展示,並且隱藏註釋正文,需要點開才能顯示:

image.png

image.png

程式碼示例文件

讀者如果看我 jsonvalue 的文件,在 At() 函式下,除了上文提到的文件正文之外,還有五個程式碼示例:

image.png

那麼,文件中的程式碼示例又應該如何寫呢?

首先,我們應該新建至少一個檔案,專門用來存放示例程式碼。比如我就把示例程式碼寫在了 example_jsonvalue_test.go 檔案中。這個檔案的 package不得與當前包名相同,而應該命名為 包名_test 的格式。

此外,需要注意的是,示例程式碼檔案也屬於單元測試檔案的內容,當執行 go test 的時候,示例檔案也會納入測試邏輯中。

示例程式碼的宣告

如何宣告一個示例程式碼,這裡我舉兩個例子。首先是在 At() 函式下名為 “Example (1)” 的示例。在程式碼中,我把這個函式命名為:

func ExampleSet_At_1() {
    ......
}

這個函式命名有幾個部分:

函式名組成部分說明
Example這是示例程式碼的固有開頭
Set表示這是型別 Set 的示例
第一個下劃線 _分隔符,在這個分隔符後面的,是 Set 型別的成員函式名
At表示這是函式 At() 的示例,搭配前面的內容,則表示這是型別 Set 的成員函式 At() 的示例
第二個下劃線 _分隔符,在這個分隔符後面的內容,是示例程式碼的額外說明
1這是示例程式碼的額外說明,也就是前面 “Example (1)” 括號裡的部分

另外,示例程式碼中應該包含標準輸出內容,這樣便於讀者瞭解執行情況。標準輸出內容在函式內的最後,採用 // Output: 單獨起一行開頭,剩下的每一行標準輸出寫一行註釋。

相對應地,如果你想要給(不屬於任何一個型別的)函式寫示例的話,則去掉上文中關於 “型別” 的欄位;如果你不需要示例的額外說明符,則去掉 “額外說明” 欄位。比如說,我給型別 Opt 寫的示例就只有一個,在程式碼中,只有一行:

func ExampleOpt() {
    ........
}

甚至連示例說明都沒有。

如果一個元素包含多個例子,那麼 godoc 會按照字母序對示例及其相應的說明排序。這也就是為什麼我乾脆在 At() 函式中,示例標為一二三四五的原因,因為這是我希望讀者閱讀示例的順序。

在官網上釋出 GoDoc

好了,當你寫好了自己的 GoDoc 之後,總不是自己看自己自娛自樂吧,總歸是要釋出出來給大家看的。

其實發布也很簡單:當你將包含了 godox 的程式碼 push 之後(比如釋出到 github 上),就可以在瀏覽器中輸入 https://pkg.go.dev/${package路徑名}。比如 jsonvalue 的 Github 路徑(也等同於 import 路徑)為 github.com/Andrew-M-C/go.jsonvalue,因此輸入 https://pkg.go.dev/github.com/Andrew-M-C/go.jsonvalue

如果這是該頁面第一次進入,那麼 pkg.go.dev 會首先獲取、解析和更新程式碼倉庫中的文件內容,並且格式化之後展示。在 pkg.go.dev 中,如果能夠找到 package 的最新的 tag 版本,那麼會列出 tag(而不是主幹分支)上的 GoDoc。

接下來更重要的是,把這份官網 GoDoc 的連結,附到你自己的 README 中。我們可以進入 pkg.go.dev 的徽章生成頁

輸入倉庫地址就可以看到相應的徽標的連結了。有 htmlmarkdown 格式任君選擇。

image.png

參考資料


本文章採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。

本文最早釋出於 雲+社群,也是本人的部落格。

原作者: amc,歡迎轉載,但請遵從上述協議註明出處。

原文標題:作為 Gopher,你知道 Go 的註釋即文件應該怎麼寫嗎?

釋出日期:2022/03/24

原文連結:https://segmentfault.com/a/1190000041604192

另:本文部分內容與筆者以前釋出過的《如何寫高大上的 godoc》一文類似,但當時成文與還沒有 pkg.go.dev 的時代,很多內容已經落伍。因此我重新寫了這篇。

image.png

相關文章