关于go命令

Go 发行版中包含一个名为 "go" 的命令,它能自动完成 Go 包和命令的下载、构建、安装及测试工作。本文档将讨论我们编写这个新命令的原因、它的功能定位、非设计目标以及使用方法。

设计初衷

你或许看过早期的 Go 演讲,其中罗布·派克开玩笑说 Go 的创意诞生于等待大型 Google 服务器编译的过程中。这确实是 Go 的核心动机:构建一门能够高效处理 Google 所编写和运行的大型软件的语言。从一开始就很明确,这样的语言必须提供清晰表达代码库间依赖关系的方法,因此有了 package 分组和显式 import 代码块。同样从一开始就清楚,你需要能够用任意语法描述被导入的代码,这就是为什么导入路径是字符串字面量。

Go 从设立之初就有一个明确目标:仅凭源代码本身的信息就能构建 Go 程序,无需编写 makefile 或其他各种现代构建工具的配置文件。如果 Go 需要配置文件来解释如何构建程序,那它就是失败的。

最初,Go 还没有编译器,早期开发工作集中于构建编译器和相应的库。为了快速推进,我们推迟了构建自动化的实现,转而使用 make 和 makefile。当编译单个包需要多次调用 Go 编译器时,我们甚至使用程序来自动生成 makefile。你可以在仓库历史中找到这个程序。

新 go 命令的诞生正是我们回归最初理想的体现:Go 程序应该无需配置,开发者除了编写必要的 import 语句外无需额外操作即可完成编译。

配置与约定

实现零配置系统的关键在于建立约定。系统仅在约定被遵守的情况下有效。在 Go 首次发布时,许多人发布的包需要安装在特定路径、使用特定名称、借助特定构建工具才能使用。这可以理解,因为这是大多数其他语言的做法。过去几年中,我们持续向用户强调 goinstall 命令(现已被 go get 取代)及其约定:首先,导入路径应从源代码 URL 以已知方式推导;其次,本地文件系统中源代码存储位置应从导入路径以已知方式推导;第三,源代码树中的每个目录对应单个包;第四,包应仅使用源代码中的信息构建。如今,绝大多数包都遵循这些约定,这使得 Go 生态系统变得更加简洁强大。

我们收到过许多请求,希望允许在包目录中通过 makefile 提供源代码之外的额外配置。但这将引入新规则。由于我们没有接受这类请求,才得以编写 go 命令并彻底告别 make 或其他构建系统的使用。

需要理解的是,go 命令并非通用构建工具。它不可配置,且不尝试构建 Go 包以外的任何内容。这些重要的简化假设不仅简化了实现,更重要的是简化了工具本身的使用。

Go 的约定体系

go 命令要求代码遵循若干关键且成熟的技术约定。

首先,导入路径应从源代码 URL 以已知方式推导。对于 Bitbucket、GitHub、Google Code 和 Launchpad,仓库根目录由去除 https:// 前缀的主要 URL 标识。子目录则通过在此路径上追加名称来定义。例如,Google 日志包 glog 的源代码可通过运行

git clone https://github.com/golang/glog
获取,因此 glog 包的导入路径为 "github.com/golang/glog"。

这些路径虽然较长,但换来的是自动管理的导入路径命名空间,以及让 go 命令等工具能够通过陌生导入路径推断源代码获取位置的能力。

其次,本地文件系统中的源代码存储位置应从导入路径以已知方式推导,具体为 $GOPATH/src/<import-path>。若未设置,$GOPATH 默认指向用户主目录下的 go 子目录。如果 $GOPATH 被设置为路径列表,go 命令将依次尝试 <dir>/src/<import-path> 中的每个目录。

按照约定,这些代码树都包含顶层目录 "bin"(存放编译后的可执行文件)、顶层目录 "pkg"(存放可导入的已编译包)以及 "src" 目录(存放包源文件)。这种结构使我们能够保持每个代码树自成体系:编译产物与源代码始终紧密相邻。

这些命名约定还允许我们反向推导:从目录名得到导入路径。这种映射关系对 go 命令的许多子命令至关重要,我们将在下文中看到。

第三,源代码树中的每个目录对应一个单独的包。通过将目录限制为单个包,我们无需创建混合导入路径来同时指定目录和该目录内的包。此外,大多数文件管理工具和用户界面都以目录作为基本操作单元。将Go语言的基本单元——包——与文件系统结构绑定,意味着文件系统工具也能成为Go包管理工具。复制、移动或删除一个包,就等同于复制、移动或删除一个目录。

第四,每个包的构建仅依赖于源文件中存在的信息。这使得构建工具能够更轻松地适应不断变化的构建环境和条件。例如,如果我们允许额外的配置(如编译器标志或命令行构建规则),那么每次构建工具更新时都需要同步修改该配置;而且这种配置本身也会与特定工具链的使用绑定。

开始使用go命令

最后,快速介绍如何使用go命令。如前所述,Unix系统上默认的 $GOPATH$HOME/go,我们将把程序存放在此处。若要使用其他位置,可以通过设置 $GOPATH 实现;详情请参阅《如何编写Go代码》

首先添加一些源代码。假设我们要使用codesearch项目的索引库以及一个左倾红黑树,可以通过 "go get" 子命令安装两者:

$ go get github.com/google/codesearch/index
$ go get github.com/petar/GoLLRB/llrb
$

这两个项目现已下载并安装到 $HOME/go 中,包含两个目录:src/github.com/google/codesearch/index/src/github.com/petar/GoLLRB/llrb/,以及这些库及其依赖项的编译包(位于 pkg/ 目录下)。

由于我们使用版本控制系统(Mercurial和Git)检出了源代码,源代码树中还包含对应代码仓库中的其他文件,例如相关包。"go list" 子命令会列出其参数对应的导入路径,模式 "./..." 表示从当前目录("./")开始,查找该目录下的所有包("..."):

$ cd $HOME/go/src
$ go list ./...
github.com/google/codesearch/cmd/cgrep
github.com/google/codesearch/cmd/cindex
github.com/google/codesearch/cmd/csearch
github.com/google/codesearch/index
github.com/google/codesearch/regexp
github.com/google/codesearch/sparse
github.com/petar/GoLLRB/example
github.com/petar/GoLLRB/llrb
$

我们还可以测试这些包:

$ go test ./...
?   	github.com/google/codesearch/cmd/cgrep	[no test files]
?   	github.com/google/codesearch/cmd/cindex	[no test files]
?   	github.com/google/codesearch/cmd/csearch	[no test files]
ok  	github.com/google/codesearch/index	0.203s
ok  	github.com/google/codesearch/regexp	0.017s
?   	github.com/google/codesearch/sparse	[no test files]
?       github.com/petar/GoLLRB/example          [no test files]
ok      github.com/petar/GoLLRB/llrb             0.231s
$

如果go子命令未列出路径参数,则默认对当前目录执行操作:

$ cd github.com/google/codesearch/regexp
$ go list
github.com/google/codesearch/regexp
$ go test -v
=== RUN   TestNstateEnc
--- PASS: TestNstateEnc (0.00s)
=== RUN   TestMatch
--- PASS: TestMatch (0.00s)
=== RUN   TestGrep
--- PASS: TestGrep (0.00s)
PASS
ok  	github.com/google/codesearch/regexp	0.018s
$ go install
$

"go install" 子命令会将包的最新副本安装到pkg目录中。由于go命令可以分析依赖关系图,"go install" 还会递归安装该包导入的、但已过时的所有依赖包。

请注意,"go install" 能够根据当前目录确定包的导入路径,这得益于目录命名的约定。如果我们可以自由选择存放源代码的目录名,可能会更方便,而且我们可能也不会选择如此长的名字,但这种能力需要工具引入额外的配置和复杂性。多输入一两个目录名作为代价,换来的是简洁性和功能性的提升。

局限性

如前所述,go命令并非通用构建工具。特别是,它不具备在构建过程中生成Go源文件的功能,尽管它提供了go generate命令,可在构建前自动创建Go文件。对于更高级的构建场景,您可能需要编写Makefile(或您选择的构建工具的配置文件)来运行生成Go文件的工具,然后将这些生成的源文件纳入版本控制。这对包作者而言是更多的工作,但对用户而言则轻松得多——他们只需使用 "go get" 即可,无需获取和构建额外的工具。

更多信息

欲了解更多详情,请阅读《如何编写Go代码》并参阅go命令文档