Go Modules怎么用?Go项目中如何初始化和依赖管理?

文章导读
在 Go 1.13 版本中,Go 的作者添加了一种新的管理 Go 项目依赖库的方法,称为 Go modules。Go modules 的引入是为了响应开发者日益增长的需求,使维护各种依赖版本更加容易,并为开发者在计算机上组织项目的方式提供更多灵活性。Go modules 通常由一个项目或库组成,包含一组一起发布的 Go 包。Go modules 通过允许用户将项目代码放在选择的目录中并为每个模块指
📋 目录
  1. 引言
  2. 先决条件
  3. 创建新 Module
  4. 理解 go.mod 文件
  5. 向您的模块添加 Go 代码
  6. 向您的模块添加一个包
  7. 将远程模块添加为依赖
  8. 使用模块的特定版本
  9. 使用 Go Workspaces 处理多个模块
  10. 使用 replace 指令
A A

引言

在 Go 1.13 版本中,Go 的作者添加了一种新的管理 Go 项目依赖库的方法,称为 Go modules。Go modules 的引入是为了响应开发者日益增长的需求,使维护各种依赖版本更加容易,并为开发者在计算机上组织项目的方式提供更多灵活性。Go modules 通常由一个项目或库组成,包含一组一起发布的 Go 包。Go modules 通过允许用户将项目代码放在选择的目录中并为每个模块指定依赖版本,解决了原始系统 GOPATH 的许多问题。

尽管 Go modules 在 Go 1.11 中引入,并在后续版本(Go 1.16+)成为默认选项,但它们现在是现代 Go 中的标准依赖管理系統。GOPATH 工作流程不再推荐用于新项目。

在本教程中,您将创建自己的公共 Go module,并向新模块添加一个包。您还将向项目添加远程依赖,并学习如何使用 tags、branches 和 commits 来引用特定模块版本。此外,您还将探索高级工作流程,包括用于管理多个模块的 Go workspaces、本地开发中的 replace 指令,以及配置对私有模块的访问。您还将了解 go get 的行为如何在不同 Go 版本中演变,以提供更清晰的依赖管理。

关键要点:

  • Go modules 允许您将项目放置在文件系统中的任何位置,而不仅仅是 GOPATHgo.mod 文件定义您的模块及其依赖,而 go.sum 包含加密校验和,以确保安全性和可重现性。
  • 始终将 go.modgo.sum 都提交到版本控制中,以确保不同环境中的一致构建,并防止依赖篡改。
  • 在添加或移除 imports、运行 go get 或提交更改之前,定期运行 go mod tidy。此命令会自动移除未使用的依赖并添加缺失的依赖。
  • 在现代 Go 中,使用 go get 管理项目中的模块依赖,使用 go install 构建和安装可执行二进制文件。这种分离提供了更清晰的意图,并防止意外修改 go.mod
  • 在添加依赖时,始终使用 @latest@v1.2.3@branch-name@commit-hash 明确指定版本。这确保了可重现构建,并使您的依赖要求清晰明了。
  • Go workspaces 允许您同时开发多个相关模块,而无需修改 go.mod 文件。使用 go work init 创建工作区,并使用 go work use 添加模块,将工作区文件保留在本地开发环境中。
  • 为私有仓库配置 GOPRIVATE 环境变量,以防止 Go 将模块路径发送到公共代理。结合 SSH 密钥或个人访问令牌进行身份验证。
  • go.mod 中标记为 // indirect 的间接依赖是传递依赖——由您的直接依赖所需但未在代码中直接导入的模块。Go 跟踪这些依赖以确保完整的构建可重现性。
  • go.mod 中使用 replace 指令进行永久模块替换,例如重定向到 fork 或在 monorepo 中使用本地版本。与 workspaces 不同,replace 指令会被提交并与团队共享。
  • Go modules 无缝支持 Git 功能,允许您使用 @ 语法引用特定 tags、branches 和 commits。这为您提供了对依赖版本的精细控制,从稳定发布到特定 commits。

先决条件

要完成本教程,您需要:

  • 已安装 Go 1.18 或更高版本。要进行设置,请按照适用于您操作系统的 How To Install Go 教程操作。

  • 熟悉在 Go 中编写 package。要了解更多,请按照 How To Write Packages in Go 教程操作。

  • (可选)对 Git 和版本控制系统有基本了解。由于 Go modules 通常通过 repository 分发,了解 repository 和版本标签的工作原理将很有帮助。

创建新 Module

乍一看,Go module 与 Go package 相似。Module 包含多个 Go 代码文件来实现 package 的功能,但它在根目录下还有两个额外的关键文件:go.mod 文件和 go.sum 文件。这些文件包含 go 工具用于跟踪 module 配置的信息,通常由工具自动维护,您无需手动操作。

首先要做的是决定 module 将位于哪个目录。随着 Go modules 的引入,Go 项目可以在文件系统中的任何位置,而不仅仅是 Go 定义的特定目录。您可能已经有一个用于项目的目录,但在本教程中,您将创建一个名为 projects 的目录,新 module 将命名为 mymodule。您可以通过 IDE 或命令行创建 projects 目录。

注意:在实际项目中,module 名称通常基于 repository 路径(例如 github.com/username/mymodule),以便他人可以导入。为简化,本教程使用 mymodule

如果您使用命令行,首先创建 projects 目录并进入该目录:

  1. mkdir projects
  2. cd projects

接下来,您将创建 module 目录本身。通常,module 的顶级目录名称与 module 名称相同,这样更容易跟踪。在您的 projects 目录中,运行以下命令创建 mymodule 目录:

  1. mkdir mymodule

