go doc

go doc命令可以打印附于Go语言程序实体上的文档。我们可以通过把程序实体的标识符作为该命令的参数来达到查看其文档的目的。

所谓Go语言的程序实体,是指变量、常量、函数、结构体以及接口。而程序实体的标识符即是代表它们的名称。标识符又分非限定标识符和限定标识符。其中,限定标识符一般用于表示某个代码包中的程序实体或者某个结构体类型中的方法或字段。例如,标准库代码包io中的名为EOF的变量用限定标识符表示即io.EOF。又例如,如果我有一个sync.WaitGroup类型的变量wg并且想调用它的Add方法,那么可以这样写wg.Add()。其中,wg.Add就是一个限定标识符,而后面的()则代表了调用操作。

下面说明怎样使用go doc命令。先来看一下go doc命令课结束的标记。

标记名称 标记描述
-c 加入此标记后会使go doc命令区分参数中字母的大小写。默认情况下,命令是大小写不敏感的。
-cmd 加入此标记后会使go doc命令同时打印出main包中的可导出的程序实体(其名称的首字母大写)的文档。默认情况下,这部分文档是不会被打印出来的。
-u 加入此标记后会使go doc命令同时打印出不可导出的程序实体(其名称的首字母小写)的文档。默认情况下,这部分文档是不会被打印出来的。

这几个标记的意图都非常简单和明确,大家可以根据实际情况选用。

go doc命令可以后跟一个或两个参数。当然,我们也可以不附加任务参数。如果不附加参数,那么go doc命令会试图打印出当前目录所代表的代码包的文档及其中的包级程序实体的列表。

例如,我要在goc2p项目的loadgen代码包所在目录中运行go doc命令的话,那么就会是这样:

1
hc@ubt:~/golang/goc2p/src/loadgen$ go doc
1
2
3
4
5
6
7
8
package loadgen // import "loadgen"

func NewGenerator(
    caller lib.Caller,
    timeoutNs time.Duration,
    lps uint32,
    durationNs time.Duration,
    resultCh chan *lib.CallResult) (lib.Generator, error)

如果你需要指定代码包或程序实体,那么就需要在go doc命令后附上参数了。例如,只要我本地的goc2p项目的所在目录存在于GOPATH环境变量中,我就可以在任意目录中敲入go doc loadgen。如此得到的输出一定是与上面那个示例一致的。

看过loadgen代码包中源码的读者会知道,其中只有一个可导出的程序实体,即NewGenerator函数。这也是上述示例中如此输出的原因。该代码包中的结构体类型myGenerator是不可导出,但是我们只需附加-u标记便可查看它的文档了:

1
hc@ubt:~$ go doc -u loadgen.myGenerator
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type myGenerator struct {
    caller      lib.Caller           // 调用器。
    timeoutNs   time.Duration        // 处理超时时间,单位:纳秒。
    lps         uint32               // 每秒载荷量。
    durationNs  time.Duration        // 负载持续时间,单位:纳秒。
    concurrency uint32               // 并发量。
    tickets     lib.GoTickets        // Goroutine票池。
    stopSign    chan byte            // 停止信号的传递通道。
    cancelSign  byte                 // 取消发送后续结果的信号。
    endSign     chan uint64          // 完结信号的传递通道,同时被用于传递调用执行计数。
    callCount   uint64               // 调用执行计数。
    status      lib.GenStatus        // 状态。
    resultCh    chan *lib.CallResult // 调用结果通道。
}

    载荷发生器的实现

func (gen *myGenerator) Start()
func (gen*myGenerator) Status() lib.GenStatus
func (gen *myGenerator) Stop() (uint64, bool)
func (gen*myGenerator) asyncCall()
func (gen *myGenerator) genLoad(throttle <-chan time.Time)
func (gen*myGenerator) handleStopSign(callCount uint64)
func (gen *myGenerator) init() error
func (gen*myGenerator) interact(rawReq *lib.RawReq)*lib.RawResp
func (gen *myGenerator) sendResult(result*lib.CallResult) bool

如此一来,loadgen.myGenerator类型的文档、字段和方法都尽收眼底。注意,这里我们使用到了限定标识符。下面再进一步,如果你只想查看loadgen.myGenerator类型的init方法的文档,那么只要续写这个限定标识符就可以了,像这样:

1
hc@ubt:~$ go doc -u loadgen.myGenerator.init
1
2
3
func (gen *myGenerator) init() error

    初始化载荷发生器

