Go Wiki:Go Modules

本 Wiki 页面用作使用和故障排除指南。

自 1.11 版本以来,Go 已包含对版本化模块的支持,如此处提出的。最初的原型 vgo 于 2018 年 2 月发布。2018 年 7 月,版本化模块加入了 Go 主仓库。

Go 1.14 起,模块支持被认为已可用于生产环境,鼓励所有用户从其他依赖管理系统迁移到模块。如果您因 Go 工具链中的问题而无法迁移,请确保该问题已有未关闭的问题提交。(如果该问题不在 Go1.16 里程碑中,请评论说明为何它阻止您迁移,以便适当地优先处理)。您还可以提供一份使用体验报告以提供更详细的反馈。

最新更改

Go 1.16

详细信息请参阅Go 1.16 版本说明

Go 1.15

详细信息请参阅Go 1.15 版本说明

Go 1.14

详细信息请参阅Go 1.14 版本说明

Go 1.13

详细信息请参阅Go 1.13 版本说明

目录

对于刚开始使用模块的人来说,“快速入门”和“新概念”部分尤为重要。“操作方法…”部分涵盖了更多机制细节。本页的大部分内容都在回答更具体问题的常见问题解答(FAQ)中;至少浏览一下此处列出的常见问题解答单行摘要也是值得的。

快速入门

示例

详细信息在本页的其余部分中介绍,但这里提供一个从头开始创建模块的简单示例。

在 GOPATH 之外创建一个目录,并选择初始化 VCS

$ mkdir -p /tmp/scratchpad/repo
$ cd /tmp/scratchpad/repo
$ git init -q
$ git remote add origin https://github.com/my/repo

初始化一个新模块

$ go mod init github.com/my/repo

go: creating new go.mod: module github.com/my/repo

编写代码

$ cat <<EOF > hello.go
package main

import (
    "fmt"
    "rsc.io/quote"
)

func main() {
    fmt.Println(quote.Hello())
}
EOF

构建并运行

$ go mod tidy
go: finding module for package rsc.io/quote
go: found rsc.io/quote in rsc.io/quote v1.5.2
$ go build -o hello
$ ./hello
Hello, world.

go.mod 文件已更新,以包含您的依赖项的明确版本,其中此处的 v1.5.2 是一个 semver 标签

$ cat go.mod
module github.com/my/repo

go 1.16

require rsc.io/quote v1.5.2

日常工作流程

在 1.16 之前,在运行 go build -o hello 之前不需要 go getgo mod tidy。在 1.16 中,默认禁用了对 go.modgo.sum 文件的隐式修改。

您典型的日常工作流程可以是

您可能使用的其他常见功能的简要介绍

阅读完接下来的关于“新概念”的四个部分后,您将获得足够的知识来开始在大多数项目中使用模块。回顾上面的目录(包括其中的常见问题解答单行摘要)以熟悉更详细的主题列表也很有用。

新概念

这些部分提供了主要新概念的高级介绍。有关更多详细信息和原理,请参阅 Russ Cox 介绍设计理念的 40 分钟介绍视频官方提案文档,或更详细的最初 vgo 博客系列

模块

模块(module)是相关 Go 包的集合,它们作为一个单元进行版本控制。

模块记录精确的依赖项要求并创建可复现的构建。

通常情况下,版本控制仓库仅在仓库根目录中定义一个模块。(单个仓库支持多个模块,但通常这会比每个仓库一个模块带来更多持续工作)。

总结仓库、模块和包之间的关系

模块必须根据semver进行语义版本控制,通常采用 v(major).(minor).(patch) 的形式,例如 v0.1.0v1.2.3v1.5.0-rc.1。前导的 v 是必需的。如果使用 Git,请使用版本标记已发布的提交。公共和私有模块仓库和代理正在变得可用(参见下文常见问题解答 below)。

go.mod

模块由 Go 源文件树和树的根目录中的 go.mod 文件定义。模块源代码可以位于 GOPATH 之外。有四个指令:modulerequirereplaceexclude

以下是一个定义模块 github.com/my/thinggo.mod 文件示例

module github.com/my/thing

require (
    github.com/some/dependency v1.2.3
    github.com/another/dependency/v4 v4.0.0
)

模块通过 go.mod 文件中的 module 指令声明其身份,该指令提供*模块路径*。模块中所有包的导入路径共享该模块路径作为共同前缀。模块路径和从 go.mod 到包目录的相对路径共同确定一个包的导入路径。

例如,如果您正在为仓库 github.com/user/mymod 创建一个模块,该仓库将包含导入路径分别为 github.com/user/mymod/foogithub.com/user/mymod/bar 的两个包,那么您的 go.mod 文件中的第一行通常会将您的模块路径声明为 module github.com/user/mymod,并且相应的磁盘结构可能是

