剛入門 Go 開發時,在開源專案的主頁上我們經常可以看到這樣的一個徽章:
點選徽章,就可以開啟 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
可能可以指以下含義:
- 在 2019.11 月之前,表示
https://godoc.org
中的內容 - 現在
godoc.org
已經下線,會重定向到pkg.go.dev
,並且其功能也都重新遷移到這上面——下文以 “pkg.go.dev
” 指代這個含義 - Go 開發工具的一個命令,就叫做
godoc
——下文直接以 “godoc
” 指代這個工具 pkg.go.dev
的相關命令,被叫做pkgsite
,程式碼託管在 GitHub 上——下文以 “pkgsite
” 指代這個工具- 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 頁面了,如下圖所示:
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/
,頁面如下圖所示:
pkg.go.dev 內容
總體內容
由於筆者在 jsonvalue 中對 GoDoc 玩得比較多,因此還是以這個庫為例子。我們開啟 pkg.go.dev
中相關包的主頁,可以看到這些內容:
- 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,可能包含以下這些內容:
小節 | 說明 |
---|---|
Overview | 這是整個 package 的概覽說明,取的是 go 程式碼中的 “包註釋” 部分 |
Index | 這是整個 GoDoc 內容的總目錄,包含了所有可匯出的函式、方法、常量、變數和示例程式碼 |
Variables | 這裡列出了所有可匯出變數。實際上一個封裝得比較好的 package,這裡點進去之後應該是空的 |
Functions | 所有的可匯出函式(返回可匯出型別的函式除外) |
Types | 所有的可匯出型別及其方法,以及能夠生成對應型別的可匯出函式列表(比如各種建構函式) |
其實 Documentation 的內容,就是 GoDoc。Go 秉承 “註釋即文件” 的理念,其中 pkg.go.dev
、godoc
和 pkgsite
都使用同一套 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
頁面上的展示效果是這樣的:
但是,請讀者注意,按照 Go 官方的推薦,程式碼註釋的第一個單詞,應該是被註釋的內容本身。比如前文中,SomeTypeA
的註釋應該是 // SomeTypeA
開頭。下文開始將會統一使用這一規範。
換行(段落)
讀者可以注意到,前文中的所有有效註釋,我都換了一行;但是在 pkgsite
的頁面展示中,並沒有發生換行。
實際上,在註釋中如果只是單純的一個換行另寫註釋的話,在頁面是不會將其當作新的一段來看待的,GoDoc 的邏輯,也僅僅渲染完這一行之後,再加一個空格,然後繼續渲染下一行。
如果要在同一個註釋塊中新加一個段落,那麼我們需要插入一行空註釋,如下:
// SomeNewLine 只是用來展示如何在 GoDoc 中換行。
//
// 你看,這就是新的一行了,耶~✌️
func SomeNewLine() error {
return nil
}
內嵌程式碼
如果有需要的話,我們可以在註釋中內嵌一小段程式碼,程式碼會被獨立為一個段落,並且使用等寬字元展示。比如下面的一個例子:
// 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) {
// ......
}
總結一下:在註釋塊中,如果部分註釋行符合以下標準之一,則視為程式碼塊:
- 註釋行以製表符
\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
一方面會在目錄中標識出來:
此外,在正文中,也會刻意用灰色字型低調展示,並且隱藏註釋正文,需要點開才能顯示:
程式碼示例文件
讀者如果看我 jsonvalue 的文件,在 At()
函式下,除了上文提到的文件正文之外,還有五個程式碼示例:
那麼,文件中的程式碼示例又應該如何寫呢?
首先,我們應該新建至少一個檔案,專門用來存放示例程式碼。比如我就把示例程式碼寫在了 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
的徽章生成頁
輸入倉庫地址就可以看到相應的徽標的連結了。有 html
和 markdown
格式任君選擇。
參考資料
本文章採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。
原作者: amc,歡迎轉載,但請遵從上述協議註明出處。
原文標題:作為 Gopher,你知道 Go 的註釋即文件應該怎麼寫嗎?
釋出日期:2022/03/24
原文連結:https://segmentfault.com/a/1190000041604192。
另:本文部分內容與筆者以前釋出過的《如何寫高大上的 godoc》一文類似,但當時成文與還沒有 pkg.go.dev
的時代,很多內容已經落伍。因此我重新寫了這篇。