注意,结构体类型中的字段的文档是无法被单独打印的。另外,go doc命令根据参数查找代码包或程序实体的顺序是:先Go语言根目录(即GOROOT所环境变量指定的那个目录)后工作区目录(即GOPATH环境变量包含的那些目录)。并且,在前者或后者中,go doc命令的查找顺序遵循字典序。因此,如果某个工作区目录中的代码包与标准库中的包重名了,那么它是无法被打印出来的。go doc命令只会打印出第一个匹配的代码包或程序实体的文档。

我们在前面说过,go doc命令还可以接受两个参数。这是一种更加精细的指定代码包或程序实体的方式。一个显著的区别是,如果你想打印标准库代码包net/http中的结构体类型Request的文档,那么可以这样敲入go doc命令:

1
go doc http.Request

注意,这里并没有写入net/http代码包的导入路径,而只是写入了其中的最后一个元素http。但是如果你把http.Request拆成两个参数(即http Request)的话,命令程序就会什么也查不到了。因为这与前一种用法的解析方式是不一样的。正确的做法是,当你指定两个参数时,作为第一个参数的代码包名称必须是完整的导入路径,即:在敲入命令go doc net/http Request后,你会得到想要的结果。

最后,在给定两个参数时,go doc会打印出所有匹配的文档,而不是像给定一个参数时那样只打印出第一个匹配的文档。这对于查找只有大小写不同的多个方法(如New和new)的文档来说非常有用。

1
hc@ubt:~$ go doc fmt

为了节省篇幅,我们在这里略去了文档查询结果。读者可以自己运行一下上述命令。在该命令被执行之后,我们就可以看到编排整齐有序的文档内容了。这包括代码包fmt及其中所有可导出的包级程序实体的声明、文档和例子。

有时候我们只是想查看某一个函数或者结构体类型的文档,那么我们可以将这个函数或者结构体的名称加入命令的后面,像这样:

1
hc@ubt:~$ go doc fmt Printf

或者:

1
hc@ubt:~$ go doc os File

godoc

顾名思义,godoc 就是 Go 语言的文档。在实际应用中,godoc 可能可以指以下含义:

  1. https://godoc.org 中的内容
  2. Go 开发工具安装之后,自带的一个命令,就叫做 godoc
  3. Go 工具包的文档以及生成该文档所相关的格式

我们从 Go 自带的 godoc 工具讲起吧。前面我们说到的 godoc.org,是 Go 最为官方的文档网站。其中我们可以查阅 Go 原生 package 的文档说明。而 godoc 命令的作用,则是可以让我们在本地建立一个属于自己的 godoc 网站服务(官方的 godoc 其实也基本上是用同一个工具建立起来的)。

本地安装godoc工具

1
go get golang.org/x/tools/cmd/godoc

自建的 godoc 有两个作用,一是解决某局域网内无法访问 godoc.org 的尴尬,另一个则是可以本地调试自己的文档。

我们可以用下面的命令在本地启动自己的 godoc 服务:

1
godoc -http=127.0.0.1:6060 -play

或者简写为:

1
godoc -http=:6060 -play

在浏览器输入 http://127.0.0.1:6060 之后,就可以看到熟悉的 Go 文档页面了:

原理上,godoc 读取的包路径来自于 $GOROOT。因此,如果你要让本地的 godoc 认识并解析你自己的开发包,就应该在 $GOROOT 目录下按照路径结构放好自己的工程代码——软链接也是支持的。比如笔者的 jsonvalue 包,我放在了这个路径下:~/project/github.com/Andrew-M-C/go.jsonvalue,于是我就在 $GOROOT 下建了软链接:

1
2
go env | grep GOROOT | sed 's/GOROOT=//g' | xargs cd
ln -s ~/project/github.com ./

然后在浏览器中输入 http://127.0.0.1:6060/pkg/github.com/Andrew-M-C/go.jsonvalue/,就可以看到和 godoc.org 一样的页面了。

godoc 约定

