模块是一个 Go 包的目录树,其根目录下有一个 go.mod 文件。go.mod 写明模块的名字,并列出它所依赖的其他每个模块的版本。用 go mod init 创建模块:
mkdir myapp
cd myapp
go mod init example.com/myapp
go: creating new go.mod: module example.com/myapp
生成的 go.mod:
module example.com/myapp
go 1.24.5
这就足以构建了。从 Go 1.16 起,每个 Go 项目都是一个模块,没有模块时 go build、go run . 和 go test 都拒绝工作:
go: go.mod file not found in current directory or any parent directory; see 'go help modules'
选择模块路径
模块路径是模块内每个导入路径的前缀。如果模块是 example.com/myapp,那么 internal/store 目录下的包要以 example.com/myapp/internal/store 导入。
| 情况 | 模块路径 |
|---|---|
| 托管在 GitHub、别人可能会导入的代码 | github.com/yourname/project |
| 公司的代码 | yourcompany.com/project 或仓库 URL |
| 私人程序或练习 | example.com/myapp,或者直接写 myapp |
| 已发布模块的 v2 及以后版本 | github.com/yourname/project/v2 |
对于库,路径必须和代码所在的位置一致,因为 go get 要用它来找到仓库。对于只有你自己运行的程序,路径只是一个名字。避免使用和标准库包同名的单个单词,比如 go mod init fmt 或 go mod init strings:这样构建会以 ambiguous import: found package fmt in multiple modules 失败。
在旧的 GOPATH 布局之外,不带参数运行 go mod init 会失败:
go: cannot determine module path for source directory /home/ana/myapp (outside GOPATH, module path must be specified)
给它一个路径就好。
添加依赖
先写 import,再让 Go 去获取。假设 main.go 导入了 github.com/google/uuid。在模块知道这个依赖之前就构建,会得到一条清楚的提示:
main.go:6:2: no required module provides package github.com/google/uuid; to add it:
go get github.com/google/uuid
下面任意一条命令都能解决:
go get github.com/google/uuid
# or, to sync go.mod with every import in the module:
go mod tidy
go get 会报告它改了什么:
go: downloading github.com/google/uuid v1.6.0
go: added github.com/google/uuid v1.6.0
go mod tidy 会报告它是怎样解析这个导入的:
go: finding module for package github.com/google/uuid
go: downloading github.com/google/uuid v1.6.0
go: found github.com/google/uuid in github.com/google/uuid v1.6.0
现在 go.mod 里多了一行 require:
module example.com/myapp
go 1.24.5
require github.com/google/uuid v1.6.0
用 go get 指定版本
| 命令 | 效果 |
|---|---|
go get pkg | 以最新发布版添加 pkg;如果已经依赖它,则保持当前版本 |
go get pkg@v1.5.0 | 精确使用 v1.5.0(升级或降级) |
go get pkg@latest | 升级到最新发布版 |
go get pkg@abc1234 | 使用某个特定提交(记录为伪版本) |
go get -u ./... | 把每个依赖升级到最新的次版本或补丁版本 |
go get -u=patch ./... | 只升级到最新的补丁版本 |
go get pkg@none | 删除这个依赖项 |
go get go@1.24 | 提高模块要求的最低 Go 版本 |
go get 修改的是 go.mod。它不再构建或安装程序;从 Go 1.18 起,这是 go install pkg@version 的工作。
间接依赖
标记为 // indirect 的依赖项,是你的代码没有直接导入、但构建需要的模块。从 Go 1.17 开始,go.mod 会列出为构建提供包的每个模块,所以依赖的依赖也会带着这个标记出现在这里。这些标记由 go mod tidy 管理,不需要你自己添加。
go mod tidy
每当你添加或删除 import 时,都运行一下 go mod tidy。它会:
- 为缺失的已导入包添加依赖项,
- 删除不再被任何代码导入的依赖项,
- 添加构建所需的
go.sum条目,并删掉过时的条目。
它会检查模块中的每个包,包括测试和所有构建标签组合,所以有时会保留一个你在自己的平台上看不到有人使用的依赖。每次提交前运行 go mod tidy,并在 CI 中检查它不会产生任何差异,就能让 go.mod 保持真实准确。
go.sum
go.sum 为构建用到的每个模块版本记录一个哈希:
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
第一行是模块文件的哈希,第二行只是它的 go.mod 的哈希。go 命令下载模块时,会把哈希与 go.sum 以及公共校验和数据库(sum.golang.org)进行比对,所以被篡改或被悄悄修改过的发布版会导致构建失败。把 go.sum 和 go.mod 一起提交,永远不要手动编辑它。
Go 如何选择版本
Go 使用最小版本选择。每个模块都列出它所需的每个依赖的最低版本,构建时使用这些最低版本中最高的那个,绝不会用更新的版本。如果你的模块要求 uuid v1.5.0,而某个依赖要求 uuid v1.6.0,构建就使用 v1.6.0,即使 v1.7.0 已经存在。除非有人用 go get 主动要求,否则什么都不会升级。
这样做的结果是,不需要锁文件,构建也能复现:go.mod 加上依赖图就完全确定了每个版本。go list -m all 会打印出结果:
go list -m all
example.com/myapp
github.com/google/uuid v1.6.0
主版本号
v2 及以上版本的模块必须在路径中包含主版本号:github.com/yourname/project/v2。导入路径也随之改变,所以 project 和 project/v2 是不同的模块,可以同时出现在一次构建中。这是 Go 处理不兼容变更的规则,这也是为什么你会看到 github.com/jackc/pgx/v5 这样的导入。
replace:在本地修改依赖
要在发布之前测试对某个依赖的修改,可以把它的路径指向一个本地目录:
module example.com/myapp
go 1.24.5
require example.com/mylib v1.2.0
replace example.com/mylib => ../mylib
或者在命令行中执行:
go mod edit -replace example.com/mylib=../mylib
go mod tidy
这个目录必须包含它自己的 go.mod。replace 只在直接构建当前模块时生效,别人依赖这个模块时不会生效,所以记得在打发布标签之前删掉它。
如果要同时修改多个模块,又不想动它们的 go.mod 文件,可以使用 Go 1.18 新增的工作区:
go work init . ../mylib
这会写入一个 go.work 文件,让本地的 mylib 优先生效。除非整个团队都使用相同的目录布局,否则不要把 go.work 纳入版本控制。
工具依赖(Go 1.24)
Go 1.24 新增了 tool 指令,这样代码生成器和 linter 就可以在 go.mod 中管理版本,而不用全局安装:
go get -tool golang.org/x/tools/cmd/stringer
go tool stringer -type=Color
func init 是另一回事
搜索“golang init”的人,往往指的是 init 函数,它和 go mod init 毫无关系。任何包都可以声明 func init()。它不接收参数,不能被你的代码调用,会在包级变量设置完成之后、main 之前自动运行一次:
一个文件可以有多个 init 函数,它们按出现的顺序运行。被导入的包会先完成自己的初始化。让 init 保持精简:可能失败的工作,写成从 main 调用的普通函数,会更容易处理和测试。
私有模块与代理
默认情况下,go 通过 proxy.golang.org 下载模块。这个代理看不到私有仓库,所以要告诉 Go 哪些路径是私有的:
go env -w GOPRIVATE=github.com/yourcompany/*
匹配 GOPRIVATE 的模块会使用你的 git 凭据直接从仓库获取,并跳过校验和数据库。
常见问题
go mod init 是做什么的?
它在当前目录下创建一个 go.mod 文件,让这个目录成为一个模块的根目录。go mod init example.com/myapp 会写入模块路径和 Go 版本:
module example.com/myapp
go 1.24.5
每个项目运行一次即可,要在 go build 或 go get 之前运行。
go mod init 的模块路径应该写什么?
写别人导入这段代码时要从哪里获取它的地址,通常就是仓库路径:go mod init github.com/yourname/project。对于没人会导入的程序,任何名字都可以(go mod init myapp),但像 example.com/myapp 这样带点的域名式路径,可以避免和标准库的包名冲突。
go get 和 go mod tidy 有什么区别?
go get pkg@version 添加一个依赖或改变它的版本。go mod tidy 会读取你的源文件,添加导入所需但 go.mod 中缺少的模块,删除没有任何代码导入的依赖项,并更新 go.sum。常见的流程是先写好 import,再运行 go mod tidy。
go.sum 要提交吗?
要。go.sum 保存着构建用到的每个模块版本的加密哈希。提交它,go 命令就能验证每个人下载到的依赖都是逐字节相同的。永远不要手动编辑它,交给 go mod tidy 维护。
“golang init”和 go mod init 是一回事吗?
不是,它们只是名字里都有 init。go mod init 是一个用来创建 go.mod 的终端命令。func init() 是你在 Go 代码里写的函数;它会在包的变量初始化之后、main 之前自动运行一次。