Go 1.24 中我最喜欢的功能之一是管理开发者工具依赖项的新功能。
这里指的是,用于辅助开发、测试、构建或部署的工具,例如用于静态代码分析的 staticcheck 、用于漏洞扫描的 govulncheck 或用于实时重新加载应用程序的 air 。
一直以来,管理这些依赖项------尤其是在团队环境中------都是件棘手的事情。之前的解决方案是使用 tools.go 文件或 go run 模式,但尽管这些方法有效,却总感觉像是权宜之计,存在一些缺点。
Go 1.24 终于带来了更好的方法。
一个简单的例子
为了演示新功能,我们搭建一个简单的模块并添加一些应用程序代码。
bash
$ go mod init example.com
go: creating new go.mod: module example.com
$ touch main.go
go
// main.go
package main
import (
"fmt"
"github.com/kr/text"
)
func main() {
wrapped := text.Wrap("This is an informational message that should be wrapped.", 30)
fmt.Println(wrapped)
}
现在下载 github.com/kr/text 包并运行代码。输出结果应该如下所示:
bash
$ go get github.com/kr/text
go: downloading github.com/kr/text v0.2.0
go: added github.com/kr/text v0.2.0
$ go run .
This is an informational
message that should be
wrapped.
向 module 添加 tools
Go 1.24 为 go get 命令引入了 -tool 标志,你可以像这样使用它:
bash
go get -tool import_path@version
此命令将下载导入路径指定的包(及其所有子依赖项),将其存储在模块缓存中,并记录到 go.mod 文件中。 @version 部分是可选的------如果省略,则会下载最新版本。
让我们使用此功能将最新版本的 stringer 和 govulncheck 添加到我们的模块中作为开发者工具,以及 staticcheck 版本 0.5.1 。
bash
$ go get -tool golang.org/x/tools/cmd/stringer
go: downloading golang.org/x/tools v0.30.0
go: downloading golang.org/x/sync v0.11.0
go: downloading golang.org/x/mod v0.23.0
go: added golang.org/x/mod v0.23.0
go: added golang.org/x/sync v0.11.0
go: added golang.org/x/tools v0.30.0
$ go get -tool golang.org/x/vuln/cmd/govulncheck
go: downloading golang.org/x/vuln v1.1.4
go: downloading golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7
go: downloading golang.org/x/sys v0.30.0
go: upgraded golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 => v0.0.0-20240522233618-39ace7a40ae7
go: added golang.org/x/vuln v1.1.4
$ go get -tool honnef.co/go/tools/cmd/staticcheck@v0.5.1
go: downloading honnef.co/go/tools v0.5.1
go: downloading golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678
go: downloading github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c
go: downloading golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa
go: added github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c
go: added golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678
go: added honnef.co/go/tools v0.5.1
运行这些命令后,您的 go.mod 文件现在将包含一个 tool (...) 部分,列出您添加的工具。所有依赖项的相应模块路径和版本将出现在 require (...) 部分,并标记为间接依赖:
bash
module example.com
go 1.24.0
require (
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
github.com/kr/text v0.2.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect
golang.org/x/mod v0.23.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 // indirect
golang.org/x/tools v0.30.0 // indirect
golang.org/x/vuln v1.1.4 // indirect
honnef.co/go/tools v0.5.1 // indirect
)
tool (
golang.org/x/tools/cmd/stringer
golang.org/x/vuln/cmd/govulncheck
honnef.co/go/tools/cmd/staticcheck
)
使用 tools
添加完成后,您可以使用 go tool 命令运行工具。
在命令行
要在 module 中通过命令行运行特定工具,可以使用 go tool 后跟该工具导入路径中最后一个非主版本号部分(通常就是工具名称)。例如:
bash
$ go tool staticcheck -version
staticcheck 2024.1.1 (0.5.1)
$ go tool govulncheck
No vulnerabilities found.
在 Makefile 中
如果您想从脚本或 Makefile 中执行工具, go tool 命令也非常好用。例如,我们创建一个 Makefile,其中包含一个 audit 任务,该任务会对代码库运行 staticcheck 和 govulncheck。
makefile
# Makefile
.PHONY: audit
audit:
go vet ./...
go tool staticcheck ./...
go tool govulncheck
如果运行 make audit ,应该可以看到所有检查都成功完成。
bash
$ make audit
go vet ./...
go tool staticcheck ./...
go tool govulncheck
No vulnerabilities found.
和 go:generate 结合使用
我们再来看一个例子,其中我们将 stringer 工具与 go:generate 结合使用,为一些 iota 常量生成 String() 方法。
go
// main.go
package main
import (
"fmt"
"github.com/kr/text"
)
//go:generate go tool stringer -type=Level
type Level int
const (
Info Level = iota
Error
Fatal
)
func main() {
wrapped := text.Wrap("This is an informational message that should be wrapped.", 30)
fmt.Printf("%s: %s\n", Info, wrapped)
}
这里最重要的是 //go:generate 代码。当你对这个文件运行 go generate 命令时,它会使用 go tool 来执行 go.mod 文件中指定的 stringer 工具版本。
我们来试一试:
bash
$ go generate .
$ ls
go.mod go.sum level_string.go main.go Makefile
你应该会看到一个新的 level_string.go 文件被创建,运行应用程序后应该会产生类似这样的输出:
bash
$ go run .
Info: This is an informational
message that should be
wrapped.
查看 tools
您可以通过运行 go list tool 来查看模块中已添加哪些工具,如下所示
bash
$ go list tool
honnef.co/go/tools/cmd/staticcheck
golang.org/x/tools/cmd/stringer
golang.org/x/vuln/cmd/govulncheck
检验 tools
由于这些工具作为依赖项包含在您的 go.mod 文件中,因此如果您想检查存储在模块缓存中的工具代码是否已更改,只需运行 go mod verify 即可:
bash
$ go mod verify
all modules verified
这将检查模块缓存中的代码是否与 go.sum 文件中的相应校验和完全匹配。
Vendoring tools
如果你运行 go mod vendor ,工具依赖项的代码将包含在 vendor 文件夹中,并且 vendor/modules.txt 清单将与非工具依赖项一起包含在内。
bash
$ go mod vendor
$ tree -L 3
.
├── go.mod
├── go.sum
├── main.go
├── Makefile
└── vendor
├── github.com
│ ├── BurntSushi
│ └── kr
├── golang.org
│ └── x
├── honnef.co
│ └── go
└── modules.txt
以这种方式将工具打包到 vendor 目录中后,运行 go tool 将执行 vendor 目录中的相应代码。请注意, go mod verify 对 vendor 目录中的代码无效。
升级和降级 tools
要将特定工具升级或降级到特定版本,可以使用与最初添加该工具时相同的 go get -tool import_path@version 命令。例如:
bash
go get -tool honnef.co/go/tools/cmd/staticcheck
要升级到特定工具的最新版本,请省略 @version 后缀。
bash
$ go get -tool honnef.co/go/tools/cmd/staticcheck@v0.5.0
您还可以运行 go get tool 将所有工具升级到最新版本。注意:这里的 tool 是一个子命令,而不是一个标志。
bash
$ go get tool
如果您的工具依赖项已通过 vendor 方式提供,则在任何升级或降级后,您都需要重新运行 go mod vendor 。
截至撰写本文时,我还没有找到任何简单的方法来专门列出哪些工具有升级版本------如果您知道这样的方法,请告诉我!
移除 tools
要从模块中完全移除该工具,请使用带有特殊版本标签 @none go get -tool 。
bash
$ go get -tool honnef.co/go/tools/cmd/staticcheck@none
再次提醒,如果您使用了 vendor 方式,请确保在移除工具后运行 go mod vendor 。
使用单独的 mod 文件来管理 tools
一位 Reddit 用户评论道,如果你的工具与应用程序代码共享依赖项,可能会出现问题。例如,假设你的应用程序代码依赖于 golang.org/x/sync v0.11.0 版本,并且经过测试,已知该版本可以正常工作。那么,如果你添加了一个依赖于更新版本的 golang.org/x/sync 的工具,你的 go.mod 文件中的版本号也会更新,你的应用程序代码也会使用更新的版本。
理论上,只要所有依赖项及其子依赖项都稳定,遵循严格的语义化版本控制,并且不会在没有主版本号递增的情况下进行不兼容的更改,这就不会有问题。但是,现实世界总是充满变数,不兼容的更改可能会发生,从而意外地破坏应用程序代码。
值得注意的是,这个问题不仅限于工具依赖项------如果你的应用程序代码和非工具依赖项都依赖于同一个包,也会发生同样的情况。但是,将工具包含在 go.mod 中会增加风险。
为了降低这种风险,你可以使用单独的 modfile 文件来存放工具依赖项,而不是将它们包含在主 go.mod 中。你可以使用 -modfile 标志来指定一个备用文件,例如 go.tool.mod ,如下所示:
bash
# Initialize a go.tool.mod modfile
$ go mod init -modfile=go.tool.mod example.com
# Add a tool to the module
$ go get -tool -modfile=go.tool.mod golang.org/x/vuln/cmd/govulncheck
# Run the tool from the command line
$ go tool -modfile=go.tool.mod govulncheck
# List all tools added to the module
$ go list -modfile=go.tool.mod tool
# Verify the integrity of the tool dependencies
$ go mod verify -modfile=go.tool.mod
# Upgrade or downgrade a tool to a specific version
$ go get -tool -modfile=go.tool.mod golang.org/x/vuln/cmd/govulncheck@v1.1.2
# Upgrade all tools to their latest version
$ go get -modfile=go.tool.mod tool
# Remove a tool from the module
$ go get -tool -modfile=go.tool.mod golang.org/x/vuln/cmd/govulncheck@none