创建 module 目录后,目录结构将如下所示:

└── projects
    └── mymodule

下一步是在 mymodule 目录中创建 go.mod 文件,以定义 Go module。为此,您将使用 go 工具的 mod init 命令,并提供 module 名称,在本例中为 mymodule。现在从 mymodule 目录运行 go mod init 并提供 module 名称 mymodule,从而创建 module:

  1. go mod init mymodule

注意:在生产环境中,您通常会运行:

  1. go mod init github.com/username/mymodule

使用完全限定的 module 路径可确保您的 module 可以被正确导入和版本化。

此命令在创建 module 时将返回以下输出:

Output
go: creating new go.mod: module mymodule

创建 module 后,您的目录结构现在将如下所示:

└── projects
    └── mymodule
        └── go.mod

现在您已经创建了 module,让我们看看 go.mod 文件内部,了解 go mod init 命令做了什么。

理解 go.mod 文件

当你使用 go 工具运行命令时,go.mod 文件是过程中的一个非常重要的部分。它包含模块名称以及你的模块所依赖的其他模块的版本。它还可以包含其他指令,例如 replace,这在同时开发多个模块时非常有用。

mymodule 目录中,使用 nano 或你喜欢的文本编辑器打开 go.mod 文件:

  1. nano go.mod

内容看起来类似于这样,并不多:

projects/mymodule/go.mod
module mymodule

go 1.26

第一行,即 module 指令,告诉 Go 你的模块名称,这样当它查看包中的 import 路径时,就知道不必在其他地方寻找 mymodulemymodule 值来自于你传递给 go mod init 的参数:

module mymodule

目前文件中唯一的其他一行,即 go 指令,告诉 Go 该模块针对的语言版本。在这种情况下,由于模块是使用较新版本的 Go 创建的,go 指令反映了该版本:

go 1.26

注意: go 指令指定模块所需的最低 Go 版本,并可能影响模块行为。在现代 Go 版本中,此值会在需要时由 go 工具自动更新。

随着模块中添加更多信息,此文件会扩展,但现在查看它是个好主意,以便了解后续添加依赖时它如何变化。

除了 go.mod,在添加依赖时你还会看到创建 go.sum 文件。go.sum 文件存储模块依赖的加密校验和,以确保其完整性。这有助于保证不同环境中使用相同的依赖版本,并且下载的模块未被篡改。

注意: 你应该将 go.modgo.sum 都提交到版本控制中,以确保可重现构建。

你现在已经使用 go mod init 创建了一个 Go 模块,并查看了初始 go.mod 文件的内容,但你的模块还什么都不做。是时候进一步开发模块并添加一些代码了。

向您的模块添加 Go 代码

为了确保模块创建正确并添加代码,以便运行您的第一个 Go 模块,您将在 mymodule 目录中创建一个 main.go 文件。main.go 文件在 Go 程序中通常用于指示程序的起点。文件名本身并不如其中的 main function 重要,但将两者匹配可以更容易找到。在本教程中,运行 main function 将打印 Hello, Modules!

要创建该文件,请使用 nano 或您喜欢的文本编辑器打开 main.go 文件:

  1. nano main.go

main.go 文件中,添加以下代码来定义您的 main package,导入 fmt package,然后在 main function 中打印 Hello, Modules! 消息:

projects/mymodule/main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello, Modules!")
}

在 Go 中,每个目录被视为自己的 package,每个文件都有自己的 package 声明行。在您刚刚创建的 main.go 文件中,package 名为 main。通常,您可以为 package 命名任何您喜欢的名称,但 main package 在 Go 中是特殊的。当 Go 看到一个 package 名为 main 时,它知道该 package 应被视为 binary,并编译成可执行文件,而不是设计为在其他程序中使用的 library。

在定义 package 之后,import 声明表示导入 fmt package,以便您可以使用其 Println function 将 "Hello, Modules!" 消息打印到屏幕上。

最后,定义了 main function。main function 是 Go 中的另一个特殊情况,与 main package 相关。当 Go 在名为 main 的 package 中看到名为 main 的 function 时,它知道 main function 是它应该首先运行的 function。这被称为程序的 entry point。

创建 main.go 文件后,模块的目录结构将类似于此:

└── projects
    └── mymodule
        └── go.mod
        └── main.go

如果您熟悉使用 Go 和 GOPATH,在 module 中运行代码类似于从 GOPATH 中的目录运行。(如果您不熟悉 GOPATH,也不用担心,因为使用 modules 会取代其用法。)

在 Go 中运行可执行程序有两种常见方式:使用 go build 构建 binary,或使用 go run 运行文件。在本教程中,您将使用 go run 直接运行 module,而不是构建 binary,后者需要单独运行。

使用 go run 运行您创建的 main.go 文件:

  1. go run main.go

运行该命令将打印代码中定义的 Hello, Modules! 文本:

Output
Hello, Modules!

提示: 在 module-aware mode(现代 Go 版本的默认模式)下,您还可以使用以下命令运行整个 module:

  1. go run .

在本节中,您向 module 添加了一个 main.go 文件,其中包含一个初始的 main function,用于打印 Hello, Modules!。此时,您的程序尚未从成为 Go module 中受益——它可以是计算机上任何位置的一个文件,使用 go run 运行。Go modules 的第一个真正好处是能够在任何目录中向项目添加 dependencies,而不仅仅是 GOPATH 目录结构。您还可以向 module 添加 packages。在下一节中,您将通过在其内部创建额外的 package 来扩展您的 module。

向您的模块添加一个包

