go 工具来了,集大成,全整合。没了 Makefile 还真有点不习惯。此文甚好,早就想翻译了,无奈最近焦头烂额……不管怎么样,还是动手了。
原文要翻墙,访问请谨慎:The go tool
—————-翻译分隔线—————-
线上介绍了 go 命令的最新 weekly 发布后,我决定写一些关于它的内容。我得承认,在第一次听说统一 go 工具的时候,我满是怀疑并对此非常恐惧。我担心它会像大多数其他语言特定的包管理器一样混乱。个人认为多数这种包管理器都是在重新发明轮子,并且与操作系统的包管理器发生冲突,让系统管理员生活的更加艰辛。另外,我确实喜欢 makefile,它们简单并且直接,工作得也很好。幸运的是,新的 go 工具驱散了我的恐惧!
拒绝重复…
最近在 go nuts 邮件列表上有大量的关于新的 go 工具的信息。官方的 go 文档也包含了一些关于如何使用 go 工具编写 go 代码的短文。
无论如何,我觉得当前在这些文档中还存在一些缺漏,因此这成为了撰写关于新的 go 工具的快速指南,并对一些技巧加以演示的理由。
配置的约定
这是我恐惧最大的来源,多半是因为我在 Ruby on Rails 上的经历。所有熟悉 Rails 的开发者都会同意,每当你尝试做一些微小的改进,一点点小技巧,一些不遵循规则的东西,它就……
不过还是来谈谈最佳实践吧。首先,每个 go 工具只做一件事情,并且把这件事情做得很好。例如,我们有:
go build
– 编译包,go get
– 解析并安装依赖,go test
– 执行测试用例和性能测试,go install
– 安装包,go doc
– 生成文档,go fmt
– 格式化代码,go run
– 构建并执行应用,go tool
– 调用扩展工具,- 等等……
go 包没有任何的构建配置。没有 makefile,没有依赖描述等等。那么它如何工作的?所有都是从代码中获取。为了让这个魔法生效,首先有一件事情要做。需要指定 go 那些七七八八都存在哪里。环境变量 GOPATH 定义了 go 代码树的路径。例如,下面是在 ~/.bashrc 中的:
GOPATH="/home/nu7/gocode"
……告诉 go 工具你的 go 代码树存放在指定的目录中。但是你可能想知道 go 代码树到底是什么?简单说,它就是所有 go 资源、包和命令存放的地方。例如:
$ ls /home/nu7/gocode/ bin pkg src
所有的代码会放到 src 目录。我所说的所有的代码意味着包含你的应用、包和依赖。pkg 目录包含编译和安装后的包,而 bin 存放命令。
GOPATH 变量的工作方式与 PATH 类似,可以随意设置若干个 go 路径。不过务必记得其中的第一个是主要路径,因此使用 go install 安装的内容将全部存放于此。
解决依赖
没有用来描述依赖关系的配置文件……那么高明的 go 工具是如何判断安装什么,以及从哪里下载呢!你认为在某个地方有一个仓库?不对,没有这个!Go 带来了叫做 importpath 的东西,来看看:
import "github.com/nu7hatch/gouuid"
导入路径是二合一的。它是代码仓库 URL 和包将安装的本地路径。go get 工具只需要看看导入路径就知道从哪里获得依赖,而 go build 也能知道在本地从哪里导入它们。
为了在系统中安装依赖,必须这样使用 go get 工具:
$ go get package-name
等等,等等……这里的 package-name 是什么?这是希望安装的依赖的包的名字。假设在 go 代码中有一个叫做 foo 的包,调用 go get foo 将会安装其所有的依赖。同样可以在包中直接运行这个工具:
$ cd ~/gocode/src/foo $ go get .
其他所有的 go 工具都是类似的方式工作,并且可以在包中直接调用,或指定导入路径。同时也可以在一组嵌套的包上使用 …(三个点)通配执行命令。如果 foo 包包含一些嵌套的包,为了一次安装所有需要的依赖,只需要执行:
$ go get ./...
如果指定的依赖已经安装在了 go 代码树中,除非明确指定,否则就不会进行更新。在执行 go get 进行依赖安装的时候,增加 -u 标识进行更新:
$ go get -u package-name
很简单,不是吗?
依赖的地狱!
go tool 有一个约定是我特别喜欢的,不过恐怕与此同时……go 工具通过检出代码库的 HEAD 版本来解决依赖。这强制包保持向下兼容,并且……
绿色主线政策是我在工作中一直坚持的。默认分支总是首先被检出的,因此它应当保持干净,或者至少它应当可以工作!一旦官方发布,或者已经成熟,它应当同样有向下兼容的能力——总不能在补丁或者小版本上推翻或者修改 API 吧。
不过我们都知道在实践的时候是怎么回事。许多人像把向下兼容当作一坨屎,或把默认分支当作游乐场,等等。对于那些人,以及所有期望同新的 go 工具和平共处的开发者,我有一些建议……
在该死的程序员生涯中应当遵循的守则:
- 保持该死的主线干净!
- 在该死的独立分支上进行该死的新功能的开发!
- 一旦你公布了代码,并且某些人使用了它,那就 TMD 不要修改 API!
- 如果想要或者必须修改 API,修改 TMD 主版本号 并且 在独立于原代码库的新的代码库上进行该死的开发!
- 如果有必要,非常有必要使用某些特别的标签,做分支或提交作为依赖,你得 TMD 用自己 fork 出来的代码库进行 TMD 所需要的提交!
- 哦,还有保持简单,狗娘养的!
别让我给你读以西结书(译注:《希伯来圣经》中的一部先知书)来提醒你这些……
构建和安装
好,让我们回到 go 命令。go build 命令用于编译包。它仅仅编译指定包,而不会进行安装。重要的是,它要求包已经检出在本地代码树中。要安装远程包应当使用 go get 来代替:
$ go get github.com/nu7hatch/gouuid
安装本地包当然使用 go install 工具。(如果有必要的话)它首先构建包然后将其安装在 $GOPATH/pkg 或者/以及 $GOPATH/bin。
go 工具在构建的时候也能忽略文件,无需显式的指定额外的参数或者特别的配置。对于要忽略的文件唯一要做的,就是让其名字前有一个下划线:
$ ls _bar.go foo.go $ go build .
在上面的例子中,构建时 _bar.go 将会被忽略。
唔,就这样吧……我想关于这些也没什么好说的了,让我们继续下面的内容。
用 CGO 的 C 扩展
go 通过 cgo 命令获得了构建 C 扩展的良好支持。实际上,编译大多数 C 支持的应用,甚至无需了解 cgo,go build 工具足够了。
实话说,关于 cgo 真没什么好讲的,文档和 go user wiki 中的文章已经涵盖了大部分内容。
首先要说说的是那些我确实不喜欢的东西,也就是在官方例子中展示的将 C 代码写在注释中的做法。你得知道,这些例子主要是为了减少代码的尺寸并让每个例子都在同一个文件中演示。在实际的应用中 C 代码不应当放在注释部分!go build 工具能够聪明的处理包中的 .h 和 .c 文件。
需要一些例子?来看一个将所有参数都打印到屏幕上的简单的 echo 命令,不过要使用来自 stdio.h 的 printf 函数。与 wiki 上所述一致,go 不允许调用变长参数的 C 函数,因此需要对 printf 函数编写一个简单的包裹。代码看起来大约是这样(同样可在 github 上浏览到):
echo.h:
#ifndef _ECHO_H_ #define _ECHO_H_ #include <stdio.h> void echo(char*); #endif /* _ECHO_H_ */
echo.c:
#include "echo.h" void echo(char* s) { printf("%s\n", s); }
echo.go:
package main /* #include <stdlib.h> #include "echo.h" */ import "C" import ( "flag" "unsafe" "strings" ) func main() { flag.Parse() cs := C.CString(strings.Join(flag.Args(), " ")) C.echo(cs) C.free(unsafe.Pointer(cs)) }
现在可以用 go build 工具无缝的编译所有的东西。它能识别出在包中所有的 C 文件。简单来说这就能工作了!
特定平台的构建
go build 另一个很酷却又有趣的地方是处理特定平台的文件。它能通过名字来识别(必须是像 file_GOOS_GOARCH.go 或者 file_GOARCH.go ):
foo_darwin_amd64.go foo_386.go foo.go
这个功能对 C 文件同样适用:
foo_amd64.c foo_386.c foo.h foo.go
就像文档里说的那样,你可能永远都用不到这个功能,不过我还是像给大家展示一下 go 工具在如此简单的基础上做到了多么的灵活。
好,但是你们当中可能有人会问,如果要做一些棘手的事情需要特殊的编译参数或者一些配置,例如……
救世主 Makefile
是的,不要害怕使用 makefile!这是最简便的方法来做额外的配置,一些预请求等等。makefile 不但对于 C 扩展很有帮助,对于具有多个包的应用也同样适用(例如,在 webrocket 中使用了一个顶级的 makefile 来让任务更加轻松)。
更加清楚的例子……设想一个包含了内核包和基于此的命令行工具的应用。可以套用 echo 的例子,不过更加模块化:
echo/ pkg/ echo/ echo.c echo.h echo.go cmd/ echo/ echo.go
希望 pkg/echo 包提供一个可重用的 C printf 函数的包裹,它的代码看起来跟前面的例子中的一样。cmd/echo 命令是一个使用了核心包来打印到屏幕的可执行文件。cmd/echo 命令是这样:
package main import ( "github.com/nu7hatch/cgoecho2/pkg/echo" "flag" ) func main() { flag.Parse() echo.Echo(flag.Args()...) }
注意:对于那些不了解 slice 的人来说 … 意味着将一个 slice 映射到变长参数,就像 Ruby 中的 *args。
回到主题,为了让对包的工作更加简单,需要一些 Makefile。看起来大约是这样:
all: echo-pkg echo-cmd echo-pkg: go build ./pkg/echo echo-cmd: go build ./cmd/echo
现在就可以通过调用 make 来快速的编译包和命令两个部分,还有一个很重要的,可以使用 go 命令远程安装:
$ go get github.com/nu7hatch/cgoecho2/cmd/echo
当然这时非常简单的一个例子,完全可以使用通配来进行快速的构建,而不必这么小题大作:
$ go build ./...
但是面对更大的应用,包含许多包和/或命令就会是王道了。那时有各种理由使用 makefile、shell 脚本或者任何你喜欢的构建工具。
总结
我不得不明确表态,我 TMD 超级喜欢新的 go 工具!在第一次用的时候,我遇到了一大堆麻烦,不过大多数都是因为我从其他包管理工具上养成的坏习惯。哈,最近我在 go-nuts IRC 上问了许多愚蠢的问题,而我想要的答案是如此的显而易见并且简单……
现在回顾用过的全部工具,例如 easy_install、rubygems 或者 bundler,我对它们只有一个印象……哦,我还是不要说出来了,免得许多人恨我 :)。取而代之,我可以向你展示我是如何看待新的 go 工具的……
看到 Go 正在向好的方向发展,我很欣慰!今天就到这里吧,好运,Gopher 们!
有疑问加站长微信联系(非本文作者)