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

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

1.1. 导语

Go 一直奉行 “注释即文档” 的概念, 在代码中针对各种 public 内容进行注释之后, 这些注释也就是对应内容的文档, 这称为 GoDoc。那么作为 gopher, 你知道 GoDoc 应该怎么写吗?

1.2. 引言

刚入门 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)。读者可以点开, 并与代码中的内容做参考对比。

1.3. 什么是 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 服务器之用。

1.4. 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:// I P : {IP}: IP:{PORT}/pkg/github.com/Andrew-M-C/go.jsonvalue/, 就可以访问我的 jsonvalue 库的 GoDoc 页面了, 如下图所示:

在这里插入图片描述

1.5. 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/", 页面如下图所示:

在这里插入图片描述

1.6. pkg.go.dev 内容

1.6.1. 总体内容

由于笔者在 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 的格式在本文就不予说明, 相信码农们都耳熟能详了。

1.6.2. Documentation

让我们点开 Documentation, 一个完整的 package, 可能包含以下这些内容:

在这里插入图片描述

在这里插入图片描述

其实 Documentation 的内容, 就是 GoDoc。Go 秉承"注释即文档"的理念, 其中 pkg.go.dev、godoc 和 pkgsite 都使用同一套 GoDoc 格式, 三者都按照该格式从文档的注释中提取, 并生成文档。

下面我们具体来说明一下 GoDoc 的语法。

1.7. GoDoc 语法

在 GoDoc 中, 当前 package 的所有可导出类型, 都会在 pkg.go.dev 页面中展示出来, 即便某个可导出类型没有任何的注释, GoDoc 也会将这个可导出内容的原型展示出来——当然了, 我们应该时时刻刻记住: 所有的可导出内容, 都应该写好注释。

GoDoc 支持//和/* … /两种模式的注释符。但是笔者还是推荐使用//, 这也是目前的注释符主流, 而且大部分 IDE 也都支持一键将多行文本直接转为注释(比如 Mac 的 VsCode, 使用 command+/)。虽然/ */在多行注释中非常方便, 但一旦看到这个, 总觉得好像是上古时代的代码 (狗头)。

1.7.1. 绑定 GoDoc 与指定类型

对于任意一个可导出内容, 紧跟着代码定义上方一行的注释, 都会被视为该内容的 GoDoc, 从而被提取出来。比如说:

// 这一行, 会被视为 SomeTypeA 的 GoDoc, // 因为它紧挨着 SomeTypeA 的定义。type SomeTypeA struct{}
// 这一行与 SomeTypeB 的定义之间隔了一行, // 所以并不会认为是 SomeTypeB 的 GoDoc。
type SomeTypeB struct{}
/*使用这种注释符的注释也是同理, 因为整个注释块紧挨着 SomeTypeC 的定义, 因此会被视为 SomeTypeC 的注释。*/
type SomeTypeC struct{}

这三个类型在 pkgsite 页面上的展示效果是这样的:

在这里插入图片描述

但是, 请读者注意, 按照 Go 官方的推荐, 代码注释的第一个单词, 应该是被注释的内容本身。比如前文中, SomeTypeA 的注释应该是// SomeTypeA 开头。下文开始将会统一使用这一规范。

1.7.2. 换行(段落)

读者可以注意到, 前文中的所有有效注释, 我都换了一行; 但是在 pkgsite 的页面展示中, 并没有发生换行。

实际上, 在注释中如果只是单纯的一个换行另写注释的话, 在页面是不会将其当作新的一段来看待的, GoDoc 的逻辑, 也仅仅渲染完这一行之后, 再加一个空格, 然后继续渲染下一行。

如果要在同一个注释块中新加一个段落, 那么我们需要插入一行空注释, 如下:

// SomeNewLine 只是用来展示如何在 GoDoc 中换行。 你看, 这就是新的一行了, 耶~✌️
func SomeNewLine() error {return nil}

在这里插入图片描述

1.7.3. 内嵌代码

如果有需要的话, 我们可以在注释中内嵌一小段代码, 代码会被独立为一个段落, 并且使用等宽字符展示。比如下面的一个例子:

// IntsElem 用于不 panic 地从一个 int 切片中读取元素, 并且返回值和实际在切片中的位置。
 不论是任何情况, 如果切片长为 0, 则 actual Index 返回 -1.
 根据参数 index 可以有几种情况: 
 - 零值, 则直接取切片的第一个值
 - 正值, 则从切片 0 位置开始, 如果遇到切片结束了, 那么就循环从头开始数
 - 负值, 则表示逆序, 此时则循环从切片的最后一个值开始数
 负值的例子: 
    sli := []int{0, -1, -2, -3}
//    val, idx := IntsElem(sli, -2)
 返回得 val = -2, idx = 2func IntsElem(ints []int, index int) (value, actualIndex int) {    // ......}

在这里插入图片描述

总结: 在注释块中, 如果部分注释行符合以下标准之一, 则视为代码块:

  • 注释行以制表符 \t 开头。
  • 注释行以以多于一个空格(包括制表符)开头。

普通注释和代码块之间可以不用专门的空注释行, 但个人建议还是加上比较好。

1.8. 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)

1.9. 弃用代码声明

Go 所使用的版本号是 vX.Y.Z 的模式, 按照官方的思想, 每当 package 升级时, 尽量不要升级大版本 X 值, 这也同时代表着, 本次升级是完全向前兼容的。但是实际上, 我们在做一些小版本或中版本升级时, 有些函数/类型可能不再推荐使用。此时, GoDoc 提供了一个关键字 Deprecated:, 作为整个注释块的第一个单词, 比如我们可以这么写:

// Deprecated: ElemAt 这个函数弃用, 后续请迁移到 IntsElem 函数中。
func ElemAt(ints []int, index int) int {    
    // ......
}

针对 deprecated 的内容, pkgsite 一方面会在目录中标识出来:

在这里插入图片描述

此外, 在正文中, 也会刻意用灰色字体低调展示, 并且隐藏注释正文, 需要点开才能显示:

在这里插入图片描述

在这里插入图片描述

1.10. 代码示例文档

读者如果看我 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 的时候, 示例文件也会纳入测试逻辑中。

1.10.1. 示例代码的声明

如何声明一个示例代码, 这里我举两个例子。首先是在 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() 函数中, 示例标为一二三四五的原因, 因为这是我希望读者阅读示例的顺序。

1.10.2. 在官网上发布 GoDoc

好了, 当你写好了自己的 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 格式任君选择。

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云满笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值