类似于标准 Go package,一个 module 可以包含任意数量的 package 和子 package,也可以一个都不包含。在本例中,您将在 mymodule 目录内创建一个名为 mypackage 的 package。

通过在 mymodule 目录内运行 mkdir 命令并带上 mypackage 参数来创建这个新 package:

  1. mkdir mypackage

这将创建新的目录 mypackage,作为 mymodule 目录的子 package:

└── projects
    └── mymodule
        └── mypackage
        └── main.go
        └── go.mod

使用 cd 命令切换到您的新 mypackage 目录,然后使用 nano 或您喜欢的文本编辑器创建一个 mypackage.go 文件。这个文件可以有任意名称,但使用与 package 相同的名称可以更容易找到该 package 的主要文件:

  1. cd mypackage
  2. nano mypackage.go

mypackage.go 文件中,添加一个名为 PrintHello 的 function,当调用时它将打印消息 Hello, Modules! This is mypackage speaking!

projects/mymodule/mypackage/mypackage.go
package mypackage

import "fmt"

func PrintHello() {
    fmt.Println("Hello, Modules! This is mypackage speaking!")
}

由于您希望 PrintHello function 可以从其他 package 中访问,因此 function 名称中的大写 P 非常重要。大写字母表示该 function 是 exported 的,可以被任何外部程序访问。有关 Go 中 package 可见性工作原理的更多信息,请参阅 Understanding Package Visibility in Go

现在您已经创建了带有 exported function 的 mypackage package,您需要从 mymodule package 中 import 它才能使用它。这类似于您之前 import 其他 package(如 fmt package)的方式,只不过这次您需要在 import path 的开头包含您的 module 名称。从 mymodule 目录打开您的 main.go 文件,并通过添加以下突出显示的行来调用 PrintHello

projects/mymodule/main.go

package main

import (
    "fmt"

    "mymodule/mypackage"
)

func main() {
    fmt.Println("Hello, Modules!")

    mypackage.PrintHello()
}

如果您仔细查看 import 语句,您会看到新的 import 以 mymodule 开头,这与您在 go.mod 文件中设置的 module 名称相同。后面跟着路径分隔符和您要 import 的 package,在本例中是 mypackage

"mymodule/mypackage"

<$>[note] 注意:在真实项目中,如果 module path 是 repository URL(例如 github.com/username/mymodule),则 import path 将包含完整的路径:

"github.com/username/mymodule/mypackage"

将来,如果您在 mypackage 内添加 package,您也可以类似的方式将它们添加到 import path 的末尾。例如,如果您在 mypackage 内有一个名为 extrapackage 的 package,则该 package 的 import path 将是 mymodule/mypackage/extrapackage

像之前一样,从 mymodule 目录使用 go run 运行您更新的 module:

  1. go run .

再次运行 module 时,您将看到之前的 Hello, Modules! 消息,以及从您的新 mypackagePrintHello function 打印的新消息:

Output
Hello, Modules! Hello, Modules! This is mypackage speaking!

您现在已经通过创建一个名为 mypackage 的目录并包含 PrintHello function 来向初始 module 添加了一个新 package。不过,随着您的 module 功能扩展,在自己的 module 中开始使用其他人的 module 可能会有所帮助。在下一节中,您将添加一个远程 module 作为您的依赖。

将远程模块添加为依赖

Go modules 从版本控制仓库分发,通常是 Git 仓库。当你想将一个新模块添加为自己的依赖时,你可以使用仓库的路径来引用你想要使用的模块。当 Go 看到这些模块的 import 路径时,它可以根据这个仓库路径推断出远程位置。

在这个示例中,你将为你的模块添加对 Cobra 库的依赖。Cobra 是一个用于创建控制台应用程序的流行库,但本教程不会详细介绍它。

类似于你创建 mymodule 模块时,你将再次使用 go 工具。不过,这次你将从 mymodule 目录运行 go get 命令。在现代 Go 版本中,go get 用于在你的 go.mod 文件中添加或更新依赖,而安装可执行文件则单独使用 go install

运行 go get 并提供你想要添加的模块。在这种情况下,你将获取 github.com/spf13/cobra

  1. go get github.com/spf13/cobra@latest

当你运行此命令时,go 工具将根据你指定的路径查找 Cobra 仓库,并通过查看仓库的分支和标签来确定 Cobra 的最新版本。然后,它将下载该版本,并通过将模块名称和版本添加到 go.mod 文件中来记录所选择的版本,以供以后参考。

添加依赖后,建议运行:

  1. go mod tidy

此命令会清理未使用的依赖,并确保 go.modgo.sum 保持同步。

现在,打开 mymodule 目录中的 go.mod 文件,查看 go 工具在你添加新依赖时如何更新了 go.mod 文件。下面示例可能会根据当前发布的 Cobra 版本或你使用的 Go 工具版本而变化,但更改的整体结构应该类似:

projects/mymodule/go.mod
module mymodule

go 1.26

require (
    github.com/inconshreveable/mousetrap v1.0.0 // indirect
    github.com/spf13/cobra v1.7.0 // indirect
    github.com/spf13/pflag v1.0.5 // indirect
)

添加了一个使用 require 指令的新部分。此指令告诉 Go 你想要哪个模块,例如 github.com/spf13/cobra,以及你添加的模块版本。有时 require 指令还会包含一个 // indirect 注释。此注释表示,在添加 require 指令时,该模块未在模块的任何源文件中直接引用。文件中还添加了一些额外的 require 行。这些行是 Cobra 依赖的其他模块,Go 工具确定也应该被引用。

你可能还注意到,在添加依赖后,mymodule 目录中创建了一个新文件 go.sum。此文件包含下载模块的校验和,确保依赖在不同环境中保持一致和安全。

