导语 | Go一直奉行“注释即文档”的概念,在代码中针对各种public内容进行注释之后,这些注释也就是对应内容的文档,这称为GoDoc。那么作为gopher,你知道GoDoc应该怎么写吗?
引言
刚入门Go开发时,在开源项目的主页上我们经常可以看到这样的一个徽章:
点击徽章,就可以打开https://pkg.go.dev/的网页,网页中给出了这个开源项目所对应的Go文档。在刚接触Go的时候,我曾一度以为,pkg.go.dev上面的文档是需要开发者上传并审核的——要不然那些文档咋都显得那么专业呢。
然而当我写自己的轮子时,慢慢的我就发现并非如此。
划重点:在pkg.go.dev上的文档,都是Go自动从开源项目的工程代码中爬取、格式化后展现出来的。换句话说,每个人都可以写自己的GoDoc并且展示在pkg.go.dev上,只需要遵从GoDoc的格式标准即可,也不需要任何审核动作。
本文章的目的是通过例子,简要说明GoDoc的格式,让读者也可以自己写一段高大上的godoc。以下内容以我自己的jsonvalue(https://github.com/Andrew-M-C/go.jsonvalue)包为例子。其对应的GoDoc在这里(https://pkg.go.dev/github.com/Andrew-M-C/go.jsonvalue)。读者可以点开,并与代码中的内容做参考对比。
一、什么是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(https://github.com/Andrew-M-C/go.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/”,页面如下图所示:
四、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,可能包含以下这些内容:
其实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文件,那我们可以统一就在这个文件里写包注释,比如这样:(https://github.com/Andrew-M-C/go.jsonvalue/blob/v1.2.0/jsonvalue.go#L1)
如果xxx.go文件本身承载了较多代码,或者是包注释比较长,那么我们可以专门开一个doc.go文件,用来写包注释,比如这样:(https://github.com/Andrew-M-C/go.jsonvalue/blob/v1.0.0/doc.go#L1)
七、弃用代码声明
Go所使用的版本号是vX.Y.Z的模式,按照官方的思想,每当package升级时,尽量不要升级大版本X值,这也同时代表着,本次升级是完全向前兼容的。但是实际上,我们在做一些小版本或中版本升级时,有些函数/类型可能不再推荐使用。此时,GoDoc提供了一个关键字Deprecated:,作为整个注释块的第一个单词,比如我们可以这么写:
// Deprecated: ElemAt 这个函数弃用,后续请迁移到 IntsElem 函数中.
func ElemAt(ints []int, index int) int {
// ......
}
针对deprecated的内容,pkgsite一方面会在目录中标识出来:
此外,在正文中,也会刻意用灰色字体低调展示,并且隐藏注释正文,需要点开才能显示:
八、代码示例文档
读者如果看我jsonvalue的文档(https://pkg.go.dev/github.com/Andrew-M-C/go.jsonvalue#Set.At),在At()函数下,除了上文提到的文档正文之外,还有五个代码示例:
那么,文档中的代码示例又应该如何写呢?
首先,我们应该新建至少一个文件,专门用来存放示例代码。比如我就把示例代码写在了example_jsonvalue_test.go(https://github.com/Andrew-M-C/go.jsonvalue/blob/master/example_jsonvalue_test.go)文件中。这个文件的package名不得与当前包名相同,而应该命名为包名_test的格式。
此外,需要注意的是,示例代码文件也属于单元测试文件的内容,当执行go test的时候,示例文件也会纳入测试逻辑中。
如何声明一个示例代码,这里我举两个例子。首先是在At()函数下名为“Example (1)”的示例。在代码(https://github.com/Andrew-M-C/go.jsonvalue/blob/master/example_jsonvalue_test.go#L112)中,我把这个函数命名为:
func ExampleSet_At_1() {
......
}
这个函数命名有几个部分:
另外,示例代码中应该包含标准输出内容,这样便于读者了解执行情况。标准输出内容在函数内的最后,采用//Output: 单独起一行开头,剩下的每一行标准输出写一行注释。
相对应地,如果你想要给(不属于任何一个类型的)函数写示例的话,则去掉上文中关于“类型”的字段;如果你不需要示例的额外说明符,则去掉“额外说明”字段。比如说,我给类型Opt写的示例(https://pkg.go.dev/github.com/Andrew-M-C/go.jsonvalue#example-Opt)就只有一个,在代码(https://github.com/Andrew-M-C/go.jsonvalue/blob/master/example_jsonvalue_test.go#L43)中,只有一行:
func ExampleOpt() {
........
}
甚至连示例说明都没有。
如果一个元素包含多个例子,那么godoc会按照字母序对示例及其相应的说明排序。这也就是为什么我干脆在At()函数中,示例标为一二三四五的原因,因为这是我希望读者阅读示例的顺序。
好了,当你写好了自己的GoDoc之后,总不是自己看自己自娱自乐吧,总归是要发布出来给大家看的。
其实发布也很简单:当你将包含了godoc的代码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的徽章生成页(https://pkg.go.dev/badge/)
输入仓库地址就可以看到相应的徽标的链接了。有html和markdown格式任君选择。
1.万字长文解读pkg.go.dev的设计和实现
2.pkg.go.dev源码
( 转载须取得作者同意,未经许可,禁止二次转载 )
作者简介
张敏
腾讯高级后台工程师
腾讯高级后台工程师,在电子和互联网行业深耕多年,拥有丰富的嵌入式和云服务后台开发经验,个人博客共有过百篇文章,云+社区Top50原创作者,技术创作101第二季讲师,现负责腾讯产品后台开发。
推荐阅读