如何在 Go 1.24+ 中管理 tool 依赖

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 部分是可选的------如果省略,则会下载最新版本。

让我们使用此功能将最新版本的 stringergovulncheck 添加到我们的模块中作为开发者工具,以及 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
相关推荐
程序员爱钓鱼2 小时前
用 Go 做浏览器自动化?chromedp 带你飞!
后端·go·trae
小信啊啊9 小时前
Go语言结构体
golang·go
moxiaoran575321 小时前
Go语言的常量
go
武大打工仔1 天前
如何理解 Golang 中的 Context?
go
Java陈序员2 天前
精致简约!一款优雅的开源云盘系统!
mysql·docker·开源·go·云盘
捧 花2 天前
Go语言模板的使用
golang·go·template method·模板·web app
凉凉的知识库2 天前
在Go中读取MySQL Date类型,一不小心就踩坑
mysql·面试·go
51973 天前
goup是一个纯Rust编写的优雅的Go多版本管理工具
go