下载依赖后,你将想要用一些最小的 Cobra 代码更新你的 main.go 文件,以使用新依赖。用下面的 Cobra 代码更新 mymodule 目录中的 main.go 文件,以使用新依赖:

projects/mymodule/main.go
package main

import (
    "fmt"
    "github.com/spf13/cobra"
    "mymodule/mypackage"
)

func main() {
    cmd := &cobra.Command{
        Run: func(cmd *cobra.Command, args []string) {
            fmt.Println("Hello, Modules!")

            mypackage.PrintHello()
        },
    }

    fmt.Println("Calling cmd.Execute()!")
    cmd.Execute()
}

此代码创建了一个 cobra.Command 结构,其中包含一个 Run 函数,内有你现有的“Hello”语句,然后通过调用 cmd.Execute() 来执行。现在,运行更新后的代码:

  1. go run .

你将看到以下输出,看起来与之前看到的类似。不过这次,它使用了你的新依赖,如 Calling cmd.Execute()! 行所示:

Output
Calling cmd.Execute()! Hello, Modules! Hello, Modules! This is mypackage speaking!

使用 go get 添加或更新远程依赖(如 github.com/spf13/cobra),可以更容易地保持你的依赖与模块中定义的版本保持一致。然而,在许多情况下,你可能希望对项目中使用的模块版本有更多控制。例如,你可能想要使用特定的发布版本、开发分支,甚至特定的 commit。

在下一节中,你将使用带有版本标识符的 go get 来引用这些不同类型的模块版本。

使用模块的特定版本

由于 Go modules 是从版本控制仓库分发的,它们可以使用版本控制功能,如 tags、branches,甚至 commits。你可以在依赖项中通过在模块路径末尾使用 @ 符号加上你想要使用的版本来引用这些内容。之前,当你安装 Cobra 的最新版本时,你利用了这一功能,但你不需要显式地将它添加到你的命令中。go 工具知道,如果没有使用 @ 提供特定版本,它应该使用特殊的版本 latest

例如,当你最初添加依赖项时,你也可以使用以下命令获得相同的结果:

  1. go get github.com/spf13/cobra@latest

在现代 Go 工作流程中,推荐使用 @ 显式指定版本,以确保构建的清晰性和可重现性。

现在,想象有一个你正在使用的模块目前处于开发中。为此示例,称它为 your_domain/sammy/awesome。这个 awesome 模块正在添加一个新功能,工作在一个名为 new-feature 的 branch 中。要将这个 branch 添加为你的模块的依赖项,你需要向 go get 提供模块路径,后跟 @ 符号,再后跟 branch 名称:

  1. go get your_domain/sammy/awesome@new-feature

运行此命令将导致 go 连接到 your_domain/sammy/awesome 仓库,下载 new-feature branch 的当前最新 commit,并将该信息添加到 go.mod 文件中。

不过,branches 并不是使用 @ 选项的唯一方式。这种语法也可用于 tags 甚至仓库的特定 commits。例如,有时你使用的库的最新版本可能有一个损坏的 commit。在这些情况下,引用损坏 commit 之前的 commit 可能很有用。

以你的模块的 Cobra 依赖为例,假设你需要引用 github.com/spf13/cobra 的 commit 07445ea,因为它有一些你需要的变化,而且由于某些原因你无法使用其他版本。在这种情况下,你可以在 @ 符号后提供 commit hash,就像对 branch 或 tag 一样。在你的 mymodule 目录中运行 go get 命令,使用模块和版本来下载新版本:

  1. go get github.com/spf13/cobra@07445ea

如果你再次打开你的模块的 go.mod 文件,你会看到 go get 已更新 github.com/spf13/cobrarequire 行,以引用你指定的 commit:

projects/mymodule/go.mod
module mymodule

go 1.26

require (
    github.com/inconshreveable/mousetrap v1.0.0 // indirect
    github.com/spf13/cobra v1.1.2-0.20210209210842-07445ea179fc // indirect
    github.com/spf13/pflag v1.0.5 // indirect
)

由于 commit 是时间上的一个特定点,与 tag 或 branch 不同,Go 在 require 指令中包含额外信息,以确保将来使用正确的版本。如果你仔细查看版本,你会看到它确实包含了你提供的 commit hash:v1.1.2-0.20210209210842-07445ea179fc

Go modules 也使用此功能来支持发布模块的不同版本。当 Go module 发布新版本时,会在仓库中添加一个带有版本号作为 tag 的新 tag。如果你想使用特定版本,你可以查看仓库中的 tag 列表来找到你需要的版本。如果你已经知道版本,你可能不需要搜索 tag,因为版本 tag 的命名是一致的。

回到 Cobra 的例子,假设你想使用 Cobra 版本 1.1.1。你可以查看 Cobra 仓库,看到它有一个名为 v1.1.1 的 tag。要使用这个 tagged 版本,你会在 go get 命令中使用 @ 符号,就像使用非版本 tag 或 branch 一样。现在,通过运行带有 v1.1.1 作为版本的 go get 命令来更新你的模块以使用 Cobra 1.1.1:

  1. go get github.com/spf13/cobra@v1.1.1

现在,如果你打开你的模块的 go.mod 文件,你会看到 go get 已更新 github.com/spf13/cobrarequire 行,以引用你提供的版本:

module mymodule

go 1.26

require (
    github.com/inconshreveable/mousetrap v1.0.0 // indirect
    github.com/spf13/cobra v1.1.1 // indirect
    github.com/spf13/pflag v1.0.5 // indirect
)