mymod
|-- bar
|   `-- bar.go
|-- foo
|   `-- foo.go
`-- go.mod

在 Go 源代码中,包是使用包括模块路径在内的完整路径导入的。例如,如果在上面的示例中,我们在 go.mod 中将模块身份声明为 module github.com/user/mymod,使用者可以这样做

import "github.com/user/mymod/bar"

这将从模块 github.com/user/mymod 导入包 bar

excludereplace 指令仅作用于当前(“主”)模块。构建主模块时,主模块之外的模块中的 excludereplace 指令将被忽略。因此,replaceexclude 语句允许主模块完全控制其自身的构建,而无需完全受制于依赖项的控制。(关于何时使用 replace 指令的讨论,请参阅下文常见问题解答 below)。

版本选择

如果您在源代码中添加了新的导入,但 go.mod 中尚未包含相应的 require,则像 ‘go build’ 和 ‘go test’ 这样的大多数 go 命令会自动查找适当的模块,并将该新直接依赖项的*最高*版本作为 require 指令添加到您的模块的 go.mod 中。例如,如果您的新导入对应于依赖项 M,其最新标记的发布版本是 v1.2.3,那么您的模块的 go.mod 最终将包含 require M v1.2.3,这表明模块 M 是一个依赖项,允许的版本 >= v1.2.3(并且 < v2,因为 v2 被认为与 v1 不兼容)。

*最小版本选择*算法用于选择构建中使用的所有模块的版本。对于构建中的每个模块,最小版本选择所选择的版本始终是主模块或其某个依赖项中的 require 指令明确列出的版本中语义上的*最高*版本。

例如,如果您的模块依赖于包含 require D v1.0.0 的模块 A,并且您的模块还依赖于包含 require D v1.1.1 的模块 B,那么最小版本选择将选择 D 的 v1.1.1 版本包含在构建中(因为它是在 require 中列出的最高版本)。即使稍后出现了 D 的 v1.2.0 版本,此 v1.1.1 的选择仍保持一致。这是模块系统如何提供 100% 可复现构建的一个示例。准备就绪时,模块作者或用户可以选择升级到 D 的最新可用版本或选择 D 的明确版本。

关于最小版本选择算法的简要原理和概述,请参阅官方提案的“高保真构建”部分,或参阅更详细的 vgo 博客系列

要查看选定的模块版本列表(包括间接依赖项),请使用 go list -m all

另请参阅下文的“如何升级和降级依赖项”部分以及下文的“版本如何被标记为不兼容?”常见问题解答。

语义导入版本控制

多年来,官方 Go 常见问题解答中包含了关于包版本控制的以下建议

“旨在公共使用的包应尽量在演进过程中保持向后兼容性。Go 1 兼容性指南是很好的参考:不要删除导出的名称,鼓励使用标记复合字面量等。如果需要不同的功能,添加一个新名称而不是更改旧名称。如果需要完全中断兼容性,则创建一个具有新导入路径的新包。”

最后一句尤为重要——如果您破坏兼容性,您应该更改包的导入路径。通过 Go 1.11 模块,这一建议被正式化为*导入兼容性规则*

“如果一个旧包和一个新包具有相同的导入路径,则新包必须与旧包向后兼容。”

回想一下,semver 要求当 v1 或更高版本的包进行向后不兼容的更改时,需要更改主要版本。遵循导入兼容性规则和 semver 的结果称为*语义导入版本控制*(Semantic Import Versioning),其中主要版本包含在导入路径中——这确保了只要主要版本由于兼容性破坏而增加,导入路径就会改变。

作为语义导入版本控制的结果,选择使用 Go 模块的代码必须遵守以下规则

通常,具有不同导入路径的包是不同的包。例如,math/randcrypto/rand 是不同的包。如果不同的导入路径是由于导入路径中出现不同的主要版本,这也是成立的。因此,example.com/my/mod/mypkgexample.com/my/mod/v2/mypkg 是不同的包,两者可以在单个构建中同时导入,这除了其他好处外,还有助于解决菱形依赖问题,并允许 v1 模块根据其 v2 替换版本实现,反之亦然。

更多关于语义导入版本控制的详细信息,请参阅 go 命令文档的“模块兼容性和语义版本控制”部分,更多关于语义版本控制的信息请参阅 https://semver.org

到目前为止,本节重点介绍了已选择使用模块并导入其他模块的代码。然而,在 v2+ 模块的导入路径中包含主要版本可能会与旧版本的 Go 或尚未选择使用模块的代码产生不兼容性。为了帮助解决这个问题,对于上述行为和规则,有三个重要的过渡性特殊情况或例外。随着时间推移,随着更多包选择使用模块,这些过渡性例外将变得不那么重要。

三个过渡性例外

  1. gopkg.in

    使用以 gopkg.in 开头(例如 gopkg.in/yaml.v1gopkg.in/yaml.v2)的导入路径的现有代码,即使选择使用模块后,仍可继续使用这些形式作为其模块路径和导入路径。

  2. 导入非模块 v2+ 包时的 ‘+incompatible’

    模块可以导入尚未选择使用模块本身的 v2+ 包。具有有效的 v2+ semver 标签的非模块 v2+ 包将在导入模块的 go.mod 文件中记录带有 +incompatible 后缀。+incompatible 后缀表示即使 v2+ 包具有有效的 v2+ semver 标签(例如 v2.0.0),该 v2+ 包也未主动选择使用模块,因此假定该 v2+ 包在创建时理解语义导入版本控制的影响以及如何在导入路径中使用主要版本。因此,在模块模式下运行时,go 工具将非模块 v2+ 包视为该包 v1 版本系列的(不兼容)扩展,并假定该包不了解语义导入版本控制,而 +incompatible 后缀表示 go 工具正在这样做。

  3. 未启用模块模式时的“最小模块兼容性”

    为了帮助实现向后兼容性,Go 1.9.7+、1.10.3+ 和 1.11 版本已更新,以便使用这些版本构建的代码更容易地正常消费 v2+ 模块,而无需修改现有代码。此行为称为“最小模块兼容性”,仅在 go 工具的完整模块模式被禁用时生效,例如在 Go 1.11 中设置 GO111MODULE=off,或使用 Go 1.9.7+ 或 1.10.3+ 版本时。在 Go 1.9.7+、1.10.3+ 和 1.11 中依赖此“最小模块兼容性”机制时,尚未选择使用模块的包在导入任何 v2+ 模块时将在导入路径中包含主要版本。相比之下,已选择使用模块的包在导入任何 v2+ 模块时必须在导入路径中包含主要版本(以便在 go 工具以完全了解语义导入版本控制的完整模块模式运行时正确导入 v2+ 模块)。

关于发布 v2+ 模块所需的精确机制,请参阅下文的“发布模块(v2 或更高版本)”部分

如何使用模块

如何安装和激活模块支持

要使用模块,有两种安装选项

安装后,您可以通过以下两种方式之一激活模块支持

如何定义模块

为现有项目创建 go.mod 文件

  1. 导航到模块源代码树的根目录,位于 GOPATH 之外

    $ cd <project path outside $GOPATH/src>         # e.g., cd ~/projects/hello
    

    注意,在 GOPATH 之外,您无需设置 GO111MODULE 来激活模块模式。

    或者,如果您想在您的 GOPATH 中工作

    $ export GO111MODULE=on                         # manually active module mode
    $ cd $GOPATH/src/<project path>                 # e.g., cd $GOPATH/src/you/hello
    
  2. 创建初始模块定义并将其写入 go.mod 文件

    $ go mod init
    

    go mod init 通常能够使用辅助数据(例如 VCS 元数据)自动确定适当的模块路径,但如果 go mod init 说明它无法自动确定模块路径,或者您需要以其他方式覆盖该路径,您可以将模块路径作为可选参数提供给 go mod init,例如

    $ go mod init github.com/my/repo
    

    请注意,如果您的依赖项包含 v2+ 模块,或者您正在初始化 v2+ 模块,那么在运行 go mod init 后,您可能还需要编辑您的 go.mod.go 代码,按照上面的“语义导入版本控制”部分所述,在导入路径和模块路径中添加 /vN。即使 go mod init 自动从 dep 或其他依赖管理器转换了您的依赖信息,这也适用。(因此,在运行 go mod init 后,您通常不应运行 go mod tidy,直到您成功运行了 go build ./... 或类似的命令,这正是本节中显示的顺序)。

如果与 Go 1.21.13 或更旧版本一起使用,此步骤还会从任何现有的 depGopkg.lock 文件或任何其他总共九种支持的依赖格式进行转换,添加 require 语句以匹配现有配置。

  1. 构建模块。当从模块的根目录执行时,./... 模式匹配当前模块内的所有包。go build 将根据需要自动添加缺失或未转换的依赖项,以满足此特定构建调用中的导入需求

    $ go build ./...
    
  2. 测试配置好的模块,确保它与选定的版本配合工作

    $ go test ./...
    
  3. (可选)运行您的模块测试以及所有直接和间接依赖项的测试,以检查是否存在不兼容性

    $ go test all
    

在标记发布版本之前,请参阅下文的“如何准备发布”部分

有关所有这些主题的更多信息,官方模块文档的主要入口点可在 golang.org 上找到

如何升级和降级依赖项

日常升级和降级依赖项应使用 ‘go get’ 命令完成,它会自动更新 go.mod 文件。或者,您也可以直接编辑 go.mod 文件。

此外,像 ‘go build’、‘go test’ 甚至 ‘go list’ 这样的 go 命令将根据需要自动添加新的依赖项以满足导入(更新 go.mod 并下载新的依赖项)。

将依赖项升级到最新版本

go get example.com/package

将依赖项及其所有依赖项升级到最新版本

go get -u example.com/package

查看所有直接和间接依赖项的可用次要版本和补丁版本升级

go list -u -m all

查看直接依赖项的可用次要版本和补丁版本升级,运行

go list -u -f '{{if (and (not (or .Main .Indirect)) .Update)}}{{.Path}}: {{.Version}} -> {{.Update.Version}}{{end}}' -m all 2> /dev/null

要将当前模块的所有直接和间接依赖项升级到最新版本,可以从模块根目录运行以下命令

go get foo 更新到 foo 的最新版本。go get foo 等效于 go get foo@latest — 换句话说,如果未指定 @ 版本,则默认使用 @latest

在本节中,“latest”是指带有 semver 标签的最新版本,如果没有任何 semver 标签,则指最新的已知提交。预发布标签不会被选为“latest”,除非仓库上没有其他 semver 标签(详细信息)。

一个常见错误是认为 go get -u foo 仅获取 foo 的最新版本。实际上,go get -u foogo get -u foo@latest 中的 -u 意味着同时获取 foo所有直接和间接依赖项的最新版本。升级 foo 时一个常见的起点是改为运行不带 -ugo get foogo get foo@latest(在一切正常后,再考虑 go get -u=patch foogo get -u=patchgo get -u foogo get -u)。

要升级或降级到更具体的版本,go get 允许通过向包参数添加 @版本 后缀或“模块查询”来覆盖版本选择,例如 go get foo@v1.6.2go get foo@e3702bed2go get foo@'<v1.6.2'

使用分支名称(例如 go get foo@master,Mercurial 中为 foo@default)是一种获取最新提交的方法,无论它是否具有 semver 标签。

通常,无法解析为 semver 标签的模块查询将被记录为 go.mod 文件中的伪版本 (pseudo-versions)

有关此处主题的更多信息,请参阅 go 命令文档中的“模块感知的 go get”“模块查询”部分。

模块能够消费尚未选择加入模块的包,包括将任何可用的 semver 标签记录在 go.mod 中,并使用这些 semver 标签来升级或降级。模块也可以消费尚未拥有任何合适 semver 标签的包(在这种情况下,它们将使用 go.mod 中的伪版本进行记录)。

升级或降级任何依赖项后,您可能希望再次运行构建中所有包的测试(包括直接和间接依赖项),以检查兼容性问题

$ go test all

如何准备发布

发布模块 (所有版本)

模块发布的最佳实践预计将在最初的模块实验中出现。其中许多最终可能会通过一个未来的 'go release' 工具来自动化。

一些当前建议在标记发布前考虑的最佳实践

发布模块 (v2 或更高版本)

如果您要发布 v2 或更高版本的模块,请先回顾上面“语义导入版本控制”部分中的讨论,其中解释了为什么主版本包含在 v2+ 模块的模块路径和导入路径中,以及 Go 1.9.7+ 和 1.10.3+ 版本如何更新以简化这一转变。

请注意,如果您是首次为一个在采用模块之前已经标记为 v2.0.0 或更高版本的现有仓库或包集合采用模块,那么推荐的最佳实践是在首次采用模块时增加主版本。例如,如果您是 foo 的作者,且 foo 仓库的最新标签是 v2.2.2,并且 foo 尚未采用模块,那么最佳实践是为 foo 首次采用模块的发布(也因此是 foo 首次包含 go.mod 文件的发布)使用 v3.0.0。在这种情况下增加主版本为 foo 的消费者提供了更大的清晰度,如果需要,允许在 foo 的 v2 系列上进行额外的非模块补丁或次要版本发布,并为基于模块的 foo 消费者提供了强烈的信号,表明如果执行 import "foo" 以及相应的 require foo v2.2.2+incompatible,与执行 import "foo/v3" 以及相应的 require foo/v3 v3.0.0 相比,会得到不同的主版本。(请注意,这条关于首次采用模块时增加主版本的建议适用于其最新版本为 v0.x.x 或 v1.x.x 的现有仓库或包)。

发布 v2 或更高版本模块有两种替代机制。请注意,通过这两种技术,当模块作者推送新标签时,新的模块发布即可供消费者使用。以创建 v3.0.0 发布为例,两个选项是

  1. 主分支方法 (Major branch):更新 go.mod 文件,在 module 指令的模块路径末尾包含 /v3 (例如,module github.com/my/module/v3)。更新模块内的导入语句,使其也使用 /v3 (例如,import "github.com/my/module/v3/mypkg")。使用 v3.0.0 标记发布。

    • Go 1.9.7+、1.10.3+ 和 1.11 版本能够正确消费和构建使用此方法创建的 v2+ 模块,而无需更新尚未选择加入模块的消费者代码(如上面“语义导入版本控制”部分所述)。
    • 社区工具 github.com/marwan-at-work/mod 有助于自动化此过程。有关概述,请参阅该仓库或下方的社区工具常见问题解答
    • 为避免与此方法混淆,考虑将模块的 v3.*.* 提交放在单独的 v3 分支上。
    • 注意:创建新分支不是必需的。如果您之前一直在 master 分支上发布,并且更希望在 master 上标记 v3.0.0,这是一个可行的选项。(但是,请注意,在 master 中引入不兼容的 API 更改可能会对非模块用户在使用 go get -u 时造成问题,因为在 Go 1.11 之前,go 工具不了解 semver,或者在 Go 1.11+ 中模块模式未启用时)。
    • 现有的依赖管理解决方案,例如 dep,目前在消费以此方式创建的 v2+ 模块时可能遇到问题。例如参阅 dep#1962
  2. 主子目录方法 (Major subdirectory):创建一个新的 v3 子目录(例如,my/module/v3),并将新的 go.mod 文件放在该子目录中。模块路径必须以 /v3 结尾。将代码复制或移动到 v3 子目录中。更新模块内的导入语句,使其也使用 /v3(例如,import "github.com/my/module/v3/mypkg")。使用 v3.0.0 标记发布。

    • 这提供了更好的向后兼容性。特别是,早于 Go 1.9.7 和 1.10.3 的版本也能正确消费和构建使用此方法创建的 v2+ 模块。
    • 这里一个更复杂的方法可以利用类型别名(在 Go 1.9 中引入)以及位于不同子目录中的主版本之间的转发 shim。这可以提供额外的兼容性,并允许一个主版本基于另一个主版本实现,但这会给模块作者带来更多工作。一个正在开发中的自动化工具是 goforward。有关更多详细信息和理由,请参阅此处,以及一个可用的 goforward 初始版本。
    • 现有的依赖管理解决方案,例如 dep,应该能够消费以此方式创建的 v2+ 模块。

有关这些替代方法的更深入讨论,请参阅 https://research.swtch.com/vgo-module

发布版本

新模块版本可以通过向包含模块源代码的仓库推送标签来发布。标签由两个字符串连接而成:一个前缀和一个版本

版本是发布的语义导入版本。应遵循语义导入版本控制的规则来选择它。

前缀表示模块在仓库中的定义位置。如果模块在仓库根目录下定义,前缀为空,标签就是版本本身。然而,在多模块仓库中,前缀用于区分不同模块的版本。前缀是模块在仓库内部定义的目录。如果仓库遵循上述主子目录模式,前缀不包含主版本后缀。

例如,假设我们有一个模块 example.com/repo/sub/v2,并且我们想发布版本 v2.1.6。仓库根目录对应于 example.com/repo,模块在仓库内的 sub/v2/go.mod 中定义。该模块的前缀是 sub/。此发布的完整标签应为 sub/v2.1.6

迁移到模块

本节尝试简要列出迁移到模块时需要做出的主要决定,以及列出其他与迁移相关的主题。通常会提供对其他部分的引用以获取更多详细信息。

本材料主要基于社区在模块实验中出现的最佳实践;因此,这是一个正在进行中的部分,将随着社区经验的增加而改进。

总结

迁移主题

从现有依赖管理器自动迁移

向旧版本 Go 和非模块消费者提供依赖信息

更新现有安装说明

避免破坏现有导入路径

模块通过 go.mod 中的 module 指令声明其身份,例如 module github.com/my/module。模块内的所有包必须由任何模块感知的消费者使用与模块声明的模块路径匹配的导入路径进行导入(根包必须完全匹配,或者模块路径作为导入路径的前缀)。如果导入路径与相应模块声明的模块路径不匹配,go 命令会报告 unexpected module path 错误。

为一个现有的包集合采用模块时,应注意避免破坏现有消费者使用的现有导入路径,除非您在采用模块时增加主版本。

例如,如果您的现有 README 一直告诉消费者使用 import "gopkg.in/foo.v1",而您随后以 v1 发布采用模块,您的初始 go.mod 几乎肯定应该包含 module gopkg.in/foo.v1。如果您想不再使用 gopkg.in,这会是您当前消费者的一个破坏性更改。一种方法是,如果您稍后迁移到 v2,则更改为类似 module github.com/repo/foo/v2 的内容。

请注意,模块路径和导入路径是区分大小写的。例如,将模块从 github.com/Sirupsen/logrus 更改为 github.com/sirupsen/logrus 对消费者来说是破坏性更改,即使 GitHub 会自动将一个仓库名称转发到新的仓库名称。

在您采用模块后,更改 go.mod 中的模块路径是破坏性更改。

总的来说,这类似于在模块出现之前通过“导入路径注释”强制执行规范导入路径,这些注释有时也称为“导入 pragma”或“导入路径强制执行”。例如,包 go.uber.org/zap 目前托管在 github.com/uber-go/zap,但使用包声明旁边的导入路径注释,该注释会触发使用错误基于 GitHub 的导入路径的任何非模块消费者的错误。

package zap // import "go.uber.org/zap"

导入路径注释已被 go.mod 文件的 module 语句废弃。

首次采用模块并使用 v2+ 包时增加主版本

v2+ 模块允许在单个构建中存在多个主版本

模块消费非模块代码

非模块代码消费模块

现有 v2+ 包作者的策略

对于正在考虑选择加入模块的现有 v2+ 包的作者,总结替代方法的一种方式是选择以下三种顶级策略之一。每种选择都有后续的决定和变体(如上所述)。这些替代的顶级策略是

  1. 要求客户端使用 Go 1.9.7+、1.10.3+ 或 1.11+ 版本.

    该方法使用“主分支”方法,并依赖于回port到 1.9.7 和 1.10.3 的“最小模块感知”。有关更多详细信息,请参阅上面的“语义导入版本控制”“发布模块 (v2 或更高版本)”部分。

  2. 允许客户端使用更旧的 Go 版本,例如 Go 1.8.

    该方法使用“主子目录”方法,并涉及创建子目录,例如 /v2/v3。有关更多详细信息,请参阅上面的“语义导入版本控制”“发布模块 (v2 或更高版本)”部分。

  3. 暂缓选择加入模块.

    在此策略中,与已选择加入模块的客户端代码以及尚未选择加入模块的客户端代码都可以继续工作。随着时间的推移,Go 1.9.7+、1.10.3+ 和 1.11+ 版本将发布越来越长的时间,并且在未来的某个时候,要求 Go 1.9.7+/1.10.3+/1.11+ 版本将变得更加自然或对客户端更友好,届时您可以实施上面的策略 1(要求 Go 1.9.7+、1.10.3+ 或 1.11+ 版本),甚至上面的策略 2(尽管如果您最终为了支持像 Go 1.8 这样的旧版本而选择上面的策略 2,那么您现在就可以这样做)。

附加资源

文档和提案

入门资料

补充资料

自最初 Vgo 提案以来的更改

作为提案、原型和 Beta 测试过程的一部分,整个社区已经创建了 400 多个问题。请继续提供反馈意见。

以下是一些主要更改和改进的部分列表,其中几乎所有都主要基于社区的反馈意见

GitHub 问题

常见问题解答 (FAQ)

版本如何被标记为不兼容?

require 指令允许任何模块声明它应该使用依赖项 D 的 >= x.y.z 版本进行构建(这可能是由于与模块 D 的 < x.y.z 版本不兼容而指定的)。经验数据表明这是 depcargo 中使用的主要约束形式。此外,构建中的顶级模块可以 exclude 特定版本的依赖项或使用不同代码 replace 其他模块。有关更多详细信息和理由,请参阅完整提案此处

带版本控制的模块提案的关键目标之一是为工具和开发者添加关于 Go 代码版本的通用词汇和语义。这为将来声明其他形式的不兼容性奠定了基础,例如可能

我何时会获得旧行为与基于模块的新行为?

总的来说,在 Go 1.11 中,模块是可选加入的,因此根据设计,旧行为默认得到保留。

总结何时会获得旧的 1.10 现状行为与新的可选加入的基于模块的行为

为什么通过 go get 安装工具会失败并出现错误 cannot find main module

当您设置了 GO111MODULE=on,但在运行 go get 时,您不在包含 go.mod 的文件树中时,就会发生这种情况。

最简单的解决方案是保留 GO111MODULE 未设置(或显式设置为 GO111MODULE=auto),这可以避免此错误。

回想一下,模块存在的主要原因之一是记录精确的依赖信息。此依赖信息会写入您当前的 go.mod 文件。如果您不在包含 go.mod 的文件树中,但您通过设置 GO111MODULE=on 告诉 go get 命令以模块模式运行,那么运行 go get 将导致错误 cannot find main module,因为没有可用的 go.mod 来记录依赖信息。

替代解决方案包括

  1. 保留 GO111MODULE 未设置(默认设置,或显式设置 GO111MODULE=auto),这将带来更友好的行为。当您不在模块中时,这将使您获得 Go 1.10 的行为,从而避免 go get 报告 cannot find main module

  2. 保留 GO111MODULE=on,但根据需要临时禁用模块并在 go get 期间启用 Go 1.10 行为,例如通过 GO111MODULE=off go get example.com/cmd。这可以转换为简单的脚本或 shell 别名,例如 alias oldget='GO111MODULE=off go get'

  3. 创建一个临时 go.mod 文件,然后丢弃。这已经由 一个简单的 shell 脚本@rogpeppe 自动化完成。该脚本允许通过 vgoget example.com/cmd[@version] 选择性地提供版本信息。(这可以解决避免错误 cannot use path@version syntax in GOPATH mode 的问题)。

  4. gobin 是一个支持模块的命令,用于安装和运行 main 包。默认情况下,gobin 安装/运行 main 包时不需要先手动创建模块,但使用 -m 标志可以告诉它使用现有模块来解析依赖关系。有关详细信息和其他用例,请参阅 gobinREADMEFAQ

  5. 创建一个 go.mod,您用它来跟踪您全局安装的工具,例如在 ~/global-tools/go.mod 中,并在运行 go getgo install 安装任何全局工具之前 cd 到该目录。

  6. 在单独的目录中为每个工具创建一个 go.mod,例如 ~/tools/gorename/go.mod~/tools/goimports/go.mod,并在运行 go getgo install 安装该工具之前 cd 到相应的目录。

当前的这一限制将被解决。然而,主要问题是模块目前是可选加入的,而一个完整的解决方案可能需要等到 GO111MODULE=on 成为默认行为时才能实现。请参阅 #24250 以获取更多讨论,包括此评论

这最终显然必须能够工作。我不确定的是,就版本而言,这具体做了什么:它是否创建一个临时的模块根和 go.mod,执行安装,然后丢弃它?可能。但我不太确定,目前,我不想通过让 vgo 在 go.mod 树之外执行操作来混淆人们。当然,最终的 go 命令集成必须支持这一点。

此常见问题解答一直在讨论跟踪全局安装的工具。

如果您想跟踪特定模块所需的工具,请参阅下一个常见问题解答。

如何跟踪模块的工具依赖项?

如果您

那么在 Go 1.24 及更高版本中,您可以将 tool 指令添加到您的 go.mod 中

go 1.24

...

tool golang.org/x/tools/cmd/stringer

在 Go 1.24 之前,推荐的方法是向模块添加一个 tools.go 文件,其中包含感兴趣的工具的 import 语句(例如 import _ "golang.org/x/tools/cmd/stringer"),以及一个 //go:build tools 构建约束。import 语句允许 go 命令在模块的 go.mod 中精确记录工具的版本信息,而 //go:build tools 构建约束阻止您的正常构建实际导入您的工具。

有关如何执行此操作的具体示例,请参阅 “Go Modules by Example”演练

您还可以(自 Go 1.16 起)使用 go install tool@version 安装特定版本,或者(自 Go 1.17 起)使用 go run tool@version 运行工具而无需安装,这已在 #42088#40276 中实现,这可以消除对 tools.go 的需求。

模块支持在 IDE、编辑器和 goimports、gorename 等标准工具中的状态如何?

对模块的支持正在进入编辑器和 IDE。

例如

goimports、guru、gorename 等其他工具的状态正在一个总括问题 #24661 中跟踪。请参阅该总括问题以获取最新状态。

一些特定工具的跟踪问题包括

通常,即使您的编辑器、IDE 或其他工具尚未支持模块,如果您在 GOPATH 中使用模块并执行 go mod vendor,它们的大部分功能也应该能与模块一起工作(因为这样应该会通过 GOPATH 拾取正确的依赖关系)。

完整的修复是将加载包的程序从 go/build 转移到 golang.org/x/tools/go/packages,后者知道如何以模块感知的方式定位包。这最终可能会成为 go/packages

常见问题解答 — 附加控制

有哪些社区工具可用于使用模块?

社区正在开始在模块之上构建工具。例如

我应该何时使用 replace 指令?

如上文 “go.mod”概念部分所述,replace 指令在顶层 go.mod 中提供了额外的控制,用于确定实际使用什么来满足 Go 源代码或 go.mod 文件中找到的依赖项,而主模块之外的模块中的 replace 指令在构建主模块时将被忽略。

replace 指令允许您提供另一个导入路径,该路径可能是位于 VCS(GitHub 或其他地方)的另一个模块,或者是位于本地文件系统上的相对或绝对文件路径。使用 replace 指令中的新导入路径,而无需更新实际源代码中的导入路径。

replace 允许顶层模块控制依赖项使用的确切版本,例如

replace 也允许使用分叉的依赖项,例如

您也可以引用分支,例如

一个示例用例是,如果您需要在依赖项中修复或调查某些内容,您可以有一个本地分叉,并在顶层 go.mod 中添加如下内容

replace 还可以用于告知 go 工具关于多模块项目中模块的相对或绝对磁盘位置,例如

注意:如果 replace 指令的右侧是文件系统路径,那么目标位置必须有一个 go.mod 文件。如果 go.mod 文件不存在,您可以使用 go mod init 创建一个。

通常,您可以在 replace 指令的 => 左侧指定版本,但如果您省略它,通常更不容易发生变化(例如,如上面所有 replace 示例所示)。

直接依赖项的每个 replace 指令都需要一个 require 指令。当用文件系统路径替换依赖项时,对应 require 指令的版本实际上被忽略了;在这种情况下,伪版本 v0.0.0 是一个很好的选择,可以清楚地表明这一点,例如 require example.com/module v0.0.0

您可以通过运行 go list -m all 来确认您正在获取预期版本,该命令显示了构建中将使用的实际最终版本,包括考虑了 replace 语句。

有关更多详细信息,请参阅 “go mod edit”文档

github.com/rogpeppe/gohack 使这些类型的工作流程变得更容易,特别是如果您的目标是对模块的依赖项进行可变检出。有关概述,请参阅 仓库 或紧邻的上一个常见问题解答。

请参阅下一个常见问题解答,了解使用 replace 完全在 VCS 之外工作的详细信息。

我可以在本地文件系统上完全在 VCS 之外工作吗?

是的。不需要 VCS。

如果您想一次在 VCS 之外编辑一个模块(并且您总共只有一个模块,或者其他模块驻留在 VCS 中),这非常简单。在这种情况下,您可以将包含单个 go.mod 的文件树放在方便的位置。即使您的单个模块在 VCS 之外,您的 go buildgo test 和类似命令也将起作用(无需在 go.mod 中使用任何 replace)。

如果您想在本地磁盘上同时编辑多个相互关联的模块,那么 replace 指令是一种方法。这是一个示例 go.mod,它使用带有相对路径的 replacehello 模块指向磁盘上 goodbye 模块的位置(不依赖于任何 VCS)

module example.com/me/hello

require (
  example.com/me/goodbye v0.0.0
)

replace example.com/me/goodbye => ../goodbye

此线程 中显示了一个小的可运行示例。

如何将 vendoring 与模块一起使用?vendoring 会消失吗?

最初的 vgo 博客系列确实提议完全放弃 Vendoring,但社区的 反馈 导致保留了对 Vendoring 的支持。

简而言之,要将 Vendoring 与模块一起使用

Go 的旧版本(如 1.10)知道如何使用由 go mod vendor 创建的 vendor 目录,Go 1.11 和 1.12+ 在 模块模式 被禁用时也是如此。因此,Vendoring 是一种模块向不完全理解模块的 Go 旧版本以及尚未启用模块的消费者提供依赖项的方式。

如果您正在考虑使用 Vendoring,阅读 tip 文档的 “Modules and vendoring”“Make vendored copy of dependencies” 部分是值得的。

是否有“始终在线”的模块仓库和企业代理?

公共托管的“始终在线”不可变模块仓库以及可选的私有托管代理和仓库正在变得可用。

例如

请注意,您不需要运行代理。相反,Go 1.11 中的 go 工具通过 GOPROXY 添加了可选的代理支持,以实现更多企业用例(例如更大的控制),并更好地处理诸如“GitHub 已关闭”或人们删除 GitHub 仓库之类的情况。

我可以控制 go.mod 何时更新以及 go 工具何时使用网络来满足依赖项吗?

默认情况下,像 go build 这样的命令会根据需要连接到网络来满足导入。

有些团队希望在某些时候禁止 go 工具访问网络,或者希望对 go 工具何时更新 go.mod、如何获取依赖项以及如何使用 Vendoring 有更大的控制权。

go 工具提供了相当大的灵活性来调整或禁用这些默认行为,包括通过 -mod=readonly-mod=vendorGOFLAGSGOPROXY=offGOPROXY=file:///filesystem/pathgo mod vendorgo mod download

这些选项的详细信息分散在官方文档中。社区尝试将这些行为相关的控制选项汇总到 此处,其中包含指向官方文档的链接以获取更多信息。

如何在 Travis 或 CircleCI 等 CI 系统中使用模块?

最简单的方法可能是只设置环境变量 GO111MODULE=on,这应该适用于大多数 CI 系统。

然而,考虑到您的一些用户尚未选择加入模块,在 CI 中同时启用和禁用模块的 Go 1.11 上运行测试可能很有价值。Vendoring 也是一个需要考虑的话题。

以下两篇博客文章更具体地介绍了这些主题

如何下载构建特定包或测试所需的模块?

go mod download 命令(或等效地,go mod download all)下载构建列表中的所有模块(如 go list -m all 报告)。许多这些模块构建主模块中的包是不需要的,因为完整的构建列表包含其他模块的测试依赖项和工具依赖项。因此,使用 go mod download 准备的 Docker 镜像可能比必要的要大。

相反,考虑使用 go list。例如,go list ./... 将下载构建包 ./... 所需的模块(当从模块根目录运行时,主模块中的包集)。

要同时下载测试依赖项,请使用 go list -test ./...

默认情况下,go list 只考虑当前平台所需的依赖项。您可以设置 GOOSGOARCH 来使 go list 考虑另一个平台,例如,GOOS=linux GOARCH=amd64 go list ./...-tags 标志也可以用于选择具有特定构建标签的包。

将来实现延迟模块加载时(参见 #36460),这种技术可能会变得不那么必要,因为模块模式 all 将包含更少的模块。

常见问题解答 — go.mod 和 go.sum

为什么 ‘go mod tidy’ 会在我的 ‘go.mod’ 中记录间接和测试依赖项?

模块系统在您的 go.mod 中记录精确的依赖需求。(有关更多详细信息,请参阅上面的 go.mod 概念 部分或 go.mod tip 文档)。

go mod tidy 更新您当前的 go.mod,以包含模块中测试所需的依赖项——如果测试失败,我们必须知道使用了哪些依赖项才能重现失败。

go mod tidy 还确保您当前的 go.mod 反映所有可能的操作系统、架构和构建标签组合的依赖需求(如 此处 所述)。相比之下,go buildgo test 等其他命令只更新 go.mod 以提供当前 GOOSGOARCH 和构建标签下请求的包导入的包(这也是 go mod tidy 可能会添加 go build 或类似命令未添加的要求的原因之一)。

如果您的模块的依赖项本身没有 go.mod(例如,因为该依赖项尚未选择加入模块),或者如果其 go.mod 文件缺少一个或多个依赖项(例如,因为模块作者未运行 go mod tidy),那么缺少的传递依赖项将被添加到您的模块需求中,并带有 // indirect 注释,表明该依赖项不是来自您模块中的直接导入。

请注意,这也意味着您的直接或间接依赖项中任何缺少的测试依赖项也将被记录在您的 go.mod 中。(这种情况重要的一个例子:go test all 运行模块所有直接和间接依赖项的测试,这是验证当前版本组合是否协同工作的一种方式。如果在运行 go test all 时您的某个依赖项中的测试失败,记录一套完整的测试依赖信息非常重要,以便您拥有可重现的 go test all 行为)。

您的 go.mod 文件中可能包含 // indirect 依赖项的另一个原因是,如果您升级(或降级)了您的一个间接依赖项,使其超出了您的直接依赖项所要求的范围,例如,您运行了 go get -ugo get foo@1.2.3。go 工具需要一个地方来记录这些新版本,它会将这些信息记录在您的 go.mod 文件中(并且它不会深入到您的依赖项中修改它们go.mod 文件)。

总的来说,上述行为是模块通过记录精确的依赖信息来提供 100% 可重现构建和测试的方式的一部分。

如果您好奇为什么某个特定的模块出现在您的 go.mod 中,您可以运行 go mod why -m <module>回答 这个问题。检查需求和版本的其他有用工具包括 go mod graphgo list -m all

‘go.sum’ 是一个锁文件吗?为什么 ‘go.sum’ 包含我不再使用的模块版本信息?

不是,go.sum 不是锁文件。构建中的 go.mod 文件提供了足够的信息来实现 100% 可重现的构建。

为了验证目的,go.sum 包含特定模块版本内容的预期加密校验和。有关 go.sum 的更多详细信息(包括为什么通常应该提交 go.sum),请参阅下面的 常见问题解答,以及 tip 文档中的 “Module downloading and verification” 部分。

此外,您的模块的 go.sum 记录了构建中使用的所有直接和间接依赖项的校验和(因此您的 go.sum 文件通常会列出比 go.mod 文件更多的模块)。

我应该提交我的 ‘go.sum’ 文件以及我的 ‘go.mod’ 文件吗?

通常您的模块的 go.sum 文件应该与您的 go.mod 文件一起提交。

如果我没有任何依赖项,我是否仍应添加 ‘go.mod’ 文件?

是的。这支持在 GOPATH 之外工作,有助于向生态系统传达您正在选择加入模块,此外,您 go.mod 文件中的 module 指令是您的代码身份的明确声明(这也是 import comments 最终可能被弃用的原因之一)。当然,在 Go 1.11 中,模块完全是一个选择加入的功能。

常见问题解答 — 语义导入版本控制

为什么主要版本号必须出现在导入路径中?

请参阅上面 “语义导入版本控制” 概念部分关于语义导入版本控制和导入兼容性规则的讨论。另请参阅 宣布提案的博客文章,其中更多地讨论了导入兼容性规则的动机和理由。

为什么导入路径中省略了主要版本 v0、v1?

请参阅早期 官方提案讨论中的常见问题解答 中关于“为什么导入路径中省略了主要版本 v0、v1?”的问题。

将我的项目标记为 v0、v1 主要版本,或在 v2+ 版本中进行破坏性更改,有哪些影响?

为了回应关于“k8s 进行次要版本发布,但每次次要版本发布都更改 Go API”的评论,Russ Cox 发表了以下 回应,其中强调了在您的项目中使用 v0、v1 与经常通过 v2、v3、v4 等进行破坏性更改的一些影响

我不太完全理解 k8s 的开发周期等,但我认为通常 k8s 团队需要决定/确认他们打算向用户保证的稳定性,然后相应地应用版本号来表达这一点。

  • 要承诺 API 兼容性(这似乎是最佳用户体验!),那么就开始这样做并使用 1.X.Y。
  • 为了灵活地在每次发布中进行向后不兼容的更改,但允许大型程序的各个部分按照不同的时间表升级其代码,这意味着不同的部分可以在一个程序中使用不同主要版本的 API,那么使用 X.Y.0,以及类似 k8s.io/client/vX/foo 的导入路径。
  • 如果不承诺 API 兼容性,并且还要求每个构建无论如何只能有一个 k8s 库的副本,这意味着强制构建的所有部分使用相同的版本,即使并非所有部分都已准备好,那么使用 0.X.Y。

另外,Kubernetes 有一些非典型的构建方法(目前包括在 godep 之上的自定义包装脚本),因此 Kubernetes 对于许多其他项目来说不是一个完美的例子,但随着 Kubernetes 迈向采用 Go 1.11 模块,它可能会成为一个有趣的例子。

模块可以消费一个尚未选择使用模块的包吗?

是的。

如果一个仓库尚未选择加入模块,但已使用有效的 semver 标签(包括必需的引导 v)进行标记,则这些 semver 标签可以在 go get 中使用,并且相应的 semver 版本将记录在导入模块的 go.mod 文件中。如果该仓库没有任何有效的 semver 标签,那么该仓库的版本将记录为 “伪版本”,例如 v0.0.0-20171006230638-a6e239ea1c69(其中包括时间戳和提交哈希,并且旨在允许在 go.mod 中记录的版本之间进行总排序,并使推理哪个记录的版本比另一个记录的版本“晚”更容易)。

例如,如果包 foo 的最新版本标记为 v1.2.3,但 foo 本身尚未选择加入模块,那么在模块 M 中运行 go get foogo get foo@v1.2.3 将记录在模块 M 的 go.mod 文件中为

require  foo  v1.2.3

go 工具还将在其他工作流程中(例如 go list -u=patch,它将模块的依赖项升级到可用的补丁版本,或者 go list -u -m all,它显示可用的升级等)使用非模块包的可用 semver 标签。

请参阅下一个常见问题解答,了解与尚未选择加入模块的 v2+ 包相关的其他详细信息。

模块可以消费一个尚未选择使用模块的 v2+ 包吗?‘+incompatible’ 是什么意思?

是的,模块可以导入一个尚未选择加入模块的 v2+ 包,如果导入的 v2+ 包有一个有效的 semver 标签,它将以 +incompatible 后缀记录。

附加详情

请熟悉上面 “语义导入版本控制” 部分中的内容。

首先回顾一些核心原则会很有帮助,这些原则通常很有用,但在考虑此常见问题解答中描述的行为时尤其重要。

go 工具在模块模式下运行时(例如,GO111MODULE=on),以下核心原则总是成立

  1. 包的导入路径定义了包的身份。
    • 具有不同导入路径的包被视为不同的包。
    • 具有相同导入路径的包被视为相同的包(即使 VCS 标签表示包具有不同的主要版本,这也成立)。
  2. 不带 /vN 的导入路径被视为 v1 或 v0 模块(即使导入的包尚未选择加入模块并且 VCS 标签表示主要版本大于 1,这也成立)。
  3. 模块的 go.mod 文件开头声明的模块路径(例如 module foo/v2)既是
    • 该模块身份的明确声明
    • 消费代码必须如何导入该模块的明确声明

正如我们将在下一个常见问题解答中看到的那样,当 go 工具不在模块模式下时,这些原则并不总是成立,但当 go 工具模块模式下时,这些原则总是成立的。

简而言之,+incompatible 后缀表示当以下情况属实时,上述原则 2 生效

go 工具处于模块模式时,它将假定一个非模块的 v2+ 包没有语义导入版本控制意识,并将其视为该包 v1 版本系列的不兼容扩展(而 +incompatible 后缀表明 go 工具正在这样做)。

示例

假设

在这种情况下,例如在模块 M 中运行 go get oldpackage@latest 将在模块 M 的 go.mod 文件中记录以下内容

require  oldpackage  v3.0.1+incompatible

请注意,上面的 go get 命令或记录的 require 指令中,oldpackage 的末尾没有使用 /v3——在模块路径和导入路径中使用 /vN语义化导入版本控制 的一个特性,而 oldpackage 并未通过在 oldpackage 自身内部拥有 go.mod 文件来表明其接受并理解了语义化导入版本控制。换句话说,尽管 oldpackage 有一个 语义化版本(semver) 标签 v3.0.1,但 oldpackage 并未被授予 语义化导入版本控制 的权利和责任(例如在导入路径中使用 /vN),因为它尚未声明这样做。

+incompatible 后缀表示 oldpackagev3.0.1 版本尚未主动选择加入模块模式,因此假定 oldpackagev3.0.1 版本理解语义化导入版本控制或如何在导入路径中使用主版本。因此,当在 模块模式 下运行时,go 工具会将非模块化的 oldpackage v3.0.1 版本视为 oldpackage v1 版本系列的一个(不兼容的)扩展,并假定 oldpackagev3.0.1 版本对语义化导入版本控制一无所知,而 +incompatible 后缀正是 go 工具这样做的一个标志。

根据语义化导入版本控制,oldpackagev3.0.1 版本被认为是 v1 发布系列的一部分,这意味着例如版本 v1.0.0v2.0.0v3.0.1 都总是使用相同的导入路径进行导入

import  "oldpackage"

再次注意,oldpackage 的末尾没有使用 /v3

通常,具有不同导入路径的包是不同的包。在此示例中,鉴于 oldpackage 的版本 v1.0.0v2.0.0v3.0.1 都将使用相同的导入路径进行导入,因此在构建时它们被视为同一个包(同样是因为 oldpackage 尚未选择加入语义化导入版本控制),并且在任何给定构建中 oldpackage 只会存在一个副本。(使用的版本将是任何 require 指令中列出的语义上最高的版本;请参阅 “版本选择”)。

假设之后创建了 oldpackage 的新版本 v4.0.0,该版本采用了模块模式并因此包含一个 go.mod 文件,这就是一个信号,表明 oldpackage 现在理解了语义化导入版本控制的权利和责任,因此基于模块的消费者现在将使用导入路径中的 /v4 进行导入

import  "oldpackage/v4"

并且该版本将被记录为

require  oldpackage/v4  v4.0.0

oldpackage/v4 现在是一个与 oldpackage 不同的导入路径,因此也是一个不同的包。如果在同一个构建中有一些消费者使用 import "oldpackage/v4",而另一些消费者使用 import "oldpackage",那么在模块感知的构建中将存在两个副本(每个导入路径一个)。这是允许模块逐步采用策略的一部分,是可取的。此外,即使在模块走出当前的过渡阶段之后,这种行为也是可取的,可以允许代码随着时间的推移逐步演进,不同的消费者以不同的速率升级到更新的版本(例如,允许大型构建中的不同消费者选择以不同的速率从 oldpackage/v4 升级到将来的 oldpackage/v5)。

如果未启用模块支持,v2+ 模块在构建中如何处理?“最小模块兼容性”在 1.9.7+、1.10.3+ 和 1.11 中如何工作?

在考虑较旧的 Go 版本或尚未选择加入模块模式的 Go 代码时,语义化导入版本控制对 v2+ 模块具有显著的向后兼容性影响。

正如上面 “语义化导入版本控制” 部分所描述的

然而,预计模块和语义化导入版本控制的生态系统将以不同的速度推进其采用进程。

正如 “如何发布 v2+ 模块” 部分更详细描述的那样,在“主版本子目录”方法中,v2+ 模块的作者创建诸如 mymodule/v2mymodule/v3 之类的子目录,并将相应的包移动或复制到这些子目录下方。这意味着传统的导入路径逻辑(即使在 Go 1.8 或 1.7 等较旧的版本中)在看到诸如 import "mymodule/v2/mypkg" 的导入语句时会找到相应的包。因此,即使模块支持未启用,驻留在“主版本子目录”v2+ 模块中的包也会被找到和使用(无论是您正在运行 Go 1.11 但未启用模块,还是您正在运行 Go 1.7、1.8、1.9 或 1.10 等没有完整模块支持的旧版本)。有关“主版本子目录”方法的更多详细信息,请参阅 “如何发布 v2+ 模块” 部分。

本 FAQ 的其余部分侧重于 “如何发布 v2+ 模块” 部分描述的“主版本分支”方法。在“主版本分支”方法中,不创建 /vN 子目录,而是通过 go.mod 文件和对提交应用语义化版本标签(通常在 master 分支上,但也可能在其他分支上)来传达模块版本信息。

为了帮助度过当前的过渡期,Go 1.11 引入了“最低限度模块兼容性”,以提供对尚未选择加入模块模式的 Go 代码更大的兼容性,并且该“最低限度模块兼容性”也被回移植到 Go 1.9.7 和 1.10.3 中(这些版本实际上总是禁用完整模块模式,因为这些较旧的 Go 版本没有完整的模块支持)。

“最低限度模块兼容性”的主要目标是

  1. 允许较旧的 Go 版本 1.9.7+ 和 1.10.3+ 更容易地编译在导入路径中使用 /vN 并使用语义化导入版本控制的模块,并在 Go 1.11 中禁用 模块模式 时提供相同的行为。

  2. 允许旧代码能够使用 v2+ 模块,而无需旧的消费者代码在使用 v2+ 模块时立即更改为使用新的 /vN 导入路径。

  3. 这样做而无需依赖模块作者创建 /vN 子目录。

更多详情 – “最低限度模块兼容性”

“最低限度模块兼容性”仅在 go 工具的完整 模块模式 被禁用时生效,例如如果您在 Go 1.11 中设置了 GO111MODULE=off,或者正在使用 Go 版本 1.9.7+ 或 1.10.3+。

当 v2+ 模块作者创建 /v2/vN 子目录,而是依赖 Go 1.9.7+、1.10.3+ 和 1.11 中的“最低限度模块兼容性”机制时

如果我创建了 go.mod 但没有对我的仓库应用 semver 标签会发生什么?

语义化版本(semver) 是模块系统的基础。为了给消费者提供最佳体验,鼓励模块作者应用语义化版本 VCS 标签(例如 v0.1.0v1.2.3-rc.1),但语义化版本 VCS 标签不是严格要求的

  1. 模块必须遵循语义化版本规范,才能使 go 命令按文档所述运行。这包括遵循关于何时允许破坏性更改的语义化版本规范。

  2. 没有语义化版本 VCS 标签的模块会被消费者以 伪版本 的形式记录为一个语义化版本。通常这将是一个 v0 主版本,除非模块作者遵循 “主版本子目录” 方法构建了 v2+ 模块。

  3. 因此,未应用语义化版本 VCS 标签且未创建“主版本子目录”的模块实际上是在声明自己属于语义化版本 v0 主版本系列,基于模块的消费者会将其视为具有语义化版本 v0 主版本。

模块可以依赖于它自己的不同版本吗?

一个模块可以依赖于自身的另一个主版本:这大致上与依赖于另一个模块相当。这在多种情况下都很有用,包括允许一个模块的主版本作为另一个主版本的垫片(shim)实现。

此外,一个模块可以在循环中依赖于自身的另一个主版本,就像两个完全不同的模块可以在循环中相互依赖一样。

然而,如果您不期望一个模块依赖于自身的另一个版本,这可能是一个错误的迹象。例如,旨在从 v3 模块导入包的 .go 代码可能在导入语句中缺少必需的 /v3。这个错误可能表现为一个 v3 模块依赖于自身的 v1 版本。

如果您惊讶地看到一个模块依赖于自身的另一个版本,那么回顾上面的 “语义化导入版本控制” 部分以及 FAQ “如果我看不到依赖项的预期版本,可以检查什么?” 可能是有益的。

两者之间仍然存在一个限制,即两个 不能在循环中相互依赖。

常见问题解答 — 多模块仓库

什么是多模块仓库?

多模块仓库是一个包含多个模块的仓库,每个模块都有自己的 go.mod 文件。每个模块都从包含其 go.mod 文件的目录开始,并递归包含该目录及其子目录中的所有包,排除任何包含另一个 go.mod 文件的子树。

每个模块都有自己的版本信息。仓库根目录以下的模块的版本标签必须包含相对目录作为前缀。例如,考虑以下仓库

my-repo
`-- foo
    `-- rop
        `-- go.mod

