10.7. 工具

本章剩下的部分将讨论Go工具箱的特性, 包括如何 下载, 格式化, 构建, 测试 和 安装 Go 程序.

Go的工具箱集合了一系列的功能到一个命令集. 它可以看作是一个包管理器(类似于Linux中的apt和rpm工具), 用于包的查询, 计算的包依赖关系, 从远程版本控制系统和下载它们等任务. 它也是一个构建系统, 计算文件的依赖关系, 然后调用编译器, 汇编器 和 连接器 构建程序, 虽然它故意被设计成没有标准的make命令那么复杂. 它也是一个测试驱动程序, 我们在第11章讨论测试话题.

Go工具箱的命令有着类似"瑞士军刀"的风格, 带着一打子的子命令, 有一些我们经常用到, 例如 get, run, build, 和 fmt 等. 你可以运行 go help 命令查看内置的温度, 为了查询方便, 我们列出了最常用的命令:

$ go
...
    build            compile packages and dependencies
    clean            remove object files
    doc              show documentation for package or symbol
    env              print Go environment information
    fmt              run gofmt on package sources
    get              download and install packages and dependencies
    install          compile and install packages and dependencies
    list             list packages
    run              compile and run Go program
    test             test packages
    version          print Go version
    vet              run go tool vet on packages

Use "go help [command]" for more information about a command.
...

为了达到零配置的目标, Go的工具箱很多地方都依赖各种约定. 例如, 给定的源文件的名称, Go工具可以找到对应的包, 因为每个目录只包含了单一的包, 并且到的导入路径和工作区的目录结构是对应的. 给定一个包的导入路径, Go工具可以找到对应的目录中保存对象的文件. 它还可以发现存储代码仓库的远程服务器的URL.

10.7.1. 工作区结构

对于大多数的Go用户, 只需要配置一个名叫GOPATH的环境变量, 用来指定根工作目录即可. 当需要切换到不同工作区的时候, 只要更新GOPATH就可以了. 例如, 我们在编写本书时, 将GOPATH设置为 $HOME/gobook:

$ export GOPATH=$HOME/gobook
$ go get gopl.io/...

当你用前面介绍的命令下载本书全部的程序之后, 你的当前工作区的目录结构是这样的:

GOPATH/
    src/
        gopl.io/
            .git/
            ch1/
                helloworld/
                    main.go
                dup/
                    main.go
                ...
        golang.org/x/net/
            .git/
            html/
                parse.go
                node.go
                ...
    bin/
        helloworld
        dup
    pkg/
        darwin_amd64/
        ...

GOPATH对应的目录有三个子目录. 其中 src 子目录用于存储源代码. 每个包保存在$GOPATH/src的相对路径为包导入路径的子目录中, 例如 gopl.io/ch1/helloworld 相对路径. 我们看到, 一个GOPATH工作区的src目录中可能有多个独立的版本控制, 例如 gopl.io 或 golang.org. 其中 pkg 子目录用于保存编译后的包的目标文件, bin 子目录用于保存编译后的可执行程序, 例如 helloworld 程序.

第二个环境变量 GOROOT 用来指定Go的安装目录, 还有它自带的标准库包的位置. GOROOT 的目录结构和 GOPATH 类似, 因此存放 fmt 包的源代码目录为 $GOROOT/src/fmt. 用户一般不需要设置 GOROOT, 默认情况下, Go工具会设置为安装的位置.

其中 go env 命令用于查看工具涉及的所有环境变量的值, 包括未设置环境变量的默认值. GOOS 用于指定目标操作系统(例如 android, linux, darwin, 或 windows), GOARCH 用于指定处理器的类型, 例如 amd64, 386, 或 arm. 虽然 GOPATH 是唯一必需要设置的, 但是其它的也有偶尔用到.

$ go env
GOPATH="/home/gopher/gobook"
GOROOT="/usr/local/go"
GOARCH="amd64"
GOOS="darwin"
...

10.7.2. 下载包

使用Go工具, 不仅可以根据包导入路径找到本地工作区的包, 甚至可以从互联网上找到和更新包.

使用命令 go get 可以下载一个单一的包或者用 ... 下载整个子目录里面的每个包. Go工具同时计算并下载所依赖的每个包, 这也是前一个例子中 golang.org/x/net/html 自动出现在本地工作区目录的原因.

一旦 go get 命令下载了包, 然后就是安装包或包对应的命令. 我们将在下一节再关注它的细节, 现在只是展示下整个过程是如何的简单. 第一个命令是获取 golint 工具, 用于检测Go源代码的编程风格是否有问题. 第二个命令是用 golint 对 2.6.2节的 gopl.io/ch2/popcount 包代码进行编码风格检查. 它友好地报告了忘记了包的文档:

$ go get github.com/golang/lint/golint
$ $GOPATH/bin/golint gopl.io/ch2/popcount
src/gopl.io/ch2/popcount/main.go:1:1:
  package comment should be of the form "Package popcount ..."

go get 命令支持当前流行的托管网站 GitHub, Bitbucket, 和 Launchpad, 可以直接从它们的版本控制系统请求代码. 对于其他的网站, 你可能需要指定版本控制系统的具体路径和协议, 例如 Git 或 Mercurial. 运行 go help importpath 获取更新的信息.

go get 获取的代码是真实的本地存储仓库, 不仅仅只是复制文件, 因此你依然可以使用版本管理工具比较本地代码的变更, 或者切换到其他的版本. 例如 golang.org/x/net 目录对应一个 Git 仓库:

$ cd $GOPATH/src/golang.org/x/net
$ git remote -v
origin  https://go.googlesource.com/net (fetch)
origin  https://go.googlesource.com/net (push)

需要注意的是导入路径含有的网站域名和本地Git仓库远程的Git服务地址并不相同, 真实的Git地址是 go.googlesource.com. 这其实是Go工具箱的一个特性, 可以让包用一个自定义的导入路径, 但是真实的代码却是由更通用的服务提供, 例如 googlesource.com 或 github.com. 页面 https://golang.org/x/net/html 包含了如下的元数据, 告诉 Go 工具Git仓库的真实托管地址:

$ go build gopl.io/ch1/fetch
$ ./fetch https://golang.org/x/net/html | grep go-import
<meta name="go-import"
      content="golang.org/x/net git https://go.googlesource.com/net">

如果指定 -u 命令行标志参数, go get 将确保所有的包和依赖的包的版本都是最新的, 然后编译和安装它们. 如果不包含该标志参数, 如果包已经在本地存在, 那么将不会被更新.

go get -u 命令只是简单地保证每个包是最新版本, 如果你是第一次下载则比较很方便的; 但是如果是发布程序则可能是不合适的, 因为本地程序可能需要对依赖的包做精确的版本依赖管理. 通常的解决方案是使用 vendor 目录存储固定版本的代码, 对本地依赖的包的版本更新也是谨慎和持续可控的. 在 Go 1.5 之前, 一般需要修改包的导入路径, 所以复制后 golang.org/x/net/html 导入路径可能会变为 gopl.io/vendor/golang.org/x/net/html. 最新的Go工具已经支持 vendor 特性, 但限于篇幅这里并不讨论细节. 不过可以通过 go help gopath 目录查看 Vendor 目录的帮助.

练习 10.3:http://gopl.io/ch1/helloworld?go-get=1 获取内容, 查看本书的代码的真实托管的网址(go get请求HTML页面时包含了 go-get 参数, 以区别普通的浏览器请求.)

10.7.3. 构建包

go build 命令编译参数指定的每个包. 如果包是一个库, 则忽略输出结果; 这可以用于检测包的可以正确编译的. 如果包的名字是 main, go build 将调用连接器在当前目录创建一个可执行程序; 导入路径的最后一段作为可执行程序的名字.

因为每个目录只包含一个包, 因此每个可执行程序后者叫Unix术语中的命令, 会要求放到一个独立的目录. 这些目录有时候会放在名叫 cmd 目录的子目录下面, 例如用于提供Go文档服务的 golang.org/x/tools/cmd/godoc 命令 (§10.7.4).