最后,如果你正在使用库的特定版本,例如之前的 07445ea commit 或 v1.1.1,但你决定宁愿开始使用最新版本,可以通过使用特殊的 latest 版本来实现。要将你的模块更新到 Cobra 的最新版本,再次运行 go get,使用模块路径和 latest 版本:

  1. go get github.com/spf13/cobra@latest

此命令完成后,go.mod 文件将更新以反映模块的最新可用版本。

go get 命令是一个强大的工具,你可以用它来管理 go.mod 文件中的依赖项,而无需手动编辑它。正如本节所示,使用模块名称与 @ 字符允许你为模块使用特定版本,从发布版本到特定的仓库 commit。它甚至可以用来回退到依赖项的 latest 版本。结合使用这些选项将确保你的程序的稳定性和可重现性。

使用 Go Workspaces 处理多个模块

在现代 Go 开发中,同时处理多个相关模块是很常见的。例如,你可能有一个用于 library 的模块,另一个用于依赖它的 application。传统上,管理此类设置需要使用 replace 指令或发布模块的中间版本。

从 Go 1.18 开始,Go toolchain 引入了使用 go work 命令的 workspaces。Workspaces 允许你将多个模块组合在一起,同时处理它们,而无需修改它们各自的 go.mod 文件。

创建 Workspace

要创建 workspace,请导航到将包含相关模块的目录并运行:

  1. go work init

这将在当前目录中创建一个 go.work 文件。

将模块添加到 Workspace

接下来,将你想要包含在 workspace 中的模块添加进来。例如,如果你有两个名为 mymodulemylibrary 的模块,可以这样添加它们:

  1. go work use ./mymodule ./mylibrary

此命令会更新 go.work 文件以包含这两个模块。

生成的 go.work 文件将类似于这样:

go 1.26

use (
    ./mymodule
    ./mylibrary
)

Workspaces 的工作原理

当你在 workspace 中运行 Go 命令(如 go buildgo rungo test)时,Go toolchain 将使用 go.work 文件中列出的本地模块来解析依赖,而不是从远程仓库下载它们。

这使得以下操作变得更容易:

  • 同时开发多个模块
  • 在不发布新版本的情况下测试跨模块的更改
  • 在开发期间避免使用临时的 replace 指令

何时使用 Workspaces

Workspaces 在以下场景中特别有用:

  • Monorepos: 在单个 repository 中管理多个服务或 library
  • Library + Application 开发: 同时开发 library 及其使用该 library 的 application
  • 本地开发: 在发布新版本之前测试跨模块的更改

Workspaces 与 replace

在引入 workspaces 之前,开发者通常在 go.mod 中使用 replace 指令指向本地模块路径。虽然 replace 仍然有用,但 workspaces 提供了一种更简洁且更具可扩展性的多模块开发解决方案,而无需修改模块定义。

在下一节中,你将了解更多关于 replace 指令的信息,以及它在特定场景中的使用方式。

使用 replace 指令

go.mod 文件中的 replace 指令允许你覆盖模块的解析位置。这在开发过程中特别有用,当你想使用模块的本地版本或测试更改而无需发布新版本时。

虽然 Go workspaces(go work)现在是本地处理多个模块的首选方式,但 replace 指令仍然在几个重要的场景中被广泛使用。

基本语法

replace 指令将一个模块路径映射到另一个位置:

replace <module-path> => <replacement-path>

替换可以是:

  • 本地目录
  • 不同的模块路径
  • 另一个模块的特定版本

示例 1:使用本地模块

假设你的模块依赖于 github.com/username/mylibrary,但你有一个本地副本想要测试。

在你的 go.mod 文件中添加以下内容:

replace github.com/username/mylibrary => ../mylibrary

现在,Go 将使用本地目录 ../mylibrary,而不是从远程仓库下载模块。

这在以下情况下很有用:

  • 同时开发库和应用程序
  • 在推送或打标签发布前测试更改

示例 2:使用 fork 的依赖

有时你可能需要使用依赖的 fork 版本——例如,应用临时修复或实验更改。

replace github.com/original/library => github.com/yourfork/library v1.2.3

这告诉 Go 使用你的 fork 版本而不是原始模块。

示例 3:在 Monorepo 中固定到本地版本

在 monorepo 设置中,你可能在同一个仓库中有多个模块。你可以使用 replace 指向本地模块路径:

replace github.com/username/project/mylibrary => ./mylibrary

这确保使用本地更改,而无需发布中间版本。

重要注意事项

  • replace 指令仅影响你的本地模块。它不会传递性地应用到依赖你的其他模块。
  • 因此,replace 指令通常仅用于开发目的,并可能在发布模块前移除。
  • 如果你提交了一个指向本地路径的 replace 指令,其他开发者可能会遇到错误,如果该路径在他们的系统上不存在。

在下一节中,你将探索 go get 的行为如何在不同 Go 版本中发生变化,这对于理解现代 Go 中的依赖管理至关重要。

理解不同 Go 版本中 go get 的行为

go get 命令的行为在不同 Go 版本中发生了显著演变,这给使用多个 Go 版本的项目开发者或遵循旧版 Go 发布教程的开发者带来了困惑。理解这些变化对于正确管理依赖关系并避免现代 Go 开发中的常见陷阱至关重要。

旧版行为(Go 1.15 及更早版本)

在 Go 1.15 及更早版本中,go get 同时承担多种职责。一个命令即可下载依赖、更新模块文件并安装可执行二进制文件——所有操作一次性完成。这种设计导致了模糊的行为,常常让开发者感到困惑。

例如,运行以下命令:

  1. go get github.com/spf13/cobra