模块 “my-repo/foo/rop” 版本 1.2.3 的标签是 “foo/rop/v1.2.3”。

通常,仓库中一个模块的路径将是其他模块路径的前缀。例如,考虑这个仓库

my-repo
|-- bar
|-- foo
|   |-- rop
|   `-- yut
|-- go.mod
`-- mig
    |-- go.mod
    `-- vub

Fig. A top-level module’s path is a prefix of another module’s path.

图。顶层模块的路径是另一个模块路径的前缀。

这个仓库包含两个模块。然而,模块 “my-repo” 是模块 “my-repo/mig” 路径的前缀。

我应该在一个仓库中拥有多个模块吗?

在这种配置中添加、移除和版本化模块需要相当多的细心和斟酌,因此管理单个模块仓库几乎总是比在现有仓库中管理多个模块更容易、更简单。

Russ Cox 在 #26664 中评论道

除了高级用户,您可能希望遵循一个仓库等于一个模块的惯例。对于代码存储选项的长期演进来说,一个仓库可以包含多个模块很重要,但这几乎肯定不是您默认想做的事情。

多模块可能带来更多工作的两个例子

然而,除了这两个例子之外还有更多的细微之处。如果您正在考虑在单个仓库中拥有多个模块,请仔细阅读 本小节 中的常见问题解答。

在仓库中拥有多个 go.mod 文件可能合理的两个示例场景

  1. 如果您有一些使用示例,而这些示例本身拥有一组复杂的依赖项(例如,您的包很小,但包含了一个使用您的包与 kubernetes 集成的示例)。在这种情况下,您的仓库中拥有一个包含其自己的 go.mod 文件的 example_example 目录可能是有意义的,例如 此处 所示。

  2. 如果您的仓库拥有一组复杂的依赖项,但您有一个依赖项较少的客户端 API。在某些情况下,拥有一个包含其自己的 go.mod 文件的 apiclientapi 或类似目录可能是有意义的,或者将该 clientapi 分离到其自己的仓库中。

然而,对于这两种情况,如果您正在考虑创建多模块仓库以解决大量间接依赖项的性能或下载大小问题,强烈建议您首先尝试使用 GOPROXY,它将在 Go 1.13 中默认启用。使用 GOPROXY 在很大程度上可以获得创建多模块仓库所带来的性能优势或依赖项下载大小优势。

是否可以将模块添加到多模块仓库?

是的。然而,这个问题有两种情况

第一种情况:要添加模块的包尚未纳入版本控制(一个新包)。这种情况很简单:在同一个提交中添加包和 go.mod,打上标签,然后推送。

第二种情况:要添加模块的路径已纳入版本控制,并且包含一个或多个现有包。为了说明这一点,再次考虑以下仓库(现在位于 github.com,以更好地模拟现实世界)

github.com/my-repo
|-- bar
|-- foo
|   |-- rop
|   `-- yut
|-- go.mod
`-- mig
    `-- vub

考虑添加模块“github.com/my-repo/mig”。如果按照上述相同的方法,/my-repo/mig 包可能由两个不同的模块提供:“github.com/my-repo” 的旧版本,以及新的独立模块“github.com/my-repo/mig”。如果这两个模块都处于活动状态,导入 “github.com/my-repo/mig” 将在编译时导致“模糊导入”错误。

解决这个问题的方法是让新添加的模块依赖于它被“拆分”出来的模块,依赖的版本是拆分发生之后的版本。

让我们以上述仓库为例逐步进行,假设“github.com/my-repo”当前版本为 v1.2.3

  1. 添加 github.com/my-repo/mig/go.mod

    cd path-to/github.com/my-repo/mig
    go mod init github.com/my-repo/mig
    
    # Note: if "my-repo/mig" does not actually depend on "my-repo", add a blank
    # import.
    # Note: version must be at or after the carve-out.
    go mod edit -require github.com/myrepo@v1.3
    
  2. git commit

  3. git tag v1.3.0

  4. git tag mig/v1.0.0

  5. 接下来,我们来测试这些。我们不能直接天真地运行 go buildgo test,因为 go 命令会尝试从模块缓存中获取每个依赖模块。所以,我们需要使用 replace 规则来使 go 命令使用本地副本

    cd path-to/github.com/my-repo/mig
    go mod edit -replace github.com/my-repo@v1.3.0=../
    go test ./...
    go mod edit -dropreplace github.com/my-repo@v1.3.0
    
  6. git push origin master v1.3.0 mig/v1.0.0 推送提交和两个标签