每个包可以由它们的导入路径指定, 就像前面看到的那样, 或者有一个相对目录的路径知道, 必须以 ... 开头. 如果没有指定参数, 那么默认指定为当前的目录. 下面的命令用于构建同一个包, 虽然它们的写法各不相同:

$ cd $GOPATH/src/gopl.io/ch1/helloworld
$ go build

或者:

$ cd anywhere
$ go build gopl.io/ch1/helloworld

或者:

$ cd $GOPATH
$ go build ./src/gopl.io/ch1/helloworld

但不能这样:

$ cd $GOPATH
$ go build src/gopl.io/ch1/helloworld
Error: cannot find package "src/gopl.io/ch1/helloworld".

也可以指定包的源文件列表, 一般这只用于构建一些小程序或临时性的实验. 如果是main包, 将以第一个Go源文件的基础文件名作为可执行程序的名字.

$ cat quoteargs.go
package main

import (
    "fmt"
    "os"
)

func main() {
    fmt.Printf("%q\n", os.Args[1:])
}
$ go build quoteargs.go
$ ./quoteargs one "two three" four\ five
["one" "two three" "four five"]

特别是对于这类一次性的程序, 我们系统尽快的构建并运行它. go run 命令结合了构建和运行的两个步骤:

$ go run quoteargs.go one "two three" four\ five
["one" "two three" "four five"]

第一行的参数列表中第一个不是以 .go 结尾的将作为可执行程序的参数运行.

默认情况下, go build 命令构建指定的包和它依赖的包, 然后丢弃所有除了最后的可执行文件之外的中间编译结果. 依赖分析和编译都是很快的, 但是随着项目增加到几十个包和成千上万行代码, 依赖关系分析和编译时间的消耗将变的可观, 可能需要几秒种, 即使这些依赖项没有改变.

go install 命令和 go build 命令很相似, 但是它保存每个包的编译成果, 而不是将它们都丢弃. 被编译的包被保存到 $GOPATH/pkg 目录下和 src 目录对应, 可执行程序被保存到 $GOPATH/bin 目录. (很多用户将 $GOPATH/bin 添加到可执行程序的搜索列表中.) 还有, go install 命令和 go build 命令都不会重新编译没有发生变化的包, 这可以使后续构建更快捷. 为了方便, go build -i 将安装每个目标所依赖的包.

因为编译对应不同的操作系统平台和CPU架构, go install 会将编译结果安装到 GOOS 和 GOARCH 对应的目录. 例如, 在 Mac 系统 golang.org/x/net/html 包将被安装到 $GOPATH/pkg/darwin_amd64 目录下的 golang.org/x/net/html.a 文件.

针对不同操作系统或CPU的交叉构建也是很简单的. 只需要设置好目标对应的GOOS 和 GOARCH, 然后运行构建目录即可. 下面交叉编译的程序将输出它在编译时操作系统和CPU类型:

gopl.io/ch10/cross

func main() {
    fmt.Println(runtime.GOOS, runtime.GOARCH)
}

下面以64位和32位环境分别执行程序:

$ go build gopl.io/ch10/cross
$ ./cross
darwin amd64
$ GOARCH=386 go build gopl.io/ch10/cross
$ ./cross
darwin 386

有些包可能需要针对不同平台和处理器类型输出不同版本的代码, 以便于处理底层的可移植性问题或提供为一些特点代码提供优化. 如果一个文件名包含了一个操作系统或处理器类型名字, 例如 net_linux.go 或 asm_amd64.s, Go工具将只在对应的平台编译这些文件. 还有一个特别的构建注释注释可以提供更多的构建控制. 例如, 文件中如果包含下面的注释:

// +build linux darwin

在包声明的前面(含包的注释), 告诉 go build 只在针对 Linux 或 Mac OS X 是才编译这个文件. 下面的构建注释表示不编译这个文件:

// +build ignore

For more details, see the Build Constraints section of the go/build package’s documentation:

更多细节, 可以参考 go/build 包的构建约束部分的文档.

$ go doc go/build

10.7.4. 包文档

Go的编码风格鼓励为每个包提供良好的文档. 包中每个导出的成员和包声明前都应该包含添加目的和用法说明的注释.

Go中包文档注释一般是完整的句子, 第一行是包的摘要说明, 注释后仅跟着包声明语句. 函数的参数或其他的标识符并不需要额外的引号或其他标记注明. 例如, 下面是 fmt.Fprintf 的文档注释.

// Fprintf formats according to a format specifier and writes to w.
// It returns the number of bytes written and any write error encountered.
func Fprintf(w io.Writer, format string, a ...interface{}) (int, error)

Fprintf 函数格式化的细节在 fmt 包文档中描述. 如果注释后仅跟着包声明语句, 那注释对应整个包的文档. 包文档对应的注释只能有一个(译注: 其实可以多个, 它们会组合成一个包文档注释.), 可以出现在任何一个源文件中. 如果包的注释内容比较长, 可以当到一个独立的文件中; fmt 包注释就有 300 行之多. 这个专门用于保证包文档的文件通常叫 doc.go.

好的文档并不需要面面俱到, 文档本身应该是简洁但可不忽略的. 事实上, Go的风格喜欢简洁的文档, 并且文档也是需要想代码一样维护的. 对于一组声明语句, 可以同一个精炼的句子描述, 如果是显而易见的功能则并不需要注释.

在本书中, 只要空间允许, 我们之前很多包声明都包含了注释文档, 但你可以从标准库中发现很多更好的例子. 有两个工具可以帮到你.

go doc 命令打印包的声明和每个成员的文档注释, 下面是整个包的文档:

$ go doc time
package time // import "time"

Package time provides functionality for measuring and displaying time.

const Nanosecond Duration = 1 ...
func After(d Duration) <-chan Time
func Sleep(d Duration)
func Since(t Time) Duration
func Now() Time
type Duration int64
type Time struct { ... }
...many more...

或者是包的一个成员的注释文档:

$ go doc time.Since
func Since(t Time) Duration

    Since returns the time elapsed since t.
    It is shorthand for time.Now().Sub(t).

或者是包的一个方法的注释文档:

$ go doc time.Duration.Seconds
func (d Duration) Seconds() float64

    Seconds returns the duration as a floating-point number of seconds.

该工具并不需要输入完整的包导入路径或正确的大小写. 下面的命令打印 encoding/json 包的 (*json.Decoder).Decode 方法的文档:

$ go doc json.decode
func (dec *Decoder) Decode(v interface{}) error

    Decode reads the next JSON-encoded value from its input and stores
    it in the value pointed to by v.

第二个工具, 令人困惑的也是名叫 godoc, 提供可以相互交叉引用的 HTML 页面, 但是包含和 go doc 相同以及更多的信息. 10.1 节演示了 time 包的文档, 11.6 节将看到godoc演示可以交互的示例程序. godoc 的在线服务 https://godoc.org, 包含了成千上万的开源包的检索工具.

You can also run an instance of godoc in your workspace if you want to browse your own packages. Visit http://localhost:8000/pkg in your browser while running this command:

你也可以在自己的工作区目录允许 godoc 服务. 运行下面的命令, 然后在浏览器查看 http://localhost:8000/pkg 页面:

$ godoc -http :8000

其中 -analysis=type-analysis=pointer 命令行标志参数用于打开文档和代码中关于静态分析的结果.

10.7.5. 内部包

在Go程序中, 包的封装机制是一个重要的特性. 为导出的标识符只在同一个包内部可以访问, 导出的标识符则是面向全世界可见.

有时候, 一个中间的状态可能也是有用的, 对于一小部分信任的包是可见的, 但并不是对所有调用者都可见. 例如, 当我们计划将一个大的包拆分为很多小的更容易管理的子包, 但是我们并不想将内部的子包结构也完全暴露出去. 同时, 我们肯呢个还希望在内部子包之间共享一些通用的处理包. 或者我们只是想实验一个新包的还并不稳定的接口, 暂时只暴露给一些受限制的客户端.