会同时执行以下几个操作:

  • 下载 cobra 包及其依赖
  • go.mod 文件中添加或更新依赖(如果在模块内)
  • cobra 二进制文件构建并安装到 $GOPATH/bin 目录(如果包包含可执行文件)

这种功能过载在实践中造成了问题。例如,如果你想在项目中使用 cobra 作为库依赖,你可能会无意中也安装了它的二进制文件。相反,如果你只想安装一个 CLI 工具,你仍然会修改项目的依赖关系。这种双重行为使得难以明确表达意图,并导致 go.mod 文件臃肿,包含项目代码实际上并不需要的依赖。

此外,在自动化环境如持续集成流水线中,缺乏明确的版本控制意味着 go get 可能会根据运行时间拉取不同版本,导致构建不可重现。

过渡期(Go 1.16–1.17)

从 Go 1.16 开始,Go 团队开始通过引入更清晰的职责分离来解决这些问题。这一过渡在 Go 1.17 中完成,确立了两个具有特定职责的独立命令:

  • go get:仅管理 go.mod 文件中的依赖(添加、更新或降级版本)
  • go install:仅构建并安装可执行二进制文件到 $GOBIN$GOPATH/bin 目录

这种分离澄清了每个操作的意图。如果你想安装像 cobra 这样的 CLI 工具,现在可以使用:

  1. go install github.com/spf13/cobra-cli@latest

此命令会安装可执行文件,而不会触及项目的 go.mod 文件。相反,如果你想将 cobra 添加为项目的库依赖,可以使用:

  1. go get github.com/spf13/cobra@latest

这一变化立即改善了 Go 开发的多个方面:

  • 更清晰的意图:你使用的命令明确表示是在管理依赖还是安装工具
  • 可重现构建:使用 @version 语法指定版本成为标准实践,确保依赖解析一致
  • 更干净的模块文件:只有实际代码依赖出现在 go.mod 中,而不是你碰巧安装的工具

在过渡期内,Go 工具链会显示弃用警告,帮助开发者调整工作流程。

现代行为(Go 1.21 及更高版本)

在 Go 1.21 及后续版本中,过渡期引入的行为完全标准化。go getgo install 之间的区别现在被严格执行,最佳实践围绕显式版本指定形成了共识。

管理依赖的现代工作流程遵循以下模式:

  1. go get github.com/spf13/cobra@latest
  2. go mod tidy

第一个命令在 go.mod 文件中添加或更新依赖。第二个命令(go mod tidy)执行重要的清理操作:

  • 移除代码中不再引用的依赖
  • 添加已导入但尚未列出的缺失依赖
  • 使用加密校验和更新 go.sum 文件,以确保安全性和可重现性
  • 解析并记录间接依赖(你的依赖的依赖)

运行 go get 时,Go 工具链会智能更新你的 go.mod 文件,通过分析导入语句和依赖图。它根据语义化版本规则和兼容性要求确定合适的版本,然后将此信息记录到模块文件中。

版本指定现在是显式的,在大多数情况下是必需的:

  • @latest:使用最新的标签发布版本
  • @v1.2.3:使用特定的语义化版本
  • @main@master:使用特定分支的最新提交
  • @commit-hash:使用特定提交(适用于未发布补丁)

这些变化代表了 Go 依赖管理系统的重要改进,使构建更可靠、可重现且更容易理解。通过理解 go get 的演变,你可以避免常见陷阱,并在不同项目和 Go 版本中自信地使用 Go 模块。

在下一节中,你将学习如何使用环境变量如 GOPROXYGOPRIVATE 来处理私有模块。

处理私有模块

在许多实际开发场景中,特别是在企业和企业环境中,你需要处理不可公开访问的私有 Go modules。这些可能是托管在私有 GitHub 或 GitLab 仓库中的专有库、企业版本控制系统上的内部包,或者存储在外部访问受限的空气隔离网络中的模块。

默认情况下,Go toolchain 配置为使用公共基础设施来下载和验证模块。具体来说,它使用公共 Go module proxy proxy.golang.org 来获取模块,并使用校验和数据库 sum.golang.org 来验证其完整性。虽然这对公共开源依赖完美工作,但处理私有代码时会产生问题:

  • 隐私问题:私有模块路径会被发送到公共 proxy,可能暴露敏感的项目信息
  • 认证失败:公共 proxy 无法访问需要凭证的私有仓库
  • 校验和验证错误:公共校验和数据库没有私有模块的记录,导致验证失败

要成功处理私有模块,你需要配置特定的环境变量,告诉 Go toolchain 如何区别对待私有依赖和公共依赖。最重要的是两个变量:GOPROXYGOPRIVATE

理解 GOPROXY

GOPROXY 环境变量控制 Go toolchain 在下载依赖时在哪里查找模块。它充当 Go 应该按顺序咨询的模块 proxy 列表。

默认情况下,GOPROXY 设置为:

GOPROXY=https://proxy.golang.org,direct

此配置告诉 Go:

  1. 首先,尝试从 proxy.golang.org(Google 运营的官方公共模块 proxy)下载模块
  2. 如果失败(返回 404 或 410 错误),回退到 direct 模式,这意味着 Go 将使用标准版本控制工具如 git 直接从源仓库获取模块

公共模块 proxy 为公共模块提供了几个好处:

  • 快速下载:模块被缓存并从 Google 的 CDN 基础设施提供服务
  • 可用性:即使原始仓库被删除或移动,模块仍然可用
  • 不可变性:一旦版本被缓存,它永远不会改变,确保可重现性

但是,处理私有模块时,你有几种配置选项:

选项 1:完全禁用所有模块的 proxy

如果你主要处理私有模块或处于无互联网访问的环境中,你可以绕过所有 proxy:

  1. go env -w GOPROXY=direct

这告诉 Go 始终使用版本控制工具直接从源仓库获取模块。虽然这有效,但也意味着你会失去公共依赖的公共 proxy 好处。

选项 2:使用私有模块 proxy

许多组织运行自己的私有模块 proxy(例如 Athens、Artifactory 或 Nexus)来缓存公共和私有模块。你可以配置 Go 使用组织的 proxy:

  1. go env -w GOPROXY=https://proxy.company.internal,https://proxy.golang.org,direct

此配置创建了一个回退链:Go 将首先检查公司 proxy,然后是公共 proxy,最后如果两者都没有模块则直接获取。

通常最好的方法是使用 GOPRIVATE(下一节解释)来指定哪些模块是私有的,同时保留默认的 GOPROXY 配置用于其他所有内容。这样你能兼得两者优点。

GOPRIVATE 环境变量是告诉 Go toolchain 哪些模块路径应被视为私有模块的主要方式。当模块路径匹配 GOPRIVATE 中的模式时,Go 将:

  • 完全跳过模块 proxy,直接从源获取
  • 不检查校验和数据库进行验证
  • 不向公共服务发送有关模块的任何信息

