当你在一个项目上工作时,通常会有一些开发人员的工具依赖。这些不是代码依赖,而是你作为开发、测试、构建或部署过程的一部分而运行的工具。
例如,你可以使用 golang.org/x/text/cmd/gotext与go:generate 结合使用,以生成用于翻译的消息目录,或 honnef.co/go/tools/cmd/staticcheck来在提交修改前对你的代码进行静态分析。
这提出了几个有趣的问题--尤其是在团队环境中。你如何确保每个人的机器上都安装有必要的工具?而且他们所使用的工具都是同一个版本的?
在Go 1.17之前,管理这个问题的惯例是在你的项目中创建一个tools.go 文件,其中包含不同工具的import 语句和一个//go:build tools 构建约束。如果你还不熟悉这种方法,可以在Go的官方维基中找到描述。
但是从Go 1.17开始,你可以采取另一种方法。与 tools.go 方法相比,它有优点也有缺点,但它值得了解,而且可能很适合某些项目。
它取决于这样一个事实:go run 现在允许你执行一个特定版本的远程包。从1.17版本的发行说明中可以看出:
go run现在接受带有版本后缀的参数(例如,go run example.com/cmd@v1.0.0)。这使得go run在模块感知模式下构建和运行软件包,忽略了当前目录或任何父目录中的go.mod文件(如果有的话)。
换句话说,你可以使用go run package@version 来执行一个远程软件包,当你在一个模块之外,或者在一个模块之内,即使该软件包没有在go.mod 文件中列出。
这也是一种快速运行可执行软件包的方法,无需安装它。而不是这样:
$ go install honnef.co/go/tools/cmd/staticcheck@v0.3.1
$ staticcheck ./...
你现在可以直接这样做:
$ go run honnef.co/go/tools/cmd/staticcheck@v0.3.1 ./...
**重要提示:**当你执行go run package@version ,必要的模块将被下载并缓存在你机器的模块缓存中。因此,当你以后执行同样的go run 命令时,将使用缓存(而不是再次下载所有东西),并且应该更快地完成。
与go:generate一起使用
让我们来看看一个例子,我们使用 golang.org/x/tools/cmd/stringer工具与go:generate 结合使用,为一些iota 常量生成String() 方法。
如果你想跟着做,请运行以下命令:
$ mkdir tools
$ go mod init example.com/tools
$ touch main.go
然后在main.go 中加入以下代码。
File: main.go
package main
import "fmt"
//go:generate go run golang.org/x/tools/cmd/stringer@v0.1.10 -type=Level
type Level int
const (
Info Level = iota
Error
Fatal
)
func main() {
fmt.Printf("%s: Hello world!\n", Info)
}
这里最重要的是//go:generate 行。当你在这个文件上运行go generate ,它将反过来使用go run 来执行golang.org/x/tools/cmd/stringer 包的v0.1.10 。
让我们来试试:
$ go generate .
go: downloading golang.org/x/tools v0.1.10
go: downloading golang.org/x/sys v0.0.0-20211019181941-9d821ace8654
go: downloading golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
go: downloading golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3
你应该看到,必要的模块被下载,然后go:generate 命令成功执行完毕--结果是一个新的level_string.go 文件被生成,并有一个工作的应用程序。像这样:
$ ls
go.mod level_string.go main.go
$ go run .
Info: Hello world!
在Makefile中使用
你也可以使用go run package@version 模式来执行你的脚本或Makefile中的工具。为了说明这一点,让我们创建一个带有audit 任务的Makefile,执行一个特定版本的staticcheck 工具。
$ touch Makefile
文件:Makefile
File: Makefile
.PHONY: audit
audit:
go vet ./...
go run honnef.co/go/tools/cmd/staticcheck@v0.3.1 ./...
如果你运行make audit ,必要的模块将被下载,staticcheck 工具应成功完成检查:
$ make audit
go vet ./...
go run honnef.co/go/tools/cmd/staticcheck@v0.3.1 ./...
go: downloading honnef.co/go/tools v0.3.1
go: downloading golang.org/x/tools v0.1.11-0.20220316014157-77aa08bb151a
go: downloading golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e
go: downloading github.com/BurntSushi/toml v0.4.1
如果你第二次运行它,你会看到模块缓存被使用,它应该完成得更快:
$ make audit
go vet ./...
go run honnef.co/go/tools/cmd/staticcheck@v0.3.1 ./...
优点和缺点
就积极方面而言,go run package@version 与tools.go 方法相比有几个不错的优势:
-
它的设置更简单,需要的代码更少--不需要
tools.go文件,没有构建约束,也没有别名的导入。 -
它避免了用你的二进制文件实际上不依赖的东西来污染你的依赖关系图。
就消极因素而言:
-
如果你在整个代码库的多个地方都有相同的
go run package@version命令,并且想要升级到一个新的版本,那么你需要手动更新所有的命令(或者使用sed或查找和替换)。使用tools.go的方法,你只需要通过运行go get package@newversion来更新你的go.mod文件。 -
使用
tools.go方法,可以通过运行go mod verify来验证你的模块缓存中的缓存代码没有被改变。我不知道对go run package@version有同等的检查(如果你知道有这样的方法,请告诉我!)。从我有限的测试来看,似乎可以在你的机器上编辑模块缓存中的代码,而且go run package@version将使用这个编辑过的代码而不抱怨。 -
如果你在离线工作,那么
go run package@version可能会以dial tcp: lookup proxy.golang.org: Temporary failure in name resolution的错误而失败,因为它无法到达 Go 模块镜像 -即使在你的本地模块缓存中已经有一个副本。与此类似。
$ make audit
go vet ./...
go run honnef.co/go/tools/cmd/staticcheck@v0.3.1 ./...
go: honnef.co/go/tools/cmd/staticcheck@v0.3.1: honnef.co/go/tools/cmd/staticcheck@v0.3.1: Get "https://proxy.golang.org/honnef.co/go/tools/cmd/staticcheck/@v/v0.3.1.info": dial tcp: lookup proxy.golang.org: Temporary failure in name resolution
make: *** [Makefile:4: audit] Error 1
在我看来,当你使用tools.go 方法时,这不是一个问题,尽管你可以通过在离线时将GOPROXY 环境变量设置为direct 来相当容易地解决它。这样做会迫使go run 绕过 Go 模块镜像,直接使用你机器上的缓存模块。
$ export GOPROXY=direct
$ make audit
go vet ./...
go run honnef.co/go/tools/cmd/staticcheck@v0.3.1 ./...