为了满足这些需求, Go构建工具支持包含 internal 名字的路径段的包导入路径. 这种包叫 internal 包, 一个 internal 包只能被有和internal目录有同一个父目录的包所导入. 例如, net/http/internal/chunked 内部包只能被 net/http/httputil 或 net/http 导入, 但是不能被 net/url 包导入. 但是 net/url 包 可以导入 net/http/httputil.

net/http
net/http/internal/chunked
net/http/httputil
net/url

10.7.6. 查询包

go list 工具可以报告可用包的信息. 其最简单的形式, 可以测试包是否在工作区并打印他的导入路径:

$ go list github.com/go-sql-driver/mysql
github.com/go-sql-driver/mysql

go list 参数还可以用 "..." 表示匹配任意的包的导入路径. 我们可以用它来列表工作区中的所有包:

$ go list ...
archive/tar
archive/zip
bufio
bytes
cmd/addr2line
cmd/api
...many more...

或者是特定子目录下的所有包:

$ go list gopl.io/ch3/...
gopl.io/ch3/basename1
gopl.io/ch3/basename2
gopl.io/ch3/comma
gopl.io/ch3/mandelbrot
gopl.io/ch3/netflag
gopl.io/ch3/printints
gopl.io/ch3/surface

或者是和某个主体相关的:

$ go list ...xml...
encoding/xml
gopl.io/ch7/xmlselect

go list 可以获取每个包完整的元信息, 而不仅仅只是导入路径, 这些信息可以以不同格式提供给用户. 其中 -json 标志参数表示用JSON格式打印每个包的元信息.

$ go list -json hash
{
    "Dir": "/home/gopher/go/src/hash",
    "ImportPath": "hash",
    "Name": "hash",
    "Doc": "Package hash provides interfaces for hash functions.",
    "Target": "/home/gopher/go/pkg/darwin_amd64/hash.a",
    "Goroot": true,
    "Standard": true,
    "Root": "/home/gopher/go",
    "GoFiles": [
            "hash.go"
    ],
    "Imports": [
        "io"
    ],
    "Deps": [
        "errors",
        "io",
        "runtime",
        "sync",
        "sync/atomic",
        "unsafe"
    ]
}

参数 -f 允许用户使用 text/template (§4.6) 的模板语言定义输出文本的格式. 下面的命令打印 strconv 包的依赖的包, 然后用 join 模板函数链接为一行, 用一个空格分隔:

$ go list -f '{{join .Deps " "}}' strconv
errors math runtime unicode/utf8 unsafe

译注: 上面的命令在 Windows 的命令行运行会遇到 template: main:1: unclosed action 的错误. 产生错误的原因是因为命令行对里面的 " " 参数进行转义了. 按照下面的方法解决转义字符串的问题:

$ go list -f "{{join .Deps \" \"}}" strconv

下面的命令打印 compress 子目录下所有包的依赖包列表:

$ go list -f '{{.ImportPath}} -> {{join .Imports " "}}' compress/...
compress/bzip2 -> bufio io sort
compress/flate -> bufio fmt io math sort strconv
compress/gzip -> bufio compress/flate errors fmt hash hash/crc32 io time
compress/lzw -> bufio errors fmt io
compress/zlib -> bufio compress/flate errors fmt hash hash/adler32 io

译注: Windows 下同样有问题, 要避免转义字符串的问题:

$ go list -f "{{.ImportPath}} -> {{join .Imports \" \"}}" compress/...

go list 命令对于一次性的交互式查询或自动化构建和测试脚本都很有帮助. 我们将在 11.2.4节 中再次使用它. 更多的信息, 包括可设置的字段和意义, 可以用 go help list 命令查看.

在本章, 我们解释了Go工具箱除了测试命令之外的所有重要的命令. 在下一章, 我们将看到如何用 go test 命令去测试Go程序.

练习10.4: 创建一个工具, 根据命令行指定的参数, 报告工作区所有依赖指定包的其他包集合. 提示: 你需要运行 go list 命令两次, 一次用于初始化包, 一次用于所有包. 你可能需要用 encoding/json (§4.5) 包来分析输出的 JSON 格式的信息.