刚入门 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
的时代,很多内容已经落伍。因此我重新写了这篇。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。