刚入门 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/godocgodoc 命令有多种模式和参数,这里我们列出最常用和最简便的模式: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 格式任君选择。
参考资料万字长文解读 pkg.go.dev 的设计和实现pkg.go.dev 源码本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。本文最早发布于 云+社区,也是本人的博客。原作者: amc,欢迎转载,但请遵从上述协议注明出处。原文标题:作为 Gopher,你知道 Go 的注释即文档应该怎么写吗?发布日期:2022/03/24原文链接:https://segmentfault.com/a/1190000041604192。另:本文部分内容与笔者以前发布过的《如何写高大上的 godoc》一文类似,但当时成文与还没有 pkg.go.dev 的时代,很多内容已经落伍。因此我重新写了这篇。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。