要配置 GOPRIVATE,你提供一个逗号分隔的 glob 模式列表,匹配你的私有模块路径。例如:

  1. go env -w GOPRIVATE=github.com/your-company/*

这告诉 Go,github.com/your-company/ 下的任何模块都应被视为私有。* 通配符匹配任何路径段,因此这覆盖了组织内的所有仓库。

  • 多个模式:你可以指定用逗号分隔的多个模式:

    1. go env -w GOPRIVATE=github.com/your-company/*,gitlab.company.com/*,git.internal.company.com/*
  • 通配符模式:glob 模式支持标准通配符匹配:

    • github.com/your-company/* - 匹配公司 GitHub 组织下的所有仓库
    • *.internal.company.com/* - 匹配任何内部子域名上的所有仓库
    • github.com/your-company/secret-project - 仅匹配特定仓库
  • 相关环境变量:Go 提供了两个额外的变量用于更精细的控制:

    • GONOPROXY:指定永远不使用 proxy 的模块模式(即使设置了 GOPROXY)。默认情况下,它镜像 GOPRIVATE
    • GONOSUMDB:指定应跳过校验和数据库验证的模块模式。默认情况下,它也镜像 GOPRIVATE

在大多数情况下,设置 GOPRIVATE 就足够了,因为它会自动将 GONOPROXYGONOSUMDB 设置为相同值。但是,如果你需要,可以独立覆盖它们:

  1. go env -w GOPRIVATE=github.com/your-company/*
  2. go env -w GONOPROXY=github.com/your-company/*,github.com/partner-company/*
  3. go env -w GONOSUMDB=github.com/your-company/*

何时使用私有模块配置

当你遇到以下任何场景时,都应该配置私有模块设置:

  • 企业开发:处理非开源的专有库和内部工具
  • 客户项目:使用访问受限的客户或合作伙伴仓库
  • 预发布代码:使用尚未公开的未发布或 beta 版本模块进行开发
  • 合规要求:在受监管行业中运行,代码必须保留在内部基础设施上
  • 空气隔离环境:出于安全原因在与公共互联网隔离的网络中工作
  • 自定义 proxy:使用组织工件仓库或模块 proxy(Athens、Artifactory 等)

正确配置 GOPROXYGOPRIVATE 和认证,确保你的 Go module 工作流程保持安全、可靠,并兼容公共和私有依赖。公共和私有模块处理的分离是 Go 的优势之一,允许团队无缝处理混合依赖源,同时保持安全性和可重现性。

常见问题解答

1. go.mod 和 go.sum 有什么区别?

go.mod 文件是主要的模块定义文件,包含模块名称、所需 Go 版本以及直接依赖项及其版本列表。它是人类可读的,如果需要,你可以手动编辑它,尽管 go 工具通常会为你管理它。

另一方面,go.sum 文件包含所有已下载模块版本及其依赖项的加密校验和(SHA-256 哈希)。此文件通过验证在不同环境中使用完全相同的模块版本,并确保无人篡改代码,从而保证完整性和安全性。你绝不应该手动编辑 go.sum——它由 go 工具自动维护。两个文件都应提交到版本控制中。

2. 什么时候应该运行 go mod tidy?

每当你更改代码的依赖项时,都应该运行 go mod tidy。具体来说,在以下情况下运行它:

  • 在代码中添加新的 import 语句后
  • 移除 import 或删除使用依赖项的代码后
  • 运行 go get 添加新依赖项后
  • 在将更改提交到版本控制之前
  • 当你在 go.mod 中看到实际直接使用的依赖项带有 // indirect 注释时

该命令执行两个主要任务:添加代码中已导入但 go.mod 中尚未列出的缺失依赖项,并移除项目中任何地方都不再引用的依赖项。这可以保持你的模块文件干净且准确。

3. 如何使用本地版本的模块而不是互联网上的版本?

主要有两种方法:

选项 1:使用 replace 指令(适用于单个模块)

编辑你的 go.mod 文件并添加 replace 指令:

replace github.com/username/modulename => ../local/path/to/modulename

这告诉 Go 使用你的本地目录而不是从互联网下载。

选项 2:使用 Go workspaces(推荐用于多个模块)

对于 Go 1.18+,workspaces 提供更清晰的解决方案:

  1. go work init
  2. go work use ./your-main-module ./local-dependency-module

这会创建一个 go.work 文件,告诉 Go 使用本地版本,而无需修改你的 go.mod 文件。workspace 配置通常添加到 .gitignore 中,因为它特定于你的本地开发环境。

4. go.mod 中的 indirect dependency 是什么?

间接依赖是你的项目间接依赖的模块——也就是说,它是你直接依赖项之一的依赖,而不是你在代码中直接导入的东西。在 go.mod 中,这些依赖项用 // indirect 注释标记:

require (
    github.com/spf13/cobra v1.7.0
    github.com/spf13/pflag v1.0.5 // indirect // 间接依赖
)

在这个例子中,如果你的代码导入了 cobra,但 cobra 本身依赖于 pflag,那么 pflag 就会作为间接依赖出现。Go 跟踪间接依赖以确保构建的完全可重现性。有时一个依赖项可能会暂时被标记为间接依赖——运行 go mod tidy 将根据你的实际导入正确更新这些标记。

5. 如何将所有依赖项升级到最新版本?

要将所有依赖项升级到最新版本,请使用:

  1. go get -u ./...
  2. go mod tidy

-u 标志告诉 go get 升级到最新的 minor 或 patch 版本。./... 模式表示“应用于当前模块及其子目录中的所有包”。

如果你想升级到最新版本,包括 major 版本更新(可能包含破坏性更改),请使用:

  1. go get -u=patch ./... # 仅 patch 更新(最安全)
  2. go get -u ./... # minor 和 patch 更新
  3. go get module@latest # 特定模块到绝对最新版本

升级后始终运行 go mod tidy 来清理,并彻底测试你的代码,因为新版本可能会引入破坏性更改或 bug。

6. go mod vendor 和使用 module cache 有什么区别?

默认情况下,Go modules 存储在全局缓存中(通常是 $GOPATH/pkg/mod$GOMODCACHE),该缓存在系统上的所有项目间共享。这很高效,因为每个模块版本只下载一次。

go mod vendor 命令在你的项目根目录创建一个 vendor/ 目录,并将所有依赖项复制到其中:

  1. go mod vendor

主要区别:

  • Module cache:全局的、跨项目共享、自动管理、首次下载需要互联网访问
  • Vendor 目录:本地项目专用、自包含、可离线工作、增加仓库大小

当你需要以下功能时使用 vendoring:

  • 对依赖项的完全控制(在企业环境中很有用)
  • 保证离线构建
  • 合规要求需要签入所有代码
  • 防范上游模块消失或被修改

大多数现代 Go 项目使用 module cache,并依赖 go.sum 进行完整性验证,而不是 vendoring。

7. 如何与私有 Git 仓库一起使用 Go modules?

配置 GOPRIVATE 环境变量,告诉 Go 哪些模块是私有的:

  1. go env -w GOPRIVATE=github.com/your-company/*

然后设置认证。最常见的两种方法是:

SSH 认证(推荐):

  1. git config --global url."git@github.com:".insteadOf "https://github.com/"

确保你的 SSH 密钥已添加到 Git 托管提供商。

带 token 的 HTTPS

从你的 Git 提供商创建个人访问 token,并将其添加到 ~/.netrc 文件中:

machine github.com
login your-username
password your_token_here

然后 go get 将正常处理你的私有仓库。GOPRIVATE 设置确保 Go 不为这些模块向公共代理或校验和数据库发送请求,从而维护隐私并避免认证错误。

8. go work 是什么?什么时候应该使用它而不是 replace 指令?

go work(在 Go 1.18 中引入)创建 workspaces,允许你同时处理多个模块。运行 go work init 创建 go.work 文件,然后使用 go work use ./module1 ./module2 添加模块。

在以下情况下使用 workspaces:

  • 你正在同时积极开发多个相关模块
  • 你想在发布前测试跨模块的更改
  • 你正在 monorepo 中处理多个 Go modules
  • 你需要一个临时的本地开发设置,不会影响 go.mod

在以下情况下使用 replace 指令:

  • 你需要永久将模块重定向到 fork
  • 替换应该被提交并与团队共享
  • 你使用的是早于 1.18 的 Go 版本
  • 你希望替换在项目配置中持久存在

关键区别:workspaces 通常仅本地使用(添加到 .gitignore),不会修改你的 go.mod 文件,使其非常适合临时的多模块开发。replace 指令修改 go.mod 并通常被提交,更适合永久重定向或团队范围的配置。

结论

在本教程中,您创建了一个带有子包的 Go 模块,并在模块中使用该包。您添加了远程依赖,学习了如何使用标签、分支和提交来引用特定模块版本,并探索了使用 go getgo mod tidy 的现代依赖管理。您还了解了 go get 在不同 Go 版本中的行为演变,将其与 go install 区分开来,以实现更清晰的意图和可重现的构建。

此外,您探索了高级工作流程,包括用于同时管理多个模块的 Go workspaces、用于本地开发和测试的 replace 指令,以及使用 GOPROXYGOPRIVATE 环境变量配置私有模块访问。这些工具允许您在 GOPATH 之外组织代码,与公共和私有依赖无缝协作,并在不同开发环境中维护安全、可重现的构建。

有关 Go modules 的更多信息,Go 项目有一系列关于 使用 Go modules 以及 Go 工具如何与模块交互和理解模块的博客文章。Go 项目还在 Go Modules Reference 中提供了非常详细的技术参考。

本教程也是 How to Code in Go 系列的一部分。该系列涵盖了许多 Go 主题,从首次安装 Go 到如何使用该语言本身。有关更多 Go 相关教程,请查看以下文章: