作为 Gopher,你知道 Go 的注释即文档应该怎么写吗()

刚入门 Go 开发时,在开源项目的主页上我们经常可以看到这样的一个徽章:
作为 Gopher,你知道 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 可能可以指以下含义:

  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 页面了,如下图所示:
作为 Gopher,你知道 Go 的注释即文档应该怎么写吗()
文章图片

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/,页面如下图所示:
作为 Gopher,你知道 Go 的注释即文档应该怎么写吗()
文章图片

pkg.go.dev 内容 总体内容
由于笔者在 jsonvalue 中对 GoDoc 玩得比较多,因此还是以这个库为例子。我们打开 pkg.go.dev 中相关包的主页,可以看到这些内容:
作为 Gopher,你知道 Go 的注释即文档应该怎么写吗()
文章图片

  • 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,可能包含以下这些内容:
作为 Gopher,你知道 Go 的注释即文档应该怎么写吗()
文章图片

小节 说明
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 页面上的展示效果是这样的:
作为 Gopher,你知道 Go 的注释即文档应该怎么写吗()
文章图片

但是,请读者注意,按照 Go 官方的推荐,代码注释的第一个单词,应该是被注释的内容本身。比如前文中,SomeTypeA 的注释应该是 // SomeTypeA 开头。下文开始将会统一使用这一规范。
换行(段落)
读者可以注意到,前文中的所有有效注释,我都换了一行;但是在 pkgsite 的页面展示中,并没有发生换行。
实际上,在注释中如果只是单纯的一个换行另写注释的话,在页面是不会将其当作新的一段来看待的,GoDoc 的逻辑,也仅仅渲染完这一行之后,再加一个空格,然后继续渲染下一行。
如果要在同一个注释块中新加一个段落,那么我们需要插入一行空注释,如下:
// SomeNewLine 只是用来展示如何在 GoDoc 中换行。 // // 你看,这就是新的一行了,耶~?? func SomeNewLine() error { return nil }

作为 Gopher,你知道 Go 的注释即文档应该怎么写吗()
文章图片

内嵌代码
如果有需要的话,我们可以在注释中内嵌一小段代码,代码会被独立为一个段落,并且使用等宽字符展示。比如下面的一个例子:
// 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) { // ...... }

作为 Gopher,你知道 Go 的注释即文档应该怎么写吗()
文章图片

总结一下:在注释块中,如果部分注释行符合以下标准之一,则视为代码块:
  • 注释行以制表符 \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 一方面会在目录中标识出来:
作为 Gopher,你知道 Go 的注释即文档应该怎么写吗()
文章图片

此外,在正文中,也会刻意用灰色字体低调展示,并且隐藏注释正文,需要点开才能显示:
作为 Gopher,你知道 Go 的注释即文档应该怎么写吗()
文章图片

作为 Gopher,你知道 Go 的注释即文档应该怎么写吗()
文章图片

代码示例文档 读者如果看我 jsonvalue 的文档,在 At() 函数下,除了上文提到的文档正文之外,还有五个代码示例:
作为 Gopher,你知道 Go 的注释即文档应该怎么写吗()
文章图片

那么,文档中的代码示例又应该如何写呢?
首先,我们应该新建至少一个文件,专门用来存放示例代码。比如我就把示例代码写在了 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 格式任君选择。
作为 Gopher,你知道 Go 的注释即文档应该怎么写吗()
文章图片

参考资料
  • 万字长文解读 pkg.go.dev 的设计和实现
  • pkg.go.dev 源码
本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
本文最早发布于 云+社区,也是本人的博客。
原作者: amc,欢迎转载,但请遵从上述协议注明出处。
原文标题:作为 Gopher,你知道 Go 的注释即文档应该怎么写吗?
发布日期:2022/03/24
原文链接:https://segmentfault.com/a/1190000041604192。
另:本文部分内容与笔者以前发布过的《如何写高大上的 godoc》一文类似,但当时成文与还没有 pkg.go.dev 的时代,很多内容已经落伍。因此我重新写了这篇。
【作为 Gopher,你知道 Go 的注释即文档应该怎么写吗()】作为 Gopher,你知道 Go 的注释即文档应该怎么写吗()
文章图片

    推荐阅读