Golang中go mod download && go mod vendor && go mod tidy为什么经常需要运行两次才能成功

Golang中go mod download && go mod vendor && go mod tidy为什么经常需要运行两次才能成功 Go 模块工具经常在第一次运行时失败。它会产生一些虚假的错误,例如 missing go.sum entry。因此,我常常不得不连续运行以下命令序列两次:

  1. go mod download
  2. go mod vendor (推荐)
  3. go mod tidy

Go 能否修复一下这个模块系统?不仅重复运行两次这个序列很累人,这让我在众多 Go 项目中的工作量翻倍。而且,为了更新 Go 的缓存系统,需要调用三个不同的命令,这本身就很傻。

确实应该有一个简单的命令,比如 go mod refresh,来执行这些底层的步骤。它应该读取 go.mod 配置文件中的一个可选布尔参数,来控制是否执行 vendor 操作。它绝不应该要求重复运行命令来解决那些底层可恢复的错误。

支持一个单一、统一的刷新命令的另一个理由是,多个命令会在较大的脚本中引入细微的错误。POSIX shell 可能会使用 && 和/或 set -e 来确保真正的错误能传播到下游系统,例如 CI/CD 作业。但这种语法在 *nix 环境之外并不通用。命令提示符 (Command Prompt) 和 PowerShell 很难可靠地向上传递准确的退出码。将这些操作合并到一个命令中,可以使运行 go mod… 命令的脚本更加可靠,而不必求助于将所有内容塞进一个更冗长的 mage Go 任务中。


更多关于Golang中go mod download && go mod vendor && go mod tidy为什么经常需要运行两次才能成功的实战教程也可以访问 https://www.itying.com/category-94-b0.html

11 回复

是的。

要是 -a 能支持新文件就好了。

更多关于Golang中go mod download && go mod vendor && go mod tidy为什么经常需要运行两次才能成功的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


同意。我从未遇到过仅使用 go mod tidy 的问题。

只需运行 go mod tidy。它会自动下载所有内容并添加到 go.sum 文件中。

在我的机器上,go mod vendor 经常提示我需要事先手动运行 go mod download

你好。出现这个错误是因为步骤顺序错了。只需将你需要的模块添加到 go.mod 文件中(或者作为导入语句添加到你在处理的文件中),然后直接运行 go mod tidy 命令。它会自动下载所有依赖并添加到 go.sum 文件中。之后,如果你需要的话,可以运行 go mod vendor 命令。

go mod tidy
go mod vendor

mcandre:

必须运行两个独立的命令来更新依赖缓存是有风险的,尤其是在操作系统/Shell的可移植性方面。

这有什么风险?

mcandre:

编程语言可以通过在 go.mod 中采用一个布尔选项来改进,该选项可以在每次运行 go mod tidy 时自动触发 go mod vendor

你随时可以创建一个提案。我不知道它会获得多少关注,但我想这会是一个相对较小的改动。

go mod tidy 后接 go mod vendor 似乎可以工作。

然而,再次强调,为了更新依赖缓存而需要运行两个独立的命令是有风险的,尤其是在操作系统/Shell的兼容性方面。这门编程语言可以通过在 go.mod 中采用一个布尔选项来改进,该选项可以在每次运行 go mod tidy 时自动触发 go mod vendor

目前尚不清楚是否还有其他情况需要单独运行 go mod download

Vendor 不是一个推荐的做法。如果你需要存储特定版本的包,或者根据你的需求对它们进行修改,那么才需要使用它。这具有很强的场景依赖性,每位开发者都希望能有机会控制这一点。另外,看起来你并不清楚缓存的位置。缓存是一个全局存储,你可以通过 go mod tidy 来更新它。go mod vendor 只是将你已下载到缓存中的更新,同步到本地项目的 vendor 文件夹里。

func main() {
    fmt.Println("hello world")
}

go mod tidy 应该就足够了。但你必须先下载模块才能进行 vendoring。同时,它会检查 vendor/modules.txt 与你的 go.mod 是否存在差异,并报告错误

go mod vendor 还会创建 vendor/modules.txt 文件,其中包含已 vendor 的包列表以及它们是从哪个模块版本复制而来的。当启用 vendoring 时,此清单将作为模块版本信息的来源,由 go list -mgo version -m 报告。当 go 命令读取 vendor/modules.txt 时,它会检查模块版本是否与 go.mod 一致。如果自 vendor/modules.txt 生成后 go.mod 发生了变化,go 命令将报告错误。应再次运行 go mod vendor 以更新 vendor 目录。

尽管如此,你应该先运行一次 go mod tidy(清理/下载依赖),然后再运行 go mod vendor(对我们刚刚下载的依赖进行 vendoring)。

我觉得你严重夸大了这件事的“风险”程度。使用 git add 添加文件到暂存区,然后 git commit 提交它们,这有风险吗?

大多数时候,开发者运行这些命令是为了获取依赖项并将其纳入 vendor 目录。根据我的经验,由于 Go 项目倾向于使用比大多数语言更少的依赖项,我们并不经常运行 go mod tidy。但如果出了问题,我们当然需要在执行 go mod vendor 之前修复它。我不明白一个同时执行这两个操作的命令能带来什么好处。如果你想要一个脚本来运行两者:

#!/bin/bash

# 失败时立即退出
set -e

# 整理我们的 go mod 并下载依赖项
go mod tidy
# 将我们的依赖项纳入 vendor 目录
go mod vendor

mcandre: 然而,即使是 make/mage 也会增加比严格必要更大的攻击面,使构建过程复杂化,并拖慢 CI/CD 作业。

当你的 CI/CD 作业运行时,你的代码应该已经被更新依赖项的开发者进行了 vendor 操作和审查。再说一次——如果你打算在 CI/CD 作业运行器中运行多个命令,据我所知,如果其中一个命令失败,它们都会立即失败。例如,在 GitLab 作业运行器中:

build:api:
  image: golang:1.22
  stage: build
  script:
    - go mod tidy # 出错时作业会立即失败
    - go mod vendor # CI/CD 流水线为什么要做这个?

但即便如此——你也不会增加攻击面。你只会得到一个失败的构建。

如果你指的是将依赖项更新到具有安全修复的较新版本,这通常不由 CI/CD 流水线处理(你也不希望它处理!)。例如,GitHub 有 dependabot,它会扫描你的依赖项并创建自动化的拉取请求来将它们升级到更新的版本。但这些拉取请求是由人类开发者审查的。

这有什么风险?

问得好。

在诸如 bash、现代 sh、ksh 和 zsh 这样的 POSIX shell 中,你可以使用双与符号和/或设置 -e 选项来确保命令的退出码错误不会被忽略。但大多数人会使用不安全的形式:分号或换行。因此,任何脚本或交互式会话都可能忽略错误并继续运行后续命令。

这还没涉及到 pipefailIFS、未定义变量、通配符展开以及其他 shell 陷阱。或者 trap 的语义。任何在 CLI 上下文中运行的东西本质上都是一坨脆弱的■■■■,因此命令字符的简洁性是一种恩赐。不是应用程序行为的简洁性,而是涉及哪些连接词或其他无意义内容的简洁性。

错误仍然被隐藏。已经损坏的状态可能变得更加损坏。狼群嚎叫。大地震颤。

原生的 Windows shell 并不一致支持 POSIX sh 的连接符或安全标志。尝试应用它们往往会破坏命令提示符 / MS-DOS 批处理文件或 PowerShell 命令。

单引号、转义语法、POSIX 与 GNU 与 BSD 的 find 命令、标准输入输出句柄的名称……这些细节导致了成千上万“有用”的 Stack Overflow 帖子,其中的命令片段在实际使用中几乎总是会出问题。它们被供应商锁定,并且未能考虑状态空间的很大一部分。

Fish、(t)csh、ion 以及其他非 POSIX shell 同样不支持 100% 的这些安全选项。

两步命令模式往往会破坏安全性、可移植性,或者两者兼而有之。

作为一种变通方法,你可以把它们塞进一个 POSIX makefile 里。或者一个 mage 文件里。(Mage 允许你用纯 Go 代码编写任务,通常具有可移植性。)然而,即使是 make/mage 也会增加比严格必要更大的攻击面,使构建过程复杂化,并拖慢 CI/CD 作业。

一个执行更多操作的统一命令,有助于鼓励安全、可移植的构建步骤。

这是一个在Go社区中经常遇到的问题,主要与Go模块系统的内部状态管理和缓存机制有关。当依赖图发生变化或go.mod/go.sum文件不同步时,第一次运行命令可能无法完全解决所有不一致。

根本原因在于这些命令各自有不同的职责,且执行时对模块缓存的假设不同:

  • go mod download:下载依赖到模块缓存,但不更新go.sum
  • go mod vendor:将依赖复制到vendor目录,依赖完整的go.sum
  • go mod tidy:整理依赖,会添加缺失的go.sum条目。

go.sum不完整时,go mod vendor可能因缺少校验和而失败,此时需要go mod tidy先运行来补充go.sum。但go mod tidy本身可能需要依赖已被下载到缓存中。

典型的解决模式确实是运行两次序列,但可以通过调整顺序来避免:

go mod tidy
go mod vendor

这样go mod tidy会先确保go.sum完整,然后go mod vendor就能成功。

如果确实需要合并操作,可以创建一个简单的Go脚本来封装:

// refresh.go
package main

import (
    "os"
    "os/exec"
)

func main() {
    cmds := []string{"go mod tidy", "go mod vendor"}
    for _, cmd := range cmds {
        c := exec.Command("sh", "-c", cmd)
        c.Stdout = os.Stdout
        c.Stderr = os.Stderr
        if err := c.Run(); err != nil {
            os.Exit(1)
        }
    }
}

然后运行:

go run refresh.go

或者使用Makefile:

.PHONY: vendor
vendor:
    go mod tidy
    go mod vendor

虽然目前没有内置的go mod refresh命令,但Go团队正在持续改进模块工具链。在1.16版本中,go mod tidy已经变得更加可靠,减少了需要多次运行的情况。对于vendor操作,可以考虑是否真正需要,因为自Go 1.14以来,模块模式下的构建已经可以很好地处理依赖,无需vendor。

回到顶部