请注意,将来 golang.org/issue/28835 应该会使测试步骤更加直接简单。

另请注意,“github.com/my-repo” 模块在次版本之间删除了代码。这可能看起来不像是主要更改,有点奇怪,但在这种情况下,传递性依赖项继续在原始导入路径上提供与已删除包兼容的实现。

是否可以从多模块仓库中移除模块?

是的,与上述两种情况和类似步骤相同。

模块可以依赖于另一个模块中的 internal/ 吗?

是的。只要它们共享直到 internal/ 路径组件的相同路径前缀,一个模块中的包就可以导入另一个模块中的内部包。例如,考虑以下仓库

my-repo
|-- foo
|   `-- go.mod
|-- go.mod
`-- internal

在这里,只要模块“my-repo/foo”依赖于模块“my-repo”,包 foo 就可以导入 /my-repo/internal。类似地,在以下仓库中

my-repo
|-- foo
|   `-- go.mod
`-- internal
    `-- go.mod

在这里,只要模块“my-repo/foo”依赖于模块“my-repo/internal”,包 foo 就可以导入 my-repo/internal。两者的语义是相同的:由于 my-repomy-repo/internalmy-repo/foo 之间的共享路径前缀,包 foo 允许导入包 internal。

额外的 go.mod 文件可以排除不必要的内容吗?模块是否有类似 .gitignore 文件的功能?

在单个仓库中拥有多个 go.mod 文件的另一个用例是,如果仓库中包含应从模块中移除的文件。例如,一个仓库可能包含 Go 模块不需要的非常大的文件,或者一个多语言仓库可能包含许多非 Go 文件。

目录中的空 go.mod 文件将导致该目录及其所有子目录被排除在顶层 Go 模块之外。

如果排除的目录不包含任何 .go 文件,则除了放置空 go.mod 文件之外,不需要采取其他步骤。如果排除的目录确实包含 .go 文件,请首先仔细查阅 此多模块仓库部分 中的其他常见问题解答。

常见问题解答 — 最小版本选择

最小版本选择不会阻止开发者获得重要更新吗?

请参阅早期 官方提案讨论中的常见问题解答 中的问题“最低版本选择会不会阻止开发者获取重要更新?”。

常见问题解答 — 可能的问题

如果我遇到问题,有哪些常见事项可以快速检查?

您当前正在查看的错误可能是由您的构建中没有特定模块或包的预期版本所引起的次要问题。因此,如果某个错误的根本原因不明显,检查您的版本(如下一个常见问题解答所述)会很有帮助。

如果我没有看到预期的依赖项版本,可以检查什么?

  1. 一个好的第一步是运行 go mod tidy。这有可能解决问题,但它也会帮助您的 go.mod 文件与您的 .go 源代码保持一致的状态,这将有助于使后续的任何调查更容易。(如果 go mod tidy 本身以您不期望的方式更改了依赖项的版本,请首先阅读 关于 ‘go mod tidy’ 的常见问题解答。如果这不能解释问题,您可以尝试重置您的 go.mod,然后运行 go list -mod=readonly all,这可能会提供关于需要更改其版本的具体信息)。

  2. 第二步通常应该是检查 go list -m all 以查看您的构建中选择的实际版本列表。go list -m all 显示最终选定的版本,包括间接依赖项的版本以及解析任何共享依赖项版本后的结果。它还显示了任何 replaceexclude 指令的影响。

  3. 下一个好的步骤是检查 go mod graphgo mod graph | grep 的输出。go mod graph 打印模块需求图(包括考虑了 replace 指令)。输出中的每一行都有两个字段:第一列是消费模块,第二列是该模块的一个需求(包括该消费模块要求的版本)。这是一种快速查看哪些模块需要特定依赖项的方法,包括当您的构建中的一个依赖项有来自不同消费者的不同所需版本时(如果是这种情况,熟悉上面 “版本选择” 部分描述的行为非常重要)。

go mod why -m 在这里也可能有用,尽管它通常更常用于查看为什么包含某个依赖项(而不是该依赖项为何最终为特定版本)。

go list 提供了许多其他查询变体,如果需要,这些查询对于检查您的模块很有用。一个示例如下,它将显示您的构建中使用的确切版本,排除仅用于测试的依赖项

go list -deps -f '{{with .Module}}{{.Path}} {{.Version}}{{end}}' ./... | sort -u

关于检查您的模块的更详细的命令和示例可以在一个可运行的“Go Modules by Example” 演示 中看到。

版本不符合预期的一个原因可能是有人创建了一个无效或非预期的 go.mod 文件,或者犯了相关的错误(例如:模块的 v2.0.1 版本可能在其 go.mod 中错误地将自身声明为 module foo 而缺少必需的 /v2.go 代码中旨在导入 v3 模块的导入语句可能缺少必需的 /v3;v4 模块的 go.mod 中的 require 语句可能缺少必需的 /v4)。因此,如果您看到某个问题的根本原因不明显,首先重读上面 “go.mod”“语义化导入版本控制” 部分的材料可能是值得的(考虑到这些部分包含了模块必须遵循的重要规则),然后花几分钟快速检查最相关的 go.mod 文件和导入语句。

为什么我会收到 ‘cannot find module providing package foo’ 错误?

这是一个通用错误消息,可能由几种不同的根本原因引起。

在某些情况下,此错误仅由于路径输入错误引起,因此第一步可能是根据错误消息中列出的详细信息,仔细检查是否存在错误的路径。

如果您尚未这样做,下一个好的步骤通常是尝试 go get -v foogo get -v -x foo

其他一些可能的原因

为什么 ‘go mod init’ 会出现 ‘cannot determine module path for source directory’ 错误?

不带任何参数运行 go mod init 将尝试根据 VCS 元数据等不同提示猜测正确的模块路径。然而,不能指望 go mod init 总是能猜到正确的模块路径。

如果 go mod init 给出此错误,则表示这些启发式方法无法猜测,您必须自行提供模块路径(例如 go mod init github.com/you/hello)。

我遇到了一个复杂依赖项的问题,它尚未选择使用模块。我可以使用其当前依赖管理器中的信息吗?

是的。这需要一些手动步骤,但在某些更复杂的情况下可能会有所帮助。

Go 1.21 及更早版本尝试将先前的依赖管理器格式转换为 go.mod 格式。因此,以下说明需要 1.21.13 或更早版本;您需要使用 GOTOOLCHAIN=go1.21.13 运行以下命令,或手动安装 Go 的旧版本。

当您初始化自己的模块时运行 go mod init,它将通过转换 Gopkg.lockglide.lockvendor.json 等配置文件,自动从先前的依赖管理器进行转换,生成一个包含相应 require 指令的 go.mod 文件。例如,现有 Gopkg.lock 文件中的信息通常描述了所有直接和间接依赖项的版本信息。

然而,如果您正在添加一个新的依赖项,而该依赖项本身尚未选择使用模块,则无法从您的新依赖项可能使用的任何先前依赖管理器进行类似的自动转换过程。如果该新依赖项本身具有发生破坏性更改的非模块依赖项,那么在某些情况下可能会导致不兼容问题。换句话说,您的新依赖项的先前依赖管理器不会被自动使用,这在某些情况下可能会导致您的间接依赖项出现问题。

一种方法是在您的有问题的非模块直接依赖项上运行 go mod init,以从其当前的依赖管理器进行转换,然后使用生成的临时 go.mod 中的 require 指令来填充或更新您模块中的 go.mod

例如,如果 github.com/some/nonmodule 是您模块中一个有问题的直接依赖项,并且当前正在使用另一个依赖管理器,您可以执行类似以下操作:

$ git clone -b v1.2.3 https://github.com/some/nonmodule /tmp/scratchpad/nonmodule
$ cd /tmp/scratchpad/nonmodule
$ go mod init
$ cat go.mod

您可以将临时 go.mod 中生成的 require 信息手动移动到您模块的实际 go.mod 中,或者您可以考虑使用 https://github.com/rogpeppe/gomodmerge,这是一个针对此用例的社区工具。此外,您还需要在实际的 go.mod 中添加一个 require github.com/some/nonmodule v1.2.3,以匹配您手动克隆的版本。

在此 #28489 评论中有一个针对 docker 遵循此技术的具体示例,它说明了如何获取一组一致的 docker 依赖项版本,以避免 github.com/sirupsen/logrusgithub.com/Sirupsen/logrus 之间的区分大小写问题。

如何解决由于导入路径与声明的模块标识不匹配导致的“parsing go.mod: unexpected module path”和“error loading module requirements”错误?

为什么会出现此错误?

通常,模块通过其 go.mod 中的 module 指令声明其身份,例如 module example.com/m。这是该模块的“模块路径”,go 工具强制执行声明的模块路径与任何消费者使用的导入路径之间的一致性。如果模块的 go.mod 文件读取 module example.com/m,则消费者必须使用以该模块路径开头的导入路径(例如 import "example.com/m"import "example.com/m/sub/pkg")从该模块导入包。

如果消费者使用的导入路径与相应的声明模块路径不匹配,go 命令会报告一个 `parsing go.mod: unexpected module path` 的致命错误。此外,在某些情况下,go 命令随后会报告一个更通用的 `error loading module requirements` 错误。

此错误最常见的原因是发生了名称更改(例如,github.com/Sirupsen/logrus 变更为 github.com/sirupsen/logrus),或者由于虚荣导入路径(vanity import path)的原因,模块在模块化之前有时通过两个不同的名称使用(例如,github.com/golang/sync 与推荐的 golang.org/x/sync)。

如果您有一个依赖项仍然通过旧名称(例如 github.com/Sirupsen/logrus)或非规范名称(例如 github.com/golang/sync)导入,而该依赖项随后采用了模块化并在其 go.mod 中声明了其规范名称,那么这可能会导致问题。当找到已升级的模块版本声明了一个与旧导入路径不匹配的规范模块路径时,此处的错误可能会在升级过程中触发。

问题场景示例

go: github.com/Quasilyte/go-consistent@v0.0.0-20190521200055-c6f3937de18c: parsing go.mod: unexpected module path “github.com/quasilyte/go-consistent” go get: error loading module requirements

解决办法

此错误最常见的形式是:

go: example.com/some/OLD/name@vX.Y.Z: parsing go.mod: unexpected module path “example.com/some/NEW/name”

如果您访问 example.com/some/NEW/name(错误右侧的名称)的仓库,您可以查看最新版本或 master 分支中的 go.mod 文件,以确认 go.mod 的第一行是否声明为 module example.com/some/NEW/name。如果是这样,这表明您遇到了“旧模块名称”与“新模块名称”的问题。

本节的其余部分重点介绍如何按照以下步骤按顺序解决此错误的“旧名称”与“新名称”形式:

  1. 检查您自己的代码,查看是否使用了 example.com/some/OLD/name 进行导入。如果是,请更新您的代码,改用 example.com/some/NEW/name 进行导入。

  2. 如果您在升级过程中收到此错误,您应该尝试使用 Go 的 tip 版本进行升级,该版本具有更具针对性的升级逻辑(#26902),这通常可以规避此问题,并且通常在此情况下提供更好的错误消息。请注意,tip / 1.13 版本中的 go get 参数与 1.12 版本不同。以下是如何获取 tip 并使用它来升级依赖项的示例:

go get golang.org/dl/gotip && gotip download
gotip get -u all
gotip mod tidy

由于有问题的旧导入通常存在于间接依赖项中,使用 tip 进行升级然后运行 go mod tidy 通常可以将您升级到绕过有问题的版本,然后将不再需要的有问题的版本从 go.mod 中移除,这样当您回到使用 Go 1.12 或 1.11 进行日常工作时,就能处于正常运行状态。例如,在此可以看到这种方法有效解决 github.com/golang/lintgolang.org/x/lint 的问题。

  1. 如果您在执行 go get -u foogo get -u foo@latest 时收到此错误,请尝试移除 `-u`。这将获取 foo@latest 所使用的依赖项集,而不会将 foo 的依赖项升级到超出 foo 作者在发布 foo 时可能已验证为可用的版本。这在此过渡时期尤为重要,因为 foo 的一些直接和间接依赖项可能尚未采用 semver 或模块。(一个常见的错误是认为 go get -u foo 只获取 foo 的最新版本。实际上,go get -u foogo get -u foo@latest 中的 `-u` 意味着同时获取 foo所有直接和间接依赖项的最新版本;这可能是您想要的,但如果由于深层间接依赖项而导致失败,则可能不是您想要的)。

  2. 如果以上步骤未能解决错误,下一个方法会稍微复杂一些,但通常可以解决此错误的“旧名称”与“新名称”形式。这仅使用错误消息本身的信息,再加上对某些 VCS 历史记录的简要查看。

    4.1. 转到 example.com/some/NEW/name 仓库

    4.2. 确定 go.mod 文件是在何时引入的(例如,通过查看 go.mod 的 blame 或历史视图)。

    4.3. 选择在该 go.mod 文件引入之前的发布版本或提交。

    4.4. 在您的 go.mod 文件中,添加一个 replace 语句,在 replace 语句的两侧都使用旧名称:replace example.com/some/OLD/name => example.com/some/OLD/name <version-just-before-go.mod> 使用我们之前的例子,其中 github.com/Quasilyte/go-consistent 是旧名称,github.com/quasilyte/go-consistent 是新名称,我们可以看到 go.mod 是在提交 00c5b0cf371a 中首次引入的。该仓库没有使用 semver 标签,因此我们将使用紧接之前的提交 00dd7fb039e,并在 replace 两侧使用旧的、大写字母开头的 Quasilyte 名称将其添加到 replace 中。

replace github.com/Quasilyte/go-consistent => github.com/Quasilyte/go-consistent 00dd7fb039e

这个 replace 语句使我们能够绕过有问题的“旧名称”与“新名称”不匹配的问题进行升级,因为它有效地阻止了在存在 go.mod 的情况下将旧名称升级为新名称。通常,通过 go get -u 或类似命令进行的升级现在可以避免此错误。如果升级完成,您可以检查是否还有人导入旧名称(例如,运行 go mod graph | grep github.com/Quasilyte/go-consistent),如果没有,则可以移除该 replace 语句。(这种方法之所以经常有效,是因为即使有问题的旧导入路径在升级完成后最终结果中可能不会被使用,但在升级过程中如果使用了该路径,升级本身也可能失败,这在 #30831 中有所记录)。

  1. 如果上述步骤未能解决问题,可能是因为您一个或多个依赖项的最新版本仍在 P使用有问题的旧导入路径。在这种情况下,重要的是找出谁仍在 P使用有问题的旧导入路径,并查找或提出问题,要求有问题的导入方更改为使用当前的规范导入路径。在上述步骤 2 中使用 `gotip` 可能能找出有问题的导入方,但并非所有情况下都能做到,尤其是在升级时(#30661)。如果无法确定谁在使用有问题的旧导入路径进行导入,通常可以通过创建一个干净的模块缓存,执行触发错误的操作,然后在模块缓存中搜索有问题的旧导入路径来找出。例如:
export GOPATH=$(mktemp -d)
go get -u foo               # perform operation that generates the error of interest
cd $GOPATH/pkg/mod
grep -R --include="*.go" github.com/Quasilyte/go-consistent
  1. 如果这些步骤不足以解决问题,或者如果您是某个项目的维护者,由于循环引用而似乎无法移除对旧的有问题导入路径的引用,请参阅单独的 wiki 页面上对该问题的更详细说明。

最后,上述步骤重点介绍了如何解决潜在的“旧名称”与“新名称”问题。然而,如果 go.mod 文件放置位置错误或模块路径不正确,也可能出现相同的错误消息。在这种情况下,导入该模块总是会失败。如果您正在导入一个刚创建且之前从未成功导入过的新模块,您应该检查 go.mod 文件位置是否正确,并且其模块路径是否与该位置相对应。(最常见的方法是一个仓库一个 go.mod,将唯一的 go.mod 文件放在仓库根目录,并使用仓库名称作为 module 指令中声明的模块路径)。更多详细信息请参阅 “go.mod” 部分。

为什么 ‘go build’ 需要 gcc,以及为什么不使用 net/http 等预编译包?

简而言之

因为预构建的包是非模块构建,无法重复使用。抱歉。暂时禁用 cgo 或安装 gcc。

这仅在选择使用模块时(例如,通过设置 GO111MODULE=on)才会出现问题。请参阅 #26988 获取更多讨论。

模块是否支持 import "./subdir" 这样的相对导入?

不是。请参阅 #26645,其中包括:

在模块中,子目录终于有了名称。如果父目录声明为“module m”,则子目录导入为“m/subdir”,不再是“./subdir”。

某些所需文件可能不存在于已填充的 vendor 目录中

go mod vendor 不会将没有 `.go` 文件的目录复制到 `vendor` 目录中。这是设计如此。

简而言之,抛开任何特定的 vendoring 行为不谈,Go 构建的整体模型是:构建包所需的文件应与 `.go` 文件位于同一目录中。

以 cgo 为例,修改其他目录中的 C 源代码不会触发重新构建,而是您的构建会使用陈旧的缓存条目。cgo 文档现在包含

请注意,更改其他目录中的文件不会导致包重新编译,因此包的所有非 Go 源代码应存储在包目录中,而不是子目录中。

一个社区工具 https://github.com/goware/modvendor 允许您轻松地将模块中的一套完整的 .c, .h, .s, .proto 或其他文件复制到 `vendor` 目录中。尽管这可能有所帮助,但如果构建包所需的文件位于 `.go` 文件所在的目录之外,您仍需小心谨慎,确保您的 Go 构建总体上得到正确处理(无论是否使用 vendoring)。

请参阅 #26366 中的更多讨论。

传统 vendoring 的另一种方法是将模块缓存签入版本控制。它最终可以获得与传统 vendoring 类似的益处,并且在某些方面会得到更高保真度的副本。这种方法在“Go Modules by Example”教程中有所解释。


此内容是 Go Wiki 的一部分。