作為Gopher,你知道Go的注釋即文檔應(yīng)該怎么寫嗎?

導(dǎo)語?|?Go一直奉行“注釋即文檔”的概念,在代碼中針對(duì)各種public內(nèi)容進(jìn)行注釋之后,這些注釋也就是對(duì)應(yīng)內(nèi)容的文檔,這稱為GoDoc。那么作為gopher,你知道GoDoc應(yīng)該怎么寫嗎?
引言
剛?cè)腴TGo開發(fā)時(shí),在開源項(xiàng)目的主頁上我們經(jīng)??梢钥吹竭@樣的一個(gè)徽章:

點(diǎn)擊徽章,就可以打開https://pkg.go.dev/的網(wǎng)頁,網(wǎng)頁中給出了這個(gè)開源項(xiàng)目所對(duì)應(yīng)的Go文檔。在剛接觸Go的時(shí)候,我曾一度以為,pkg.go.dev上面的文檔是需要開發(fā)者上傳并審核的——要不然那些文檔咋都顯得那么專業(yè)呢。
然而當(dāng)我寫自己的輪子時(shí),慢慢的我就發(fā)現(xiàn)并非如此。
劃重點(diǎn):在pkg.go.dev上的文檔,都是Go自動(dòng)從開源項(xiàng)目的工程代碼中爬取、格式化后展現(xiàn)出來的。換句話說,每個(gè)人都可以寫自己的GoDoc并且展示在pkg.go.dev上,只需要遵從GoDoc的格式標(biāo)準(zhǔn)即可,也不需要任何審核動(dòng)作。
本文章的目的是通過例子,簡(jiǎn)要說明GoDoc的格式,讓讀者也可以自己寫一段高大上的godoc。以下內(nèi)容以我自己的jsonvalue(https://github.com/Andrew-M-C/go.jsonvalue)包為例子。其對(duì)應(yīng)的GoDoc在這里(https://pkg.go.dev/github.com/Andrew-M-C/go.jsonvalue)。讀者可以點(diǎn)開,并與代碼中的內(nèi)容做參考對(duì)比。
一、什么是GoDoc
顧名思義,GoDoc就是Go語言的文檔。在實(shí)際應(yīng)用中,godoc可能可以指以下含義:
在2019年11月之前,表示https://godoc.org中的內(nèi)容。
現(xiàn)在godoc.org已經(jīng)下線,會(huì)重定向到pkg.go.dev,并且其功能也都重新遷移到這上面——下文以“pkg.go.dev”指代這個(gè)含義。
Go開發(fā)工具的一個(gè)命令,就叫做godoc——下文直接以“godoc”指代這個(gè)工具。
pkg.go.dev的相關(guān)命令,被叫做pkgsite,代碼托管在GitHub上——下文以“pkgsite”指代這個(gè)工具。
Go工具包的文檔以及生成該文檔所相關(guān)的格式——下文以“GoDoc”指代這個(gè)含義。
目前的godoc和pkgsite有兩個(gè)作用,一個(gè)是用來本地調(diào)試自己的GoDoc顯示效果;另一個(gè)是在無法科學(xué)上網(wǎng)的時(shí)候,用來本地搭建GoDoc服務(wù)器之用。
二、godoc命令
我們從工具命令開始講起吧。在2019年之前,Go使用的是godoc這個(gè)工具來格式化和展示Go代碼中自帶的文檔。現(xiàn)在這個(gè)命令已經(jīng)不再包含于Go工具鏈中,而需要額外安裝:
go get -v golang.org/x/tools/cmd/godocgodoc命令有多種模式和參數(shù),這里我們列出最常用和最簡(jiǎn)便的模式:
cd XXXX; godoc -http=:6060其中XXXX是包含go.mod的一個(gè)倉庫目錄。假設(shè)XXX是我的jsonvalue(https://github.com/Andrew-M-C/go.jsonvalue)庫的本地目錄,根據(jù)go.mod,這個(gè)庫的地址是github.com/Andrew-M-C/go.jsonvalue,那么我就可以在瀏覽器中打開http://${IP}:${PORT}/pkg/github.com/Andrew-M-C/go.jsonvalue/,就可以訪問我的jsonvalue庫的GoDoc頁面了,如下圖所示:

三、pkgsite命令
正如前文所說,現(xiàn)在Go官方維護(hù)和使用的是pkg.go.dev,因此本文主要說明pkgsite的用法。
當(dāng)前的pkgsite要求Go 1.18版,因此請(qǐng)把Go版升級(jí)到1.18。然后我們需要安裝pkgsite:
go install golang.org/x/pkgsite/cmd/pkgsite@latest然后和godoc類似:
cd XXXX; pkgsite -http=:6060一樣用jsonvalue舉例。瀏覽器的地址與godoc類似,但是少了“pkg/”,頁面如下圖所示:

四、pkg.go.dev內(nèi)容
(一)總體內(nèi)容
由于筆者在jsonvalue中對(duì)GoDoc玩得比較多,因此還是以這個(gè)庫為例子。我們打開pkg.go.dev中相關(guān)包的主頁,可以看到這些內(nèi)容:

A-當(dāng)前package的完整路徑。
B-當(dāng)前package的名稱,其中的module表示這是一個(gè)符合go module的包。
C-當(dāng)前package的一些基礎(chǔ)信息,包括最新版本、發(fā)布時(shí)間、證書、依賴的包數(shù)量(包括系統(tǒng)包)、被引用的包數(shù)量。
D-如果當(dāng)前package包含README文件,則展示README文件的內(nèi)容。
E-當(dāng)前package內(nèi)的comment as document文檔內(nèi)容。
F-當(dāng)前package的文件列表,可以點(diǎn)擊快速瀏覽。
G-當(dāng)前package的子目錄列表。
如果你的README (markdown格式) 有子標(biāo)題,那么pkgsite會(huì)生成 README 下的二級(jí)目錄索引。Markdown的格式在本文就不予說明,相信碼農(nóng)們都耳熟能詳了。
(二)Documentation
讓我們點(diǎn)開Documentation,一個(gè)完整的package,可能包含以下這些內(nèi)容:


其實(shí)Documentation的內(nèi)容,就是GoDoc。Go秉承“注釋即文檔”的理念,其中pkg.go.dev、godoc和pkgsite都使用同一套GoDoc格式,三者都按照該格式從文檔的注釋中提取,并生成文檔。
下面我們具體來說明一下GoDoc的語法。
五、GoDoc語法
在GoDoc中,當(dāng)前package的所有可導(dǎo)出類型,都會(huì)在pkg.go.dev頁面中展示出來,即便某個(gè)可導(dǎo)出類型沒有任何的注釋,GoDoc也會(huì)將這個(gè)可導(dǎo)出內(nèi)容的原型展示出來——當(dāng)然了,我們應(yīng)該時(shí)時(shí)刻刻記住:所有的可導(dǎo)出內(nèi)容,都應(yīng)該寫好注釋。
GoDoc支持//和/* ... */兩種模式的注釋符。但是筆者還是推薦使用//,這也是目前的注釋符主流,而且大部分IDE也都支持一鍵將多行文本直接轉(zhuǎn)為注釋(比如Mac的VsCode,使用command+/)。雖然/* */在多行注釋中非常方便,但一旦看到這個(gè),總覺得好像是上古時(shí)代的代碼 (狗頭)。
(一)綁定GoDoc與指定類型
對(duì)于任意一個(gè)可導(dǎo)出內(nèi)容,緊跟著代碼定義上方一行的注釋,都會(huì)被視為該內(nèi)容的GoDoc,從而被提取出來。比如說:
// 這一行,會(huì)被視為 SomeTypeA 的 GoDoc,// 因?yàn)樗o挨著 SomeTypeA 的定義。type SomeTypeA struct{}// 這一行與 SomeTypeB 的定義之間隔了一行,// 所以并不會(huì)認(rèn)為是 SomeTypeB 的 GoDoc。type SomeTypeB struct{}/*使用這種注釋符的注釋也是同理,因?yàn)檎麄€(gè)注釋塊緊挨著 SomeTypeC 的定義,因此會(huì)被視為 SomeTypeC 的注釋。*/type SomeTypeC struct{}
這三個(gè)類型在pkgsite頁面上的展示效果是這樣的:

但是,請(qǐng)讀者注意,按照Go官方的推薦,代碼注釋的第一個(gè)單詞,應(yīng)該是被注釋的內(nèi)容本身。比如前文中,SomeTypeA的注釋應(yīng)該是// SomeTypeA開頭。下文開始將會(huì)統(tǒng)一使用這一規(guī)范。
(二)換行(段落)
讀者可以注意到,前文中的所有有效注釋,我都換了一行;但是在pkgsite的頁面展示中,并沒有發(fā)生換行。
實(shí)際上,在注釋中如果只是單純的一個(gè)換行另寫注釋的話,在頁面是不會(huì)將其當(dāng)作新的一段來看待的,GoDoc的邏輯,也僅僅渲染完這一行之后,再加一個(gè)空格,然后繼續(xù)渲染下一行。
如果要在同一個(gè)注釋塊中新加一個(gè)段落,那么我們需要插入一行空注釋,如下:
// SomeNewLine 只是用來展示如何在 GoDoc 中換行。//// 你看,這就是新的一行了,耶~??func SomeNewLine() error {return nil}

(三)內(nèi)嵌代碼
如果有需要的話,我們可以在注釋中內(nèi)嵌一小段代碼,代碼會(huì)被獨(dú)立為一個(gè)段落,并且使用等寬字符展示。比如下面的一個(gè)例子:
// IntsElem 用于不 panic 地從一個(gè) int 切片中讀取元素,并且返回值和實(shí)際在切片中的位置。//// 不論是任何情況,如果切片長(zhǎng)為0,則 actual Index 返回 -1.//// 根據(jù)參數(shù) index 可以有幾種情況://// - 零值,則直接取切片的第一個(gè)值//// - 正值,則從切片0位置開始,如果遇到切片結(jié)束了,那么就循環(huán)從頭開始數(shù)//// - 負(fù)值,則表示逆序,此時(shí)則循環(huán)從切片的最后一個(gè)值開始數(shù)//// 負(fù)值的例子://// sli := []int{0, -1, -2, -3}// val, idx := IntsElem(sli, -2)//// 返回得 val = -2, idx = 2func IntsElem(ints []int, index int) (value, actualIndex int) {// ......}

總結(jié):在注釋塊中,如果部分注釋行符合以下標(biāo)準(zhǔn)之一,則視為代碼塊:
注釋行以制表符\t開頭。
注釋行以以多于一個(gè)空格(包括制表符)開頭。
普通注釋和代碼塊之間可以不用專門的空注釋行,但個(gè)人建議還是加上比較好。
六、Overview部分
在Documentation中的Overview部分,是整個(gè)package的說明,這種類型的注釋,被稱為“包注釋”。包注釋是寫在go文件最開始的package xxx上面。雖然GoDoc沒有限制、但是Go官方建議包注釋應(yīng)當(dāng)以// Package xxx開頭作為文本的主語。
如果在一個(gè)package中,有多個(gè)文件都包含了包注釋,那么GoDoc會(huì)按照文件的字典序,依次展示這些文件中的包注釋。但這樣可能會(huì)帶來混亂,因此一個(gè)package我們應(yīng)當(dāng)只在一個(gè)文件中寫包注釋。
一般而言,我們可以選擇以下的文件寫包注釋:
很多package下面會(huì)有一個(gè)與package名稱同名的xxx.go文件,那我們可以統(tǒng)一就在這個(gè)文件里寫包注釋,比如這樣:(https://github.com/Andrew-M-C/go.jsonvalue/blob/v1.2.0/jsonvalue.go#L1)
如果xxx.go文件本身承載了較多代碼,或者是包注釋比較長(zhǎng),那么我們可以專門開一個(gè)doc.go文件,用來寫包注釋,比如這樣:(https://github.com/Andrew-M-C/go.jsonvalue/blob/v1.0.0/doc.go#L1)
七、棄用代碼聲明
Go所使用的版本號(hào)是vX.Y.Z的模式,按照官方的思想,每當(dāng)package升級(jí)時(shí),盡量不要升級(jí)大版本X值,這也同時(shí)代表著,本次升級(jí)是完全向前兼容的。但是實(shí)際上,我們?cè)谧鲆恍┬“姹净蛑邪姹旧?jí)時(shí),有些函數(shù)/類型可能不再推薦使用。此時(shí),GoDoc提供了一個(gè)關(guān)鍵字Deprecated:,作為整個(gè)注釋塊的第一個(gè)單詞,比如我們可以這么寫:
// Deprecated: ElemAt 這個(gè)函數(shù)棄用,后續(xù)請(qǐng)遷移到 IntsElem 函數(shù)中.func ElemAt(ints []int, index int) int {// ......}
針對(duì)deprecated的內(nèi)容,pkgsite一方面會(huì)在目錄中標(biāo)識(shí)出來:

此外,在正文中,也會(huì)刻意用灰色字體低調(diào)展示,并且隱藏注釋正文,需要點(diǎn)開才能顯示:


八、代碼示例文檔
讀者如果看我jsonvalue的文檔(https://pkg.go.dev/github.com/Andrew-M-C/go.jsonvalue#Set.At),在At()函數(shù)下,除了上文提到的文檔正文之外,還有五個(gè)代碼示例:

那么,文檔中的代碼示例又應(yīng)該如何寫呢?
首先,我們應(yīng)該新建至少一個(gè)文件,專門用來存放示例代碼。比如我就把示例代碼寫在了example_jsonvalue_test.go(https://github.com/Andrew-M-C/go.jsonvalue/blob/master/example_jsonvalue_test.go)文件中。這個(gè)文件的package名不得與當(dāng)前包名相同,而應(yīng)該命名為包名_test的格式。
此外,需要注意的是,示例代碼文件也屬于單元測(cè)試文件的內(nèi)容,當(dāng)執(zhí)行g(shù)o test的時(shí)候,示例文件也會(huì)納入測(cè)試邏輯中。
(一)示例代碼的聲明
如何聲明一個(gè)示例代碼,這里我舉兩個(gè)例子。首先是在At()函數(shù)下名為“Example (1)”的示例。在代碼(https://github.com/Andrew-M-C/go.jsonvalue/blob/master/example_jsonvalue_test.go#L112)中,我把這個(gè)函數(shù)命名為:
func ExampleSet_At_1() {......}
這個(gè)函數(shù)命名有幾個(gè)部分:

另外,示例代碼中應(yīng)該包含標(biāo)準(zhǔn)輸出內(nèi)容,這樣便于讀者了解執(zhí)行情況。標(biāo)準(zhǔn)輸出內(nèi)容在函數(shù)內(nèi)的最后,采用//Output:?單獨(dú)起一行開頭,剩下的每一行標(biāo)準(zhǔn)輸出寫一行注釋。
相對(duì)應(yīng)地,如果你想要給(不屬于任何一個(gè)類型的)函數(shù)寫示例的話,則去掉上文中關(guān)于“類型”的字段;如果你不需要示例的額外說明符,則去掉“額外說明”字段。比如說,我給類型Opt寫的示例(https://pkg.go.dev/github.com/Andrew-M-C/go.jsonvalue#example-Opt)就只有一個(gè),在代碼(https://github.com/Andrew-M-C/go.jsonvalue/blob/master/example_jsonvalue_test.go#L43)中,只有一行:
func ExampleOpt() {........}
甚至連示例說明都沒有。
如果一個(gè)元素包含多個(gè)例子,那么godoc會(huì)按照字母序?qū)κ纠捌湎鄳?yīng)的說明排序。這也就是為什么我干脆在At()函數(shù)中,示例標(biāo)為一二三四五的原因,因?yàn)檫@是我希望讀者閱讀示例的順序。
(二)在官網(wǎng)上發(fā)布GoDoc
好了,當(dāng)你寫好了自己的GoDoc之后,總不是自己看自己自娛自樂吧,總歸是要發(fā)布出來給大家看的。
其實(shí)發(fā)布也很簡(jiǎn)單:當(dāng)你將包含了godoc的代碼push之后(比如發(fā)布到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)。
如果這是該頁面第一次進(jìn)入,那么pkg.go.dev會(huì)首先獲取、解析和更新代碼倉庫中的文檔內(nèi)容,并且格式化之后展示。在pkg.go.dev中,如果能夠找到package的最新的tag版本,那么會(huì)列出tag(而不是主干分支)上的GoDoc。
接下來更重要的是,把這份官網(wǎng)GoDoc的鏈接,附到你自己的README中。我們可以進(jìn)入pkg.go.dev的徽章生成頁(????????https://pkg.go.dev/badge/?)
輸入倉庫地址就可以看到相應(yīng)的徽標(biāo)的鏈接了。有html和markdown格式任君選擇。

參考資料:
1.萬字長(zhǎng)文解讀pkg.go.dev的設(shè)計(jì)和實(shí)現(xiàn)
2.pkg.go.dev源碼
(?轉(zhuǎn)載須取得作者同意,未經(jīng)許可,禁止二次轉(zhuǎn)載?)
?作者簡(jiǎn)介
張敏
騰訊高級(jí)后臺(tái)工程師
騰訊高級(jí)后臺(tái)工程師,在電子和互聯(lián)網(wǎng)行業(yè)深耕多年,擁有豐富的嵌入式和云服務(wù)后臺(tái)開發(fā)經(jīng)驗(yàn),個(gè)人博客共有過百篇文章,云+社區(qū)Top50原創(chuàng)作者,技術(shù)創(chuàng)作101第二季講師,現(xiàn)負(fù)責(zé)騰訊產(chǎn)品后臺(tái)開發(fā)。
?推薦閱讀
技術(shù)的發(fā)展,如何高效助力行業(yè)數(shù)字化?
??戳閱讀原文前往「騰訊云+社區(qū)」作者個(gè)人主頁參與交流哦~