约定

  1. 注释符//后面要加空格, 例如: // xxx

  2. 在package, const, type, func等关键字上面并且紧邻关键字的注释才会被展示

    1
    2
    3
    4
    5
    6
    
    // 此行注释被省略
    
    // 此行注释被展示
    //
    // 此行注释被展示2
    package banana
    
  3. type, const, func以名称为注释的开头, package以Package name为注释的开头

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    // Package banana ...
    package banana
    
    // Xyz ...
    const Xyz = 1
    
    // Abc ...
    type Abc struct {}
    
    // Bcd ...
    func Bcd() {}
    
  4. 有效的关键字注释不应该超过3行

    1
    2
    3
    4
    5
    
    // Package banana ...
    // ...
    // ...
    // 最好不要超过三行
    package banana
    
  5. Package的注释如果超过3行, 应该放在当前包目录下一个单独的文件中, 如:doc.go

  6. 如果当前包目录下包含多个Package注释的go文件(包括doc.go), 那么按照文件名的字母数序优先显示

    1
    2
    3
    4
    5
    6
    
    //----- doc.go -----
    
    /*
    ...第一个显示
    */
    package banana
    
    1
    2
    3
    4
    
    //----- e.go -----
    
    // Package banana ...第二个显示
    package banana
    
    1
    2
    3
    4
    
    //----- f.go -----
    
    // Package banana ...第三个显示
    package banana
    
  7. Package的注释会出现在godoc的包列表中, 但只能展示大约523字节的长度

  8. 在无效注释中以BUG(who)开头的注释, 将被识别为已知bug, 显示在bugs区域, 示例

    1
    2
    3
    4
    
    // BUG(who): 我是bug说明
    
    // Package banana ...
    package banana
    
  9. 如果bug注释和关键字注释中间无换行, 那么混合的注释将被显示在bugs和godoc列表两个区域内

    1
    2
    3
    
    // BUG(who): 我是bug注释
    // Package banana ...也是pkg注释
    package banana
    
  10. 段落:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    /*
    abc ... bcd
    
    Basic(字体加粗变蓝需首字母大写, 中文加粗变蓝需要加上一个大写字母)
    
    abc
    ...
    ... 属于Basic的段落
    ...
    bcd
    */
    package banana
    
  11. 预格式化:

    1
    2
    3
    4
    5
    6
    7
    8
    
    
    /*
    abc ... bcd
    
    Abc(不会加粗变蓝, 预格式化和段落不能同时存在)
    
        abc ... 预格式化需要缩进 ... bcd
    */
    
  12. URL将被转化为HTML链接

Example

  • 文件必须放在当前包下
  • 文件名以example开头, _连接, test结尾, 如:example_xxx_test.go
  • 包名是当前包名 +_test, 如: strings_test
  • 函数名称的格式func Example[FuncName][_tag]()
  • 函数注释会展示在页面上
  • 函数结尾加上// Output:注释, 说明函数返回的值
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 文件必须放在 banana包目录下, 名字必须为example_xxx_test.go

// Package banana_test 为banana包的示例
package banana_test

// 此注释将会被展示在页面上
// 此函数将被展示在OverView区域
func Example() {
    fmt.Println("Hello OverView")

    // Output:
    // Hello OverView
}

// 此函数将被展示在OverView区域, 并展示noOutput标签
func Example_noOutput() {
    fmt.Println("Hello OverView")
    // (Output: )非必须, 存在时将会展示输出结果
}

// 此函数将被展示在Function区域
// Peel必须是banana包实现的方法
func ExamplePeel() {
    fmt.Println("Hello Banana")

    // Output:
    // Hello Banana
}

// 此函数将被展示在Function区域
// Peel必须是banana包实现的方法, 并展示big标签
func ExamplePeel_big() {
    fmt.Println("Hello Banana")

    // Output:
    // Hello Banana
}

godoc 一览

Go 秉承 “注释即文档” 的理念,符合 godoc 的文档均从 Go 代码中提取并生成。我们还是从 jsonvalue 的 godoc 来看,一个一个说明。在 godoc 中,文档包含三大部分:

组成 作用
Overview 总览 包含包的 import 语句和概要说明
Index 目录 包含包中可见性为 public 的常量、类型、方法、函数的总目录及说明
Examples 示例 包含文档中所有示例的快速跳转
Files 文件 列出了包中所有代码文件的超链接
其中第四部分无关紧要。下面我们按顺序说明前三部分

godoc 的 Overview

Package jsonvalue 的 Overview 部分包含了三部分内容:

  • import 语句
  • 文字说明
  • 代码部分

其中 import 部分是 godoc 自动按照 URL 生成的,这个不用管。至于文字部分和代码部分,godoc 都是从源码中提取出来的。提取的原则是:

  • 在代码中所有 package jsonvalue 语句中,找到其上方紧跟着的 // jsonvalue XXX 或者是 /jsonvalue XXX/ 注释块
  • 注释块可以有多行,但必须是连续的 // 或者 /XXX/ 开头。如果需要换行,则留一行空注释
  • 如果找到多个符合条件的注释,则按照文件字母序显示——建议把 Overview 放在一个注释块中,而不要分散撰写。

比如 jsonvalue 的 Overview 说明,统一放在 doc.go 中,这个文件中只有 package jsonvalue 语句以及包说明——这也是不少文章中推荐的做法。

Overview 的文字部分

请读者打开 doc.go,然后对比 godoc,就可以对照着看到文字部分是怎么被 godoc 呈现出来的。

Overview 的代码部分

在注释中,如果在 // 后面的注释文本中,如果以 tab 进行了锁进,那么 godoc 会将这一行视为代码块。比如下面这一段:

其中 “As a quick start:” 行的左边分别为:两个斜杠 + 一个空格。这一行,godoc 视为普通文字;而其余部分的左边为:两个斜杠 + 一个空格 + 一个tab,被 godoc 视为代码部分。于是我们在 godoc 网页上,就可以看到这样的显示结果了:

godoc 的代码文档

godoc 工具会搜寻代码中所有源码文件(自测文件除外),然后展示到页面上。搜索的依据如下:

  • 搜寻对象是代码中所有的公共部分,包括常量、变量、接口、类型、函数
  • 与 Overview 类似,紧跟着一个公共元素的、以该元素开头的注释段,会被 godoc 视为该元素的注释
  • 换行逻辑和代码块逻辑的处理也与 Overview 相同

不过在源码说明中,更多的采用代码示例来说明逻辑,因此在这一环节中,代码块比较少用。

这里我用 jsonvalue 的 At() 函数为例。在代码中,对于 Set() 函数我是这么写的(请无视我蹩脚的英文):

1
2
3
4
5
6
// At completes the following operation of Set(). It defines posttion of value in Set() and return the new value set.
//
// The usage of At() is perhaps the most important. This function will recursivly search for child value, and set the new value specified by Set() or SetXxx() series functions. Please unfold and read the following examples, they are important.
func (s *Set) At(firstParam interface{}, otherParams ...interface{}) (*V, error) {
    ......
}

godoc 解析并格式化效果如下:

godoc 的代码示例

读者可以注意到,在我的 At() 函数下,除了上文提到的文档正文之外,还有五个代码示例。那么,文档中的代码示例又应该如何写呢?

首先,我们应该新建至少一个文件,专门用来存放示例代码。比如我就把示例代码写在了 example_jsonvalue_test.go 文件中。这个文件的 package 名也不得与当前包名相同,而应该命名为 包名_test 的格式。

示例代码的声明

如何声明一个示例代码,这里我举两个例子。首先是在 At() 函数下名为 “Example (1)” 的示例。在代码中,我把这个函数命名为:

1
2
3
func ExampleSet_At_1() {
    ......
}

这个函数命名有几个部分:

函数名组成部分 说明
Example 这是示例代码的固有开头
Set 表示这是类型 Set 的示例
第一个下划线 _ 分隔符,在这个分隔符后面的,是 Set 类型的成员函数名
At 表示这是函数 At() 的示例,搭配前面的内容,则表示这是类型 Set 的成员函数 At() 的示例
第二个下划线_ 分隔符,在这个分隔符后面的内容,是示例代码的额外说明
1 这是示例代码的额外说明,也就是前面 “Example (1)” 括号里的部分

另外,示例代码中应该包含标准输出内容,这样便于读者了解执行情况。标准输出内容在函数内的最后,采用 // Output:单独起一行开头,剩下的每一行标准输出写一行注释。

相对应地,如果你想要给(不属于任何一个类型的)函数写示例的话,则去掉上文中关于 “类型” 的字段;如果你不需要示例的额外说明符,则去掉 “额外说明” 字段。比如说,我给类型 Opt 写的示例就只有一个,在代码中,只有一行:

1
2
3
func ExampleOpt() {
    ........
}

甚至连示例说明都没有。

如果一个元素包含多个例子,那么 godoc 会按照字母序对示例及其相应的说明排序。这也就是为什么我干脆在 At() 函数中,示例标为一二三四五的原因,因为这是我希望读者阅读示例的顺序。

在官网上发布 godoc

好了,当你写好了自己的 godoc 之后,总不是自己看自己自娱自乐吧,总归是要发布出来给大家看的。

其实发布也很简单:当你将包含了 godox 的代码 push 之后(比如发布到 github 上),就可以在浏览器中输入 https://godoc/org/${package路径名}。比如 jsonvalue 的 Github 路径(也等同于 import 路径)为 github.com/Andrew-M-C/go.jsonvalue,因此输入 godoc.org/github.com/Andrew-M-C/go.jsonvalue。

如果这是该页面第一次进入,那么 godoc.org 会首先获取、解析和更新代码仓库中的文档内容,并且格式化之后展示。在页面的底部,会列出该 godoc 的更新时间。

如果你发现官网上的 godoc 内容已经落后了,那么可以点 “Refresh now” 链接刷新它。

接下来更重要的是,把这份官网 godoc 的链接,附到你自己的 README 中。还是点上图的 “Tools” 链接,就可以在新页面中,看到相应的 godoc 徽标的链接了。有 html 和 markdown 格式任君选择。

参考

如何写高大上的 godoc(Go 文档)