【Go命令教程】4. go get
hc@ubt:~$ go get github.com/hyper-carrot/go_lib/logging
命令 go get 可以根据要求和实际情况从互联网上下载或更新指定的代码包及其依赖包,并对它们进行编译和安装。在上面这个示例中,我们从著名的代码托管站点 Github 上下载了一个项目(或称代码包),并安装到了环境变量 GOPATH 中包含的第一个工作区中。与此同时,我们也知道了这个代码包的导入路径就是 github.com/hyper-carrot/go_lib/logging。
一般情况下,为了分离自己与第三方的代码,我们会设置两个或更多的工作区。我们现在有一个目录路径为 /home/hc/golang/lib 的工作区,并且它是环境变量 GOPATH 值中的第一个目录路径。注意,环境变量 GOPATH 中包含的路径不能与环境变量 GOROOT 的值重复。好了,如果我们使用 go get 命令下载和安装代码包,那么这些代码包都会被安装在上面这个工作区中。我们暂且把这个工作区叫做Lib 工作区。在我们运行 go get github.com/hyper-carrot/go_lib/logging 之后,这个代码包就应该会被保存在Lib工作的src目录下,并且已经被安装妥当,如下所示:
/home/hc/golang/lib: bin/ pkg/ linux_386/ github.com/ hyper-carrot/ go_lib/ logging.a src/ github.com/ hyper-carrot/ go_lib/ logging/ ...
另一方面,如果我们想把一个项目上传到 Github 网站(或其他代码托管网站)上并被其他人使用的话,那么我们就应该把这个项目当做一个代码包来看待。其实我们在之前已经提到过原因,go get 命令会将项目下的所有子目录和源码文件存放到第一个工作区的 src 目录下,而 src 目录下的所有子目录都会是某个代码包导入路径的一部分或者全部。也就是说,我们应该直接在项目目录下存放子代码包和源码文件,并且直接存放在项目目录下的源码文件所声明的包名应该与该项目名相同(除非它是命令源码文件)。这样做可以让其他人使用 go get 命令从 Github 站点上下载你的项目之后直接就能使用它。
实际上,像 goc2p 项目这样直接以项目根目录的路径作为工作区路径的做法是不被推荐的。之所以这样做主要是想让读者更容易的理解 Go 语言的工程结构和工作区概念,也可以让读者看到另一种项目结构。当然,如果你的项目使用了 gb 这样的工具那就是另外一回事了。这样的项目的根目录就应该被视为一个工作区(但是你不必把它加入到 GOPATH 环境变量中)。它应该由 git clone 下载到 Go 语言工作区之外的某处,而不是使用 go get 命令。
远程导入路径分析
实际上,go get 命令所做的动作也被叫做代码包远程导入,而传递给该命令的作为代码包导入路径的那个参数又被叫做代码包远程导入路径。
go get 命令不仅可以从像 Github 这样著名的代码托管站点上下载代码包,还可以从任何命令支持的代码版本控制系统(英文为 Version Control System,简称为 VCS)检出代码包。任何代码托管站点都是通过某个或某些代码版本控制系统来提供代码上传下载服务的。所以,更严格地讲,go get 命令所做的是从代码版本控制系统的远程仓库中检出/更新代码包并对其进行编译和安装。
该命令所支持的 VCS 的信息如下表:
表0-2 go get 命令支持的 VCS
名称 | 主命令 | 说明 |
---|---|---|
Mercurial | hg | Mercurial是一种轻量级分布式版本控制系统,采用Python语言实现,易于学习和使用,扩展性强。 |
Git | git | Git最开始是Linux Torvalds为了帮助管理 Linux 内核开发而开发的一个开源的分布式版本控制软件。但现在已被广泛使用。它是被用来进行有效、高速的各种规模项目的版本管理。 |
Subversion | svn | Subversion是一个版本控制系统,也是第一个将分支概念和功能纳入到版本控制模型的系统。但相对于Git和Mercurial而言,它只算是传统版本控制系统的一员。 |
Bazaar | bzr | Bazaar是一个开源的分布式版本控制系统。但相比而言,用它来作为VCS的项目并不多。 |
go get 命令在检出代码包之前必须要知道代码包远程导入路径所对应的版本控制系统和远程仓库的 URL。
如果该代码包在本地工作区中已经存在,则会直接通过分析其路径来确定这几项信息。go get 命令支持的几个版本控制系统都有一个共同点,那就是会在检出的项目目录中存放一个元数据目录,名称为“.”前缀加其主命令名。例如,Git 会在检出的项目目录中加入一个名为 “.git” 的子目录。所以,这样就很容易判定代码包所用的版本控制系统。另外,又由于代码包已经存在,我们只需通过代码版本控制系统的更新命令来更新代码包,因此也就不需要知道其远程仓库的 URL 了。对于已存在于本地工作区的代码包,除非要求强行更新代码包,否则 go get 命令不会进行重复下载。如果想要强行更新代码包,可以在执行 go get 命令时加入 -u 标记。这一标记会稍后介绍。
如果本地工作区中不存在该代码包,那么就只能通过对代码包远程导入路径进行分析来获取相关信息了。首先,go get 命令会对代码包远程导入路径进行静态分析。为了使分析过程更加方便快捷,go get 命令程序中已经预置了几个著名代码托管网站的信息。如下表:
表0-3 预置的代码托管站点的信息
名称 | 主域名 | 支持的VCS | 代码包远程导入路径示例 |
---|---|---|---|
Bitbucket | bitbucket.org | Git, Mercurial | bitbucket.org/user/project bitbucket.org/user/project/sub/directory |
GitHub | github.com | Git | github.com/user/project github.com/user/project/sub/directory |
Google Code Project Hosting | code.google.com | Git, Mercurial, Subversion | code.google.com/p/project code.google.com/p/project/sub/directory code.google.com/p/project.subrepository code.google.com/p/project.subrepository/sub/directory |
Launchpad | launchpad.net | Bazaar | launchpad.net/project launchpad.net/project/series launchpad.net/project/series/sub/directory launchpad.net/~user/project/branch launchpad.net/~user/project/branch/sub/directory |
IBM DevOps Services | hub.jazz.net | Git | hub.jazz.net/git/user/project hub.jazz.net/git/user/project/sub/directory |
一般情况下,代码包远程导入路径中的第一个元素就是代码托管网站的主域名。在静态分析的时候,go get 命令会将代码包远程导入路径与预置的代码托管站点的主域名进行匹配。如果匹配成功,则在对代码包远程导入路径的初步检查后返回正常的返回值或错误信息。如果匹配不成功,则会再对代码包远程导入路径进行动态分析。至于动态分析的过程,我就不在这里详细展开了。
如果对代码包远程导入路径的静态分析或/和动态分析成功并获取到对应的版本控制系统和远程仓库 URL,那么 go get 命令就会进行代码包检出或更新的操作。随后,go get 命令会在必要时以同样的方式检出或更新这个代码包的所有依赖包。
自定义代码包远程导入路径
如果你想把你编写的(被托管在不同的代码托管网站上的)代码包的远程导入路径统一起来,或者不希望让你的代码包中夹杂某个代码托管网站的域名,那么你可以选择自定义你的代码包远程导入路径。这种自定义的实现手段叫做“导入注释”。导入注释的写法示例如下:
package analyzer // import "hypermind.cn/talon/analyzer"
代码包 analyzer 实际上属于我的一个网络爬虫项目。这个项目的代码被托管在了 Github 网站上。它的网址是:https://github.com/hyper-carrot/talon。如果用标准的导入路径来下载 analyzer 代码包的话,命令应该这样写 go get github.com/hyper-carrot/talon/analyzer。不过,如果我们像上面的示例那样在该代码包中的一个源码文件中加入导入注释的话,这样下载它就行不通了。我们来看一看这个导入注释。
导入注释的写法如同一条代码包导入语句。不同的是,它出现在了单行注释符//的右边,因此 Go 语言编译器会忽略掉它。另外,它必须出现在源码文件的第一行语句(也就是代码包声明语句)的右边。只有符合上述这两个位置条件的导入注释才是有效的。再来看其中的引号部分。被双引号包裹的应该是一个符合导入路径语法规则的字符串。其中,hypermind.cn 是我自己的一个域名。实际上,这也是用来替换掉我想隐去的代码托管网站域名及部分路径(这里是 github.com/hyper-carrot)的那部分。在 hypermind.cn 右边的依次是我的项目的名称以及要下载的那个代码包的相对路径。这些与其标准导入路径中的内容都是一致的。为了清晰起见,我们再来做下对比。
github.com/hyper-carrot/talon/analyzer // 标准的导入路径 hypermind.cn /talon/analyzer // 导入注释中的导入路径
你想用你自己的域名替换掉标准导入路径中的哪部分由你自己说了算。不过一般情况下,被替换的部分包括代码托管网站的域名以及你在那里的用户ID就可以了。这足以达到我们最开始说的那两个目的。
虽然我们在talon项目中的所有代码包中都加入了类似的导入注释,但是我们依然无法通过 go get hypermind.cn/talon/analyzer 命令来下载这个代码包。因为域名 hypermind.cn 所指向的网站并没有加入相应的处理逻辑。具体的实现步骤应该是这样的:
-
编写一个可处理HTTP请求的程序。这里无所谓用什么编程语言去实现。当然,我推荐你用 Go 语言去做。
-
将这个处理程序与hypermind.cn/talon这个路径关联在一起,并总是在作为响应的HTML文档的头中写入下面这行内容:
<meta name="go-import" content="hypermind.cn/talon git https://github.com/hyper-carrot/talon">
hypermind.cn/talon/analyzer 熟悉 HTML 的读者都应该知道,这行内容会被视为 HTML 文档的元数据。它实际上 go get 命令的文档中要求的写法。它的模式是这样的:
<meta name="go-import" content="import-prefix vcs repo-root">
实际上,content 属性中的 import-prefix 的位置上应该填入我们自定义的远程代码包导入路径的前缀。这个前缀应该与我们的处理程序关联的那个路径相一致。而 vsc 显然应该代表与版本控制系统有关的标识。还记得表0-2中的主命令列吗?这里的填入内容就应该该列中的某一项。在这里,由于 talon 项目使用的是 Git,所以这里应该填入 git。至于 repo-root,它应该是与该处理程序关联的路径对应的 Github 网站的 URL。在这里,这个路径是 hypermind.cn/talon,那么这个 URL 就应该是 https://github.com/hyper-carrot/talon。后者也是 talon 项目的实际网址。
好了,在我们做好上述处理程序之后,go get hypermind.cn/talon/analyzer 命令的执行结果就会是正确的。analyzer 代码包及其依赖包中的代码会被下载到 GOPATH 环境变量中的第一个工作区目录的 src 子目录中,然后被编译并安装。
注意,具体的代码包源码存放路径会是 /home/hc/golang/lib/src/hypermind.cn/talon/analyzer。也就是说,存放路径(包括代码包源码文件以及相应的归档文件的存放路径)会遵循导入注释中的路径(这里是 hypermind.cn/talon/analyzer),而不是原始的导入路径(这里是 github.com/hyper-carrot/talon/analyzer)。另外,我们只需在 talon 项目的每个代码包中的某一个源码文件中加入导入注释,但这些导入注释中的路径都必须是一致的。在这之后,我们就只能使用 hypermind.cn/talon/ 作为 talon 项目中的代码包的导入路径前缀了。一个反例如下:
hc@ubt:~$ go get github.com/hyper-carrot/talon/analyzer package github.com/hyper-carrot/talon/analyzer: code in directory /home/hc/golang/lib/src/github.com/hyper-carrot/talon/analyzer expects import "hypermind.cn/talon/analyzer" # 反例 #
与自定义的代码包远程导入路径有关的内容我们就介绍到这里。从中我们也可以看出,Go 语言为了让使用者的项目与代码托管网站隔离所作出的努力。只要你有自己的网站和一个不错的域名,这就很容易搞定并且非常值得。这会在你的代码包的使用者面前强化你的品牌,而不是某个代码托管网站的。当然,使你的代码包导入路径整齐划一是最直接的好处。
OK,言归正传,我下面继续关注 go get 这个命令本身。
命令特有标记
go get 命令可以接受所有可用于 go build 命令和 go install 命令的标记。这是因为 go get 命令的内部步骤中完全包含了编译和安装这两个动作。另外,go get 命令还有一些特有的标记,如下表所示:
表0-4 go get 命令的特有标记说明
标记名称 | 标记描述 |
---|---|
-d | 让命令程序只执行下载动作,而不执行安装动作。 |
-f | 仅在使用-u标记时才有效。该标记会让命令程序忽略掉对已下载代码包的导入路径的检查。如果下载并安装的代码包所属的项目是你从别人那里Fork过来的,那么这样做就尤为重要了。 |
-fix | 让命令程序在下载代码包后先执行修正动作,而后再进行编译和安装。 |
-insecure | 允许命令程序使用非安全的scheme(如HTTP)去下载指定的代码包。如果你用的代码仓库(如公司内部的Gitlab)没有HTTPS支持,可以添加此标记。请在确定安全的情况下使用它。 |
-t | 让命令程序同时下载并安装指定的代码包中的测试源码文件中依赖的代码包。 |
-u | 让命令利用网络来更新已有代码包及其依赖包。默认情况下,该命令只会从网络上下载本地不存在的代码包,而不会更新已有的代码包。 |
-v | 打印出被构建的代码包的名字 |
-x | 打印出用到的命令 |
为了更好的理解这几个特有标记,我们先清除 Lib 工作区的 src 目录和 pkg 目录中的所有子目录和文件。现在我们使用带有 -d 标记的 go get 命令来下载同样的代码包:
hc@ubt:~$ go get -d github.com/hyper-carrot/go_lib/logging
现在,让我们再来看一下 Lib 工作区的目录结构:
/home/hc/golang/lib: bin/ pkg/ src/ github.com/ hyper-carrot/ go_lib/ logging/ ...
我们可以看到,go get 命令只将代码包下载到了 Lib 工作区的 src 目录,而没有进行后续的编译和安装动作。这个加入-d标记的结果。
再来看 -fix 标记。我们知道,绝大多数计算机编程语言在进行升级和演进过程中,不可能保证 100% 的向后兼容(Backward Compatibility)。在计算机世界中,向后兼容是指在一个程序或者代码库在更新到较新的版本后,用旧的版本程序创建的软件和系统仍能被正常操作或使用,或在旧版本的代码库的基础上编写的程序仍能正常编译运行的能力。Go 语言的开发者们已想到了这点,并提供了官方的代码升级工具 --fix。fix 工具可以修复因 Go 语言规范变更而造成的语法级别的错误。关于 fix 工具,我们将放在本节的稍后位置予以说明。
假设我们本机安装的Go语言版本是1.5,但我们的程序需要用到一个很早之前用Go语言的0.9版本开发的代码包。那么我们在使用 go get 命令的时候可以加入 -fix 标记。这个标记的作用是在检出代码包之后,先对该代码包中不符合 Go 语言 1.5 版本的语言规范的语法进行修正,然后再下载它的依赖包,最后再对它们进行编译和安装。
标记 -u 的意图和执行的动作都比较简单。我们在执行 go get 命令时加入 -u 标记就意味着,如果在本地工作区中已存在相关的代码包,那么就是用对应的代码版本控制系统的更新命令更新它,并进行编译和安装。这相当于强行更新指定的代码包及其依赖包。我们来看如下示例:
hc@ubt:~$ go get -v github.com/hyper-carrot/go_lib/logging
因为我们在之前已经检出并安装了这个代码包,所以我们执行上面这条命令后什么也没发生。还记得加入标记-v标记意味着会打印出被构建的代码包的名字吗?现在我们使用标记 -u 来强行更新代码包:
hc@ubt:~$ go get -v -u github.com/hyper-carrot/go_lib/logging github.com/hyper-carrot/go_lib (download)
其中,“(download)”后缀意味着命令从远程仓库检出或更新了该行显示的代码包。如果我们要查看附带 -u 的 go get 命令到底做了些什么,还可以加上一个 -x 标记,以打印出用到的命令。读者可以自己试用一下它。
智能的下载
命令 go get 还有一个很值得称道的功能。在使用它检出或更新代码包之后,它会寻找与本地已安装 Go 语言的版本号相对应的标签(tag)或分支(branch)。比如,本机安装 Go 语言的版本是 1.x,那么 go get 命令会在该代码包的远程仓库中寻找名为 “go1” 的标签或者分支。如果找到指定的标签或者分支,则将本地代码包的版本切换到此标签或者分支。如果没有找到指定的标签或者分支,则将本地代码包的版本切换到主干的最新版本。
前面我们说在执行 go get 命令时也可以加入 -x 标记,这样可以看到 go get 命令执行过程中所使用的所有命令。不知道读者是否已经自己尝试了。下面我们还是以代码包 github.com/hyper-carrot/go_lib 为例,并且通过之前示例中的命令的执行此代码包已经被检出到本地。这时我们再次更新这个代码包:
hc@ubt:~$ go get -v -u -x github.com/hyper-carrot/go_lib github.com/hyper-carrot/go_lib (download) cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib git fetch cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib git show-ref cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib git checkout origin/master WORK=/tmp/go-build034263530
在上述示例中,go get 命令通过 git fetch 命令将所有远程分支更新到本地,而后有用 git show-ref 命令列出本地和远程仓库中记录的代码包的所有分支和标签。最后,当确定没有名为 “go1” 的标签或者分支后,go get 命令使用 git checkout origin/master 命令将代码包的版本切换到主干的最新版本。下面,我们在本地增加一个名为 “go1” 的标签,看看 go get 命令的执行过程又会发生什么改变:
hc@ubt:~$ cd ~/golang/lib/src/github.com/hyper-carrot/go_lib hc@ubt:~/golang/lib/src/github.com/hyper-carrot/go_lib$ git tag go1 hc@ubt:~$ go get -v -u -x github.com/hyper-carrot/go_lib github.com/hyper-carrot/go_lib (download) cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib git fetch cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib git show-ref cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib git show-ref tags/go1 origin/go1 cd /home/hc/golang/lib/src/github.com/hyper-carrot/go_lib git checkout tags/go1 WORK=/tmp/go-build636338114
将这两个示例进行对比,我们会很容易发现它们之间的区别。第二个示例的命令执行过程中使用git show-ref查看所有分支和标签,当发现有匹配的信息又通过 git show-ref tags/go1 origin/go1 命令进行精确查找,在确认无误后将本地代码包的版本切换到标签 “go1” 之上。
命令 go get 的这一功能是非常有用的。我们的代码在直接或间接依赖某些同时针对多个 Go 语言版本开发的代码包时,可以自动的检出其正确的版本。也可以说,go get 命令内置了一定的代码包多版本依赖管理的功能。
到这里,我向大家介绍了 go get 命令的使用方式。go get 命令与之前介绍的两个命令一样,是我们编写 Go 语言程序、构建 Go 语言项目时必不可少的辅助工具。
摘自:
http://wiki.jikexueyuan.com/project/go-command-tutorial/0.3.html
自定义路径
即便将代码托管在 GitHub,但我们依然希望使用 自有域名定义下载 和 导入路径。方法很简单,在 Web 服务器对应路径返回中包含 “go-import”跳转信息即可。
myserver.go
package main import ( "fmt" "net/http" ) func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, ` <meta name="go-import" content="qyunhen.com/test git https://githua.com/qyuhen/test"> `) } func main() { http.HandleFunc("/test", handler) http.ListenAndServe(":80", nil) }
编译并启动服务器后,用新路径下载该包。
$ go get -v -insecure qyuhen.com/test Fetching http://qyuhen.com/test?go-get=1 Parsing meta tags from http://qyuhen.com/test?go-get=1 (status code 200) get "qyuhen.com/test"; found meta tag main.meta Import( Prefix:"qyuhen.com/test", VCS:"git" RepoRoot:"https://github.com/qyuhen/test" ) at http://test.com/test?go-get=1 qyuhen.com/test (download)
从输出信息中,可以看到解析过程。最终保存路径不再是 github.com,而是自有域名。只是如此一来,该包就有 2 个下载路径,本地也可能因此存在 2 个副本。为避免版本不一致等情况发生,可添加“import comment”,让编译器检查导入路径是否与该注释一致。
github.com/qyuhen/test/hello.go
package lib // import "qyuhen.com/test" func Hello() { println("Hello!") }
如此,就要求该包必须以“qyuhen.com/test”路径导入,否则编译会出错。因为 go get 也会执行编译操作,所以用“github.com/qyuhen/test”下载安装同样失败。
$ go build can't load package: package test: code in directory github.com/qyuhen/test expects import "qyuhen.com/test"
使用唯一的导入路径,方便日后迁移存储端。糟心的是,此方式对 vendor 机制失效。
摘自:《Go语言学习笔记 . 雨痕》