Skip to content

Go Modules:依赖管理、版本选择与工程实践

面向有 Maven/Gradle 使用经验的 Java 开发者,基于 Go 1.26。

Go Modules 通常被翻译成“Go 模块”,但不要直接把它套进 Java 9 的 JPMS module 或 Maven module。它首先是 Go 的发布、版本化和依赖解析单元:一个 module 可以容纳许多 package,并由 go.mod 记录路径、最低 Go 版本和依赖要求。

只会运行 go mod initgo mod tidy,确实足以让小项目跑起来。等到多人协作、私有仓库、多 module 联调或依赖冲突出现时,go.mod 中的每一次变化都需要解释。这一章要解决的就是这个问题:依赖为什么变化、该用什么命令验证,以及哪些文件必须提交。

目录

1. 先建立三个概念

1.1 Repository

repository 指 Git 等版本控制系统中的仓库。它可以包含:

  • 一个 module;
  • 多个 module;
  • 完全没有 Go module;
  • Go、Java、前端等多种工程。

repository 是源码管理边界,不必与 Go 的版本边界一一对应。

1.2 Module

module 是一组一起发布、一起选择版本的 package。它的根目录中有一个 go.mod

text
myproject/
├── go.mod
├── go.sum
├── main.go
├── config/
│   └── config.go
└── internal/
    └── service/
        └── service.go

整个目录树属于同一个 module,除非某个子目录又出现了一个 go.mod。嵌套的 go.mod 会切出新的 module 边界,父 module 不再包含这个子 module。

1.3 Package

package 是编译和 import 的基本单元。通常,一个目录中的 .go 文件共同组成一个 package:

text
config/
├── load.go
├── parse.go
└── config_test.go

这三个文件通常都声明:

go
package config

关系可以概括为:

text
repository
└── module(go.mod,发布和版本单元)
    ├── package A(目录,编译和 import 单元)
    ├── package B
    └── package C

不要给每个 package 都创建 go.mod。大多数应用仓库使用一个 module 就够了;拆成多个 module 意味着独立版本、跨 module 联调和发布顺序,过早引入这些边界只会增加维护成本。

2. 创建第一个 module

2.1 初始化

在项目根目录执行:

bash
go mod init example.com/acme/order-service

生成:

go
module example.com/acme/order-service

go 1.25.0

这里的 module path 是代码的逻辑身份。代码准备发布到 GitHub 时,通常使用仓库地址:

bash
go mod init github.com/yourname/order-service

若只是本地练习,可以使用保留给示例的路径:

bash
go mod init example/order-service

本项目当前写的是:

go
module go-study

go 1.26

go-study 对只在本地构建的主 module 没问题;如果以后要让其他项目通过网络依赖它,应该改成能定位仓库的稳定路径,例如 github.com/owner/go-study

2.2 为什么 Go 1.26 执行 init 可能生成 go 1.25.0

从 Go 1.26 开始,正式版本的 go mod init 默认把新 module 的 go 指令设为前一个 Go 语言版本。这是为了鼓励新 module 默认兼容当前仍受支持的工具链,而不是无意中把最低版本提高到本机最新版。

如果代码确实需要 Go 1.26,可以显式更新:

bash
go get go@1.26

或者:

bash
go mod edit -go=1.26

最低版本取决于代码实际使用的语言和标准库能力,不能简单照抄开发者电脑上的 go version

2.3 module 可以放在哪里

Module 模式下,项目不需要放到 $GOPATH/src。可以放在任意普通目录:

text
/work/order-service
D:\code\order-service

GOPATH 仍然存在,但在现代 module 模式中,它主要承载:

  • module 下载缓存,默认是 $GOPATH/pkg/mod
  • 通过 go install 安装的可执行文件,默认是 $GOPATH/bin

不要再按早期教程把所有源码强行放进 $GOPATH/src

3. module path 与 import path

3.1 import path 的计算

一个 package 的 import path 等于:

text
module path + package 相对 module 根目录的路径

例如:

go
module github.com/acme/order-service

目录:

text
order-service/
├── go.mod
├── main.go
├── config/
│   └── config.go
└── internal/
    └── pricing/
        └── pricing.go

对应 import:

go
import (
	"github.com/acme/order-service/config"
	"github.com/acme/order-service/internal/pricing"
)

module 根目录本身也是一个 package,其 import path 就是 module path。不过应用根目录通常是 package main,外部代码一般不会 import 一个可执行程序。

3.2 package name 不等于 import path

go
import "github.com/google/go-cmp/cmp"

代码中使用的是最后声明的 package name:

go
cmp.Equal(a, b)

目录名、import path 最后一段和 package name 通常保持一致,但语言并不强制三者完全相同。若不同,阅读成本会明显上升,应有充分理由。

发生名称冲突时可以在当前文件设置 import 别名:

go
import (
	htmltemplate "html/template"
	texttemplate "text/template"
)

别名只是当前文件中的局部名字,不会改变 module path 或 package 的真实身份。

3.3 import 的是 package,不是 module

这是初学 Modules 时最常见的概念混淆:

go
import "go.uber.org/zap"

源码 import 的是 go.uber.org/zap package;go.modrequire 记录提供这个 package 的 module 及其版本。很多时候 package path 恰好等于 module path,但不是永远如此:

go
import "golang.org/x/text/language"

这里 package 是 golang.org/x/text/language,提供它的 module 是 golang.org/x/text

因此,不能只看 import 字符串最后一段就判断依赖 module。

4. 读懂 go.mod

一个较完整的 go.mod 可能是:

go
module github.com/acme/order-service

go 1.25.0

toolchain go1.26.5

require (
	github.com/google/uuid v1.6.0
	go.uber.org/zap v1.28.0
	golang.org/x/text v0.38.0 // indirect
)

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

replace example.com/legacy => ./third_party/legacy

exclude example.com/broken v1.4.0

retract v1.2.0

不要把 go.mod 简单理解成 Maven pom.xml。它同时表达 module 身份、最低工具链要求、依赖图的最低版本约束,以及只对当前主 module 生效的图变换规则。

4.1 module

go
module github.com/acme/order-service

声明当前 module 的唯一逻辑路径。它决定本 module 内各 package 的 import path。

module path 改名是一项 API 迁移:本仓库所有内部 import、其他仓库的 import 和依赖声明都可能需要修改。不要因为本地目录改名就随便修改 module path。

4.2 go

go
go 1.25.0

它不是“作者电脑安装了哪个 Go”的备注,而是:

  • 使用这个 module 所需的最低 Go 版本;
  • 决定该 module 源码可使用的语言版本;
  • 影响部分 go 命令和标准库的兼容行为;
  • 参与工具链选择。

一个 module 的 go 版本不能低于它直接要求的 module 所声明的 go 版本。升级依赖后看到 go 行被提高,可能是新依赖提高了最低 Go 要求。

库作者应尽量把它设为自己真正测试并支持的最低版本。应用项目则可以跟随团队统一的生产工具链。

4.3 toolchain

go
toolchain go1.26.5

go 指令是最低要求,toolchain 指令是主 module 或 workspace 建议使用的具体工具链。默认 GOTOOLCHAIN=auto 时,本机 go 命令可能从 PATH 查找或自动下载合适的新工具链。

两者可以表达:

go
go 1.24.0
toolchain go1.26.5

含义是代码最低兼容 Go 1.24,但本 module 的日常开发建议使用 Go 1.26.5。

CI 若禁止自动下载工具链,可以设置:

bash
GOTOOLCHAIN=local go test ./...

这时本机工具链不满足 go 指令会直接失败。是否允许自动切换,应由团队和构建环境明确决定。

4.4 require

go
require go.uber.org/zap v1.28.0

它表达当前 module 对 go.uber.org/zap最低版本要求。最终构建用哪个版本,还要经过整个依赖图的 MVS 计算。

require 不是 Maven 风格的版本范围,也不是“只能使用恰好这个版本”的 lock。大部分时间,MVS 恰好会选到这里写的版本;如果另一个依赖要求更高版本,最终构建会使用更高版本。

4.5 // indirect

go
require golang.org/x/text v0.38.0 // indirect

这不是普通注释,而是 go 命令维护的标记,表示当前 module 的源码没有直接 import 这个 module 提供的 package,但依赖图仍需要记录它。常见原因包括:

  • 它是某个直接依赖的传递依赖;
  • 它只被测试代码使用;
  • module graph pruning 需要显式记录;
  • 提供它的 module 版本高于其他依赖给出的最低要求;
  • 工具依赖需要它。

不要手工删除所有 // indirect 来追求“干净”。使用 go mod tidy 让工具按源码和 module 图计算。

4.6 replace

go
replace example.com/lib => ../lib

把依赖的内容替换为本地目录或另一个 module 版本。它只在当前 module 是 main module 时生效;你的下游用户依赖你时,不会继承你写的 replace

4.7 exclude

go
exclude example.com/lib v1.4.0

让 MVS 不选择指定坏版本。它也只对 main module 生效。

4.8 retract

go
retract (
	v1.2.0 // 包含严重 bug
	v1.3.0-pre
)

由 module 作者声明某些已发布版本不应再使用。retract 不会删除版本,也不会强制已经固定该版本的项目自动升级;它主要影响版本查询和升级建议。

4.9 tool

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

Go 1.24 起,tool 指令把 Go 编写的开发工具纳入当前 module 的依赖管理,并允许:

bash
go tool stringer

工具所属 module 仍需要相应 require 版本。通常用 go get -tool 自动维护。

4.10 godebug

go
godebug http2client=0

godebug 为主 module 设置特定的兼容行为。它不是普通业务配置,只用于 Go 运行时或标准库定义的 GODEBUG 设置。进入 workspace 模式后,主 module 的 godebug 会被 go.work 中的设置取代。

5. go.sum 是什么

5.1 它保存的是内容校验值

典型内容:

text
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8...
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4Mgqv...

一行可能校验整个 module zip,另一行只校验该版本的 go.mod。Go 下载依赖时会计算哈希,与 go.sum 和公共 checksum database 中的记录核对,发现同一路径同一版本内容被篡改时停止构建。

5.2 go.sum 不是 lock file

它不负责选择版本,也不表示其中每一行都参与当前构建。旧版本、仅下载过的 go.mod、已经不在构建列表里的版本都可能暂时留在 go.sum

版本选择主要由:

  • go.mod 中的要求;
  • 依赖 module 的 go.mod
  • MVS;
  • main module / workspace 的 replace 和 exclude

共同决定。

5.3 必须提交吗

应用和库通常都应该提交 go.modgo.sum

text
go.mod  —— 依赖要求和 module 配置
go.sum  —— 已知依赖内容的完整性记录

不要因为 go.sum 很长就加入 .gitignore。它不是本机缓存文件。

5.4 为什么同事执行命令会修改 go.sum

可能原因包括:

  • 新代码 import 了新 package;
  • 测试、工具或特定平台文件引入新依赖;
  • 第一次需要某个 module zip 或某个版本的 go.mod 哈希;
  • Go 版本对 module 图的加载方式不同;
  • 之前没有运行完整的 go mod tidy

看到变化时先读 diff,再运行与 CI 一致的 Go 版本和命令,不要盲目回滚。

6. 添加、升级、降级和移除依赖

6.1 最自然的流程:先写 import,再 tidy

先在代码中使用 package:

go
import "github.com/google/uuid"

然后:

bash
go mod tidy

tidy 会:

  • 添加源码和测试所需但缺失的 module;
  • 移除不再需要的 require;
  • 更新必要的 go.sum 条目;
  • 处理不同 build tag 和平台下可能构建的 package。

它不会只分析你当前操作系统的单一路径,因此有些你本机没执行到的条件编译依赖也会被保留。

6.2 添加指定版本

bash
go get github.com/google/uuid@v1.6.0

添加最新稳定版本:

bash
go get github.com/google/uuid@latest

go get 修改当前 module 的依赖配置。它不是单纯“下载到本机”,也不再用来安装普通命令行工具。

6.3 查看可用版本

bash
go list -m -versions github.com/google/uuid

查看当前版本和可用更新:

bash
go list -m -u github.com/google/uuid
go list -m -u all

-u all 适合发现更新,不代表应该一次性升级全部依赖。升级前应阅读 release notes,升级后运行测试和漏洞扫描。

6.4 升级与降级

bash
go get example.com/lib@v1.8.2
go get example.com/lib@v1.7.9

同一条命令既能升级也能降级。MVS 为保持依赖图一致,可能连带调整其他 module,所以每次都应检查:

bash
git diff -- go.mod go.sum
go test ./...

没有 Git 时,也至少执行:

bash
go list -m all
go mod graph

6.5 移除依赖

通常先删除源码 import,再运行:

bash
go mod tidy

显式要求移除某个 module:

bash
go get example.com/lib@none

如果仍有其他依赖需要它,Go 可能无法完全移除,或者会连带降级依赖图。用 go mod why -m 查明原因。

6.6 安装可执行工具

临时或全局安装某个明确版本:

bash
go install golang.org/x/vuln/cmd/govulncheck@latest

固定当前项目使用的工具版本则优先使用 Go 1.24+ 的工具依赖:

bash
go get -tool golang.org/x/tools/cmd/stringer@v0.40.0
go tool stringer

记忆方式:

  • go get:管理当前 module 的依赖;
  • go install command@version:安装独立命令,不修改当前 go.mod
  • go get -tool + go tool:管理并运行项目级工具。

7. go mod 常用命令

7.1 go mod init

bash
go mod init github.com/acme/order-service

创建 go.mod。一个 module 只需初始化一次。

7.2 go mod tidy

bash
go mod tidy

go.modgo.sum 与源码需要的 package 对齐。

只查看会发生什么,不写文件:

bash
go mod tidy -diff

CI 中可以用它检查开发者是否忘了 tidy。

本仓库当前执行 go mod tidy -diff 会显示:fasthttpzap 及其间接依赖都可移除,因为现有 .go 源码并未 import 它们。本文只记录这个事实,不替第二章擅自修改第一章项目依赖。

7.3 go mod download

bash
go mod download
go mod download -json
go mod download example.com/lib@v1.2.3

显式把 module 下载到缓存。正常的 go buildgo test 会按需下载,因此开发者日常不必每次手工执行。CI 缓存预热、离线镜像制作或诊断下载信息时更有用。

7.4 go mod verify

bash
go mod verify

验证 module cache 中已下载的 zip 和解压目录是否与下载时记录的哈希一致。它检查的是本地缓存有没有被修改,不等同于“重新扫描所有依赖漏洞”,也不代替 go.sum 的正常校验流程。

7.5 go mod graph

bash
go mod graph

输出 module requirement graph,每行是:

text
依赖方@版本 被依赖方@版本

例如本项目的图中有:

text
go-study github.com/valyala/fasthttp@v1.72.0
github.com/valyala/fasthttp@v1.72.0 github.com/andybalholm/brotli@v1.2.1

这说明 brotli 是通过 fasthttp 进入 requirement graph 的,但是否真正进入某个二进制,还要看实际 package import 和链接结果。

7.6 go mod why

bash
go mod why -m github.com/andybalholm/brotli

显示从主 module 到目标 module 的最短 package import 路径。若输出:

text
(main module does not need module ...)

说明当前源码和测试不需要它,go.mod 可能尚未 tidy。

7.7 go mod vendor

bash
go mod vendor

重建 vendor 目录。它会先清理并重新生成,不要在 vendor 中手工修代码。

7.8 go mod edit

适合脚本化修改:

bash
go mod edit -go=1.25.0
go mod edit -require=example.com/lib@v1.2.3
go mod edit -replace=example.com/lib@v1.2.3=../lib
go mod edit -dropreplace=example.com/lib@v1.2.3
go mod edit -json

人可以直接编辑 go.mod,但自动化脚本优先 go mod edit,避免自己实现语法解析。

7.9 不是 go mod 子命令,但很有用

bash
go list -m all
go list -m -u all
go list -m -json all
go list -deps ./...
go env GOMOD GOWORK GOPROXY GOSUMDB GOPRIVATE
go version -m ./your-binary

go version -m 可以查看 Go 二进制中记录的 module、依赖版本、替换信息和构建设置,是排查“线上这个二进制到底用了什么”的重要手段。

8. 直接依赖、间接依赖和依赖图裁剪

8.1 direct 并不等于手工写入

当前 module 的 package 直接 import 某个外部 module 提供的 package,这个 module 通常是直接依赖:

go
import "go.uber.org/zap"

对应:

go
require go.uber.org/zap v1.28.0

即使某个库最初是被另一个库传递引入,只要你的源码开始直接 import 它,它就应成为 direct requirement。

8.2 indirect 也不是“不重要”

间接依赖依然会影响:

  • 构建结果;
  • 安全漏洞;
  • 许可证;
  • 最低 Go 版本;
  • 二进制体积;
  • 依赖冲突。

// indirect 只描述依赖关系,不描述风险等级。

8.3 为什么 go.mod 会列出很多间接依赖

Go 1.17 起启用更完整的 module graph pruning。go.mod 会显式记录构建和测试需要的相关 module,以便命令不必每次加载整个传递依赖图。

这是一种性能和可重复性设计,不是依赖“泄漏”。不要用 Maven 对 pom.xml 长度的直觉评价 go.mod

8.4 package graph 与 module graph

这两张图不同:

  • package graph:哪个 package import 哪个 package;
  • module graph:哪个 module 至少要求哪个 module 版本。

go mod graph 看 module graph;go mod why 虽然可接受 -m,解释路径时仍基于 package import graph。某个 module 出现在 requirement graph,不代表其中代码一定链接进最终二进制。

9. MVS:Go 如何选择版本

9.1 Minimal Version Selection 的准确含义

假设:

text
主 module 需要 A v1.2.0
主 module 需要 B v1.0.0
A v1.2.0 需要 C v1.4.0
B v1.0.0 需要 C v1.7.0

最终选择:

text
C v1.7.0

MVS 对每个 module path 选择依赖图中要求的最高最低版本。它叫“Minimal”,是因为不会为了追新而自动选择仓库中更高的 C v1.9.0;它只选满足所有最低要求所需的最低版本。

这句话很重要:

MVS 不是选择全网最低版本,也不是像 Maven 那样按依赖声明遍历顺序做 nearest-wins;它选择依赖图要求中的最高版本。

9.2 没有常规版本范围求解

go.mod 通常不会写:

text
[1.2, 2.0)
^1.2.3
latest.release

它写一个最小版本:

go
require example.com/c v1.7.0

Go 依赖 module 在同一 major 版本内保持向后兼容。若 v1.7.0 不能替代 v1.4.0,那通常是依赖作者破坏了语义化版本兼容承诺。

9.3 升级为什么可能连带变化

升级 A 后,A 的 go.mod 可能提高 C 的最低版本,于是整个 build list 的 C 也上升。降级或移除时,为维持图的一致性,其他 module 也可能被降级或移除。

操作后检查:

bash
go list -m all
go mod graph
go test ./...

不要只盯着你主动修改的那一行。

9.4 同一路径通常只有一个版本

一个 build list 中,同一个 module path 通常只选择一个版本。这避免了 Java classpath 上同名 class 来自多个 jar 的混乱。

不同 major 版本因为 module path 不同,可以共存:

text
example.com/lib
example.com/lib/v2

从 Go 看,这是两个不同 module。

10. 语义化版本与 v2+ 路径

10.1 基本格式

text
vMAJOR.MINOR.PATCH

例如:

text
v1.4.2
v2.0.0
v1.5.0-beta.1

通常约定:

  • PATCH:向后兼容的 bug 修复;
  • MINOR:向后兼容的新能力;
  • MAJOR:不兼容变更;
  • v0:尚未承诺稳定兼容;
  • 预发布版本:低于对应正式版本。

Go module 版本必须带 v 前缀。

10.2 Semantic Import Versioning

从 v2 开始,major 版本必须进入 module path:

go
module example.com/lib/v2

用户 import:

go
import "example.com/lib/v2/client"

发布标签:

text
v2.0.0

这样 v1 和 v2 可以同时被同一个程序使用,类型和 API 不会在同一个 import path 下突然改变。

例外主要是 gopkg.in 等具有等价版本语义的历史路径,以及旧的 +incompatible module。新项目按 /v2 规则处理。

10.3 v2 代码放哪里

常见两种方案:

  1. 新分支或仓库根目录,go.mod/v2
  2. 同一分支的 /v2 子目录中放新的 go.mod

第二种可以在一个分支同时维护 v1 和 v2,但会形成嵌套 module。选择哪种取决于维护策略,不要同时采用多套含糊布局。

10.4 子目录 module 的 tag

若 module 位于 repository 的子目录:

text
repo/
└── tools/
    └── go.mod   // module example.com/repo/tools

它的版本 tag 需要带子目录前缀:

text
tools/v1.2.3

否则 Go 无法把 repository tag 正确映射到这个 module。

10.5 tag 发布后不要移动

Go proxy 和 checksum database 把 module 版本当作不可变内容。删除 tag 或把同一 tag 移到另一个 commit,不会可靠地“覆盖发布”,反而会让已缓存内容与新内容哈希冲突。

发布错了,应:

  1. 保留原 tag;
  2. 发布修复版本;
  3. 必要时在新版本的 go.mod 中 retract 错误版本。

11. 伪版本与版本查询

11.1 伪版本

依赖一个没有语义化 tag 的 commit 时,Go 会生成:

text
v0.0.0-20260725083000-abcdef123456

或以最近 tag 为基础的形式:

text
v1.4.3-0.20260725083000-abcdef123456

它编码了:

  • 版本排序基础;
  • UTC 时间戳;
  • commit 哈希前缀。

不要手工编造伪版本。使用:

bash
go get example.com/lib@abcdef123456

让 Go 根据 repository 历史生成合法值。

11.2 常用版本查询

bash
go get example.com/lib@latest
go get example.com/lib@v1.4.2
go get example.com/lib@master
go get example.com/lib@abcdef123456
go get example.com/lib@none

进入 go.mod 时,branch 和 commit 会被规范化为语义版本或伪版本。不要把 master 直接手写进 go.mod

11.3 为什么长期依赖 commit 不理想

伪版本可以精确复现,但长期大量使用会带来:

  • release notes 和兼容性语义不清晰;
  • 自动升级工具难以判断风险;
  • repository 历史变更后更难维护;
  • 依赖作者可能从未把该 commit 当作正式发布。

临时验证修复可以用 commit;稳定依赖最好推动上游发布正式 tag。

12. replaceexcluderetract

12.1 本地联调

go
require example.com/lib v1.2.3

replace example.com/lib => ../lib

右侧本地目录必须是一个 module 根目录,通常要有自己的 go.mod,并且 module path 与被替换 module 的预期身份一致。

常用命令:

bash
go mod edit -replace=example.com/lib=../lib

12.2 替换为 fork

go
require example.com/lib v1.2.3

replace example.com/lib => github.com/acme/lib-fork v1.2.3-fix.1

业务代码仍然:

go
import "example.com/lib/client"

业务代码不要跟着改成 fork 路径。replace 要保留原 module 身份,只替换内容来源。

12.3 replace 不会传递

库 A 的 go.mod

go
replace example.com/lib => ../lib

应用 B 依赖 A 时,这条 replace 不生效。只有构建命令所在的 main module 或当前 go.work 中的 replace 参与解析。

这也意味着,准备发布的库不能依靠本地 replace 才能构建。发布前,每个依赖都必须能通过正常的 module path 和版本获取。

12.4 replace 不能单独引入依赖

只有:

go
replace example.com/lib => ../lib

但依赖图没有 require example.com/lib ...,replace 不会凭空把它加入构建。通常由源码 import 后 go mod tidy 添加 require,或者显式:

go
require example.com/lib v0.0.0

12.5 exclude 的使用边界

go
exclude example.com/lib v1.4.0

exclude 可以阻止 MVS 选中已知损坏的版本,但下游不会继承它。module 作者应尽快发布修复版本并 retract 坏版本,不能期待每个用户都手工添加 exclude。

12.6 retract 的正确发布方式

要 retract v1.2.0

  1. 在准备发布的新版本中修改 go.mod
  2. 加入 retract v1.2.0 和原因注释;
  3. 发布更高版本,例如 v1.2.1

查询 @latest 时,Go 读取最高版本的 go.mod,从中得知 retraction。不要只在未打 tag 的主分支写一行 retract 就认为用户已经收到。

13. 多 module 联调:Go Workspace

13.1 解决什么问题

假设本地同时开发:

text
workspace/
├── order-service/
│   └── go.mod
└── pricing-sdk/
    └── go.mod

过去常在 order-service/go.mod 中写临时 replace。Go Workspace 允许在上层创建 go.work,把两个 module 都当作 main modules:

bash
cd workspace
go work init ./order-service ./pricing-sdk

生成:

go
go 1.26

use (
	./order-service
	./pricing-sdk
)

之后在 workspace 范围内构建 order-service,会直接使用本地 pricing-sdk

13.2 常用命令

bash
go work init ./module-a ./module-b
go work use ./module-c
go work use -r .
go work edit -replace=example.com/lib=../lib
go work sync

查看当前是否进入 workspace:

bash
go env GOWORK

临时禁用:

bash
GOWORK=off go test ./...

13.3 go.work.sum

workspace 可能生成 go.work.sum,补充各成员 module 的 go.sum 尚未记录的校验值。它和 go.sum 一样不是版本选择文件。

13.4 是否提交 go.work

分两种情况:

  • 只是开发者个人在相邻目录联调:通常不提交,避免把个人目录布局带给全团队。
  • repository 官方就是多 module monorepo,并且团队统一从 workspace 根目录工作:可以提交。

是否提交没有统一答案,要看仓库有没有把 workspace 定义为正式构建入口。即便提交,CI 也最好验证每个 module 在 GOWORK=off 下能够独立构建,避免依赖只能由本地 workspace 补齐。

13.5 workspace 不是发布单元

go.work 不会跟随某个 module 发布给下游。每个成员 module 的 go.mod 仍要独立且合法,并且能够解析所有已发布依赖。

14. 工具依赖

14.1 Go 1.24+ 推荐方式

添加工具:

bash
go get -tool golang.org/x/tools/cmd/stringer@v0.40.0

go.mod 会出现:

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

require golang.org/x/tools v0.40.0

运行:

bash
go tool stringer -type=Status

查看所有当前可用工具:

bash
go tool

升级全部工具:

bash
go get tool

在 CI 安装所有工具到 $GOBIN

bash
go install tool

14.2 为什么不总是 go install ...@latest

@latest 适合个人临时安装,但 CI 每次得到的版本可能不同。格式化器、代码生成器和 linter 的版本变化可能直接改变提交内容或检查结果。

项目级工具应固定版本并提交 go.mod/go.sum。这样本地和 CI 运行的是同一个工具依赖图。

14.3 工具也参与 MVS

tool 所属 module 及其依赖与业务依赖共享 module graph。一个工具可能抬高某个公共 module 的版本。遇到版本变化时,可用:

bash
go mod graph
go mod why -m example.com/dependency

确认它是业务代码、测试还是工具引入的。

15. 私有 module

15.1 GOPRIVATE

假设公司 module path 统一以 corp.example.com 开头:

bash
go env -w GOPRIVATE=corp.example.com

或只对当前命令设置:

bash
GOPRIVATE=corp.example.com go mod download

GOPRIVATE 告诉 Go:

  • 这些 module 是私有的;
  • 默认不要把其路径发送给公共 proxy;
  • 默认不要向公共 checksum database 查询;
  • 它也是 GONOPROXYGONOSUMDB 的默认值。

这是隐私边界配置,不只是“解决 404 的开关”。在第一次拉取私有依赖前就应设置,避免私有 module path 泄露给公共服务。

15.2 只绕过 checksum 或只绕过 proxy

更细粒度配置:

bash
go env -w GONOSUMDB=corp.example.com
go env -w GONOPROXY=corp.example.com
  • GONOSUMDB:这些 module 不使用公共 checksum database;
  • GONOPROXY:这些 module 直接访问版本库;
  • GOPRIVATE:同时为两者提供默认匹配。

有内部 module proxy 时,可能希望私有依赖仍走内部 proxy,只跳过公共 sumdb。这时应明确配置 GONOPROXYGONOSUMDB,而不是机械照抄一套环境变量。

15.3 认证交给 VCS

直接拉取私有 Git module 时,Go 会调用 Git。认证通常通过:

  • SSH key;
  • Git credential helper;
  • .netrc
  • 企业统一身份方案;
  • Git URL rewrite。

不要把 token 写进 go.mod、源码 import path、提交的 shell 脚本或明文 proxy URL。

Go 默认关闭 Git 的交互式终端提示。CI 必须预先配置非交互凭证,否则命令往往直接报认证失败。

15.4 vanity import path

公司可以使用:

text
corp.example.com/team/lib

域名服务器通过 ?go-get=1 返回 go-import meta tag,把稳定 module path 映射到真实 Git repository。这样迁移 Git 托管平台时,业务 import path 不必跟着改变。

Go 1.25+ 的 meta tag 还支持显式 repository subdirectory,但如果要兼容更老工具链,应验证其解析行为。

15.5 私有 proxy

全量内部 proxy:

text
GOPROXY=https://proxy.corp.example.com
GONOSUMDB=corp.example.com

只代理私有 module,再回退公共源:

text
GOPROXY=https://proxy.corp.example.com,https://proxy.golang.org,direct
GONOSUMDB=corp.example.com

逗号分隔时,通常只有前一个 proxy 返回 404 或 410 才继续后退;403、500 等会停止。这允许企业 proxy 用 403 阻止不合规依赖。竖线 | 分隔则允许在更多错误情况下继续回退,配置前要理解安全影响。

16. 代理、校验数据库与缓存

16.1 下载链路

公共 module 的典型流程:

text
源码 import

解析 module path 与版本

GOPROXY 下载 .mod / .info / .zip

go.sum + GOSUMDB 校验

解压到 GOMODCACHE

编译所需 package

proxy 并不是简单的 HTTP 加速器。它还提供不可变版本缓存,避免构建完全依赖上游 Git 仓库当时是否在线。

16.2 GOPROXY

查看:

bash
go env GOPROXY

常见值:

text
https://proxy.golang.org,direct

direct 表示直接访问版本控制仓库。off 表示不允许下载。代理列表顺序和分隔符决定回退条件。

16.3 GOSUMDB

查看:

bash
go env GOSUMDB

公共 checksum database 为 module 版本内容提供全局可验证记录。把它设为 off 会放弃一层供应链完整性保护,不应作为普通网络故障的第一解决方案。

私有 module 应通过 GOPRIVATEGONOSUMDB 精确排除,而不是全局关闭。

16.4 module cache

默认位置:

bash
go env GOMODCACHE

通常是:

text
$GOPATH/pkg/mod

特点:

  • 多个项目共享;
  • 可并发访问;
  • 默认没有自动大小上限;
  • 解压源码通常是只读的;
  • 与编译产物的 build cache 不是同一个缓存。

不要进入 cache 直接改依赖源码。需要调试修改时,clone 到普通目录并使用 workspace 或 replace。

清空:

bash
go clean -modcache

不要用粗暴的递归删除命令处理只读 cache。清空后所有项目都需要重新下载依赖。

16.5 GOVCS

GOVCS 控制 direct 模式允许调用哪些版本控制工具:

text
GOVCS=public:git|hg,private:all

可以进一步收紧:

text
GOVCS=github.com:git,*:off

它只约束直接 VCS 下载;通过 module proxy 获取内容时走的是 GOPROXY 协议。

16.6 GOINSECURE

GOINSECURE 允许特定 module 使用不安全的 HTTP 等方式直接获取。它不等同于 GONOSUMDB,也不应成为企业证书配置错误的长期绕过方案。

17. vendor 与离线构建

17.1 生成 vendor

bash
go mod vendor

生成:

text
vendor/
├── modules.txt
└── 各依赖 package 源码

它只复制构建和测试当前 main module 所需的 package,不会简单复制所有 module 的全部内容。

17.2 何时自动使用

当 module 的 go 版本至少为 1.14,并且根目录存在一致的 vendor 时,支持 -mod 的构建命令通常自动使用 vendor。

显式指定:

bash
go test -mod=vendor ./...
go build -mod=vendor ./...

忽略 vendor,使用 module cache:

bash
go test -mod=mod ./...

禁止隐式修改 go.mod

bash
go test -mod=readonly ./...

17.3 要不要提交 vendor

适合提交的情况:

  • 构建环境严格离线;
  • 供应链政策要求依赖源码进入本仓库审计;
  • 上游可用性风险很高;
  • 特定构建系统要求单一源码树。

不一定需要的情况:

  • CI 有可靠 module proxy 和缓存;
  • repository 对体积和 diff 可读性敏感;
  • 团队能够依赖 go.sum、proxy 和制品仓库复现。

无论是否提交,都必须提交 go.mod/go.sum。vendor 不是它们的替代品。

17.4 vendor 不能手改

go mod vendor 会重建目录,手工修复会丢失,也绕开了 module 身份和校验机制。正确做法是:

  • 升级上游;
  • 使用 fork + replace;
  • 本地联调用 workspace;
  • 推动上游发布修复版本。

17.5 离线构建的真实边界

vendor 能提供 package 源码,但工具链本身、代码生成器、cgo 系统库、外部构建脚本仍可能需要额外制品。离线构建方案必须从干净环境完整演练,不能只看到 vendor/ 就认为所有网络依赖已经消失。

18. 仓库和目录如何划分

18.1 单 module 是默认选择

典型服务:

text
order-service/
├── go.mod
├── go.sum
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── order/
│   └── storage/
└── api/

一个 module 可以有多个可执行程序:

text
cmd/server
cmd/migrate
cmd/admin

每个目录是一个 package main,共享同一个 module 的依赖版本。

18.2 internal

位于 internal 目录下的 package 只能被特定父目录树内的代码 import,这是 Go 工具链强制执行的封装边界:

text
example.com/acme/order/internal/storage

外部 module 无法 import。对应用内部实现,internal 比人为约定“请勿使用”可靠。

18.3 什么时候拆 module

有这些信号时再考虑:

  • 需要独立发布和版本;
  • 生命周期与主项目明显不同;
  • 需要被外部项目独立依赖;
  • 依赖集合和最低 Go 版本需要独立控制;
  • 团队边界和 API 稳定性已经清楚。

不要因为“目录看起来大”就拆 module。package 已经是代码组织和封装单元,module 是更重的版本边界。

18.4 多 module monorepo

示例:

text
repo/
├── go.work
├── services/
│   ├── order/
│   │   └── go.mod
│   └── payment/
│       └── go.mod
└── libs/
    └── observability/
        └── go.mod

优点:

  • 每个服务独立依赖和发布;
  • workspace 支持本地联调。

成本:

  • 每个 module 独立 tidy、测试和版本;
  • 共享库变更要处理发布顺序;
  • tag 需要正确的子目录前缀;
  • CI 不能只在 repository 根目录执行一次 go test ./... 就假定覆盖全部嵌套 module。

18.5 module 不能循环依赖

package import graph 必须无环,module 之间最终也不能通过 package 形成循环。若 A 依赖 B、B 又需要 A,通常说明抽象边界不合理。

常见重构方式:

  • 抽取双方共同依赖的较小 package/module C;
  • 把接口定义放到使用方;
  • 通过回调或依赖注入反转依赖;
  • 合并本来就不该独立版本化的 module。

19. 发布一个 module

19.1 发布前检查

bash
go mod tidy
go test ./...
go vet ./...

建议再做:

bash
go test -race ./...
govulncheck ./...

并确认:

  • module path 与 repository 地址一致;
  • 公开 API 有文档;
  • go 指令确实是支持的最低版本;
  • 没有本地目录 replace;
  • 没有提交凭证或内部地址;
  • LICENSE 和依赖许可证符合要求;
  • 工作树干净;
  • v2+ module path 有正确 major suffix。

19.2 打 tag

根 module:

bash
git tag v1.2.0
git push origin v1.2.0

子目录 module:

bash
git tag tools/v1.2.0
git push origin tools/v1.2.0

Go 没有必须上传到中央仓库的“publish”命令。对公开 Git module,正确 tag 和可解析的 module path 就构成发布基础;proxy 会在用户请求后抓取并缓存。

19.3 先发布预发布版本

text
v2.0.0-alpha.1
v2.0.0-beta.1
v2.0.0-rc.1

用户必须显式选择预发布版本,@latest 通常优先最高稳定 release。破坏性变更在正式 v2 前通过预发布验证,能减少错误 tag 无法收回的问题。

19.4 发布后验证

在 module 外的临时目录验证:

bash
go mod init example/test-consumer
go get example.com/lib@v1.2.0
go list -m all

不要只在 repository 内验证,因为本地 workspace、replace、未提交源码和 module cache 都可能掩盖发布问题。

20. CI、可重复构建与供应链安全

20.1 最小 CI 流程

bash
go mod tidy -diff
go test -mod=readonly ./...
go vet ./...

说明:

  • tidy -diff 确认依赖元数据已同步;
  • -mod=readonly 防止测试过程中悄悄修改 go.mod;
  • 测试和 vet 验证选中的依赖图能正常构建。

是否额外运行 go mod verify 取决于缓存策略。它验证本地 module cache 未被修改,但正常下载本来就会通过 go.sum 和 checksum database 做内容认证。

20.2 固定 Go 工具链

可选方案:

  • 容器镜像固定完整 patch 版本;
  • CI setup action 固定 go-version-file: go.mod 或明确版本;
  • toolchain 指令统一开发建议;
  • GOTOOLCHAIN=local 禁止 CI 临时自动下载;
  • 或允许 auto,但确保 proxy、sumdb 和缓存策略覆盖工具链下载。

最低语言版本与 CI 实际工具链是两件事。库声称支持 Go 1.24,就应至少有一条 CI job 真正使用 Go 1.24 测试,而不是只写一行 go 1.24

20.3 缓存

通常缓存:

  • GOMODCACHE:下载的 module;
  • GOCACHE:编译缓存。

缓存 key 可包含:

  • 操作系统和架构;
  • Go 工具链版本;
  • go.sum 哈希。

不要把缓存当发布制品,也不要让不同信任边界的任务共享可写 cache。

20.4 漏洞扫描

官方工具:

bash
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...

若团队要求可重复版本,把它作为 tool dependency 固定,而不是 CI 永远拉 @latest

govulncheck 不只看 module 是否出现在 go.mod,还结合可达函数判断已知漏洞是否真正影响代码,噪声通常低于只按版本匹配的扫描器。

20.5 许可证与 SBOM

go.sum 只解决内容完整性,不负责:

  • 许可证合规;
  • 维护者可信度;
  • 恶意但哈希一致的代码;
  • 依赖接管风险;
  • SBOM。

可通过 go list -m -json allgo version -m 和组织内供应链工具生成清单。安全审查不能只看是否通过 checksum。

20.6 可重复不等于 bit-for-bit

固定 module 图和工具链能显著提高可重复性,但时间戳、VCS 信息、cgo、系统链接器、环境变量和构建 flags 仍可能影响二进制。

Go 1.24+ 在有效 VCS 工作树中构建时,脏工作树信息可能以 +dirty 等形式进入版本信息。正式发布应从干净、受控环境构建,并记录:

bash
go version -m ./binary

21. 依赖问题排查手册

21.1 “no required module provides package”

先确认 import path 是否拼错:

bash
go get example.com/module/package
go mod tidy

注意 go get 可以接受 package path,并会解析提供它的 module。若是私有 module,再检查 GOPRIVATE 和 Git 认证。

21.2 “module declares its path as X but was required as Y”

依赖声明的 module path 与目标 go.modmodule 行不一致。常见于:

  • 仓库改名但没有迁移 module path;
  • fork 后错误地改了 module 行;
  • v2 忘了 /v2
  • vanity path 与 Git path 混用。

不要仅靠随意 replace 压住错误。先确定该 module 的稳定公开身份。

21.3 checksum mismatch

这是安全错误,不要第一反应删除 go.sum 或关闭 GOSUMDB。可能原因:

  • 上游移动了 tag;
  • 私有 proxy 返回了不同内容;
  • module cache 被修改;
  • 网络或代理篡改;
  • 团队成员提交了错误校验值。

排查:

bash
go clean -modcache
go mod download
go env GOPROXY GOSUMDB GOPRIVATE

若重新下载仍不一致,应停止构建并调查上游和 proxy。

21.4 “inconsistent vendoring”

go.modvendor/modules.txt 不一致:

bash
go mod vendor

检查生成 diff 后提交。不要手改 modules.txt

21.5 为什么选中了这个版本

bash
go list -m all
go mod graph
go mod why -m example.com/lib
go list -m -json example.com/lib

从 graph 中找谁提出了最高最低版本要求。若只想临时理解,不要先编辑 go.mod 试错。

21.6 为什么本地能构建,CI 不行

逐项检查:

bash
go version
go env GOMOD GOWORK GOPROXY GOSUMDB GOPRIVATE GOTOOLCHAIN
go env -changed

常见差异:

  • 本地意外启用了上层 go.work
  • 本地 module cache 已有未公开版本;
  • 本地 Git 凭证可用,CI 不可用;
  • 工具链 patch 版本不同;
  • 本地有 replace;
  • CI 使用 vendor,本地使用 module cache;
  • build tags、GOOS、GOARCH 或 cgo 不同。

用:

bash
GOWORK=off go test -mod=readonly ./...

可以快速排除 workspace 和隐式依赖修改。

21.7 为什么 tidy 删除了我手写的 require

因为当前源码、测试和 tool 指令不需要它。若只是想预下载,不应该用 require 伪装依赖;使用 go mod download module@version

若代码通过插件名、反射配置或外部生成步骤间接需要某个 module,而源码没有 import,应该重新审视构建流程,或使用 tool 指令、显式生成入口等能表达真实关系的机制。

21.8 为什么 go mod why 说不需要,但 go mod graph 里有

graph 展示 requirement 边;why 查 package import 路径。一个陈旧 require 可以存在于 graph,却不再被任何当前 package 需要。运行:

bash
go mod tidy -diff

查看是否应移除。

22. 与 Maven/Gradle 对照

Java / Maven / GradleGo Modules关键差异
Maven module / artifactGo moduleGo module 可含多个 package,并以 module path + version 标识
pom.xml / build.gradlego.modgo.mod 更小,依赖主要从源码 import 推导
groupId:artifactIdmodule pathmodule path 通常也是可定位源码的 URL 风格路径
packagepackageGo package 通常对应一个目录
Maven CentralGOPROXYGo 可配置多个 proxy,也可 direct 拉 VCS
dependency checksum/lockgo.sumgo.sum 校验内容,不负责版本选择
dependency mediationMVS选择依赖要求中的最高最低版本,不按 nearest-wins
transitive dependency// indirect requirement仍参与 module graph 和安全风险
version range最低 module versionGo 通常依赖同 major 向后兼容,不做复杂范围求解
classifier / scopepackage、build tag、测试 import、tool没有一一对应的 Maven scope 模型
parent/BOM无直接等价物通常由各 module 自己维护,组织策略交给自动化
Maven Wrapper / toolchainsgo + toolchain + GOTOOLCHAINGo 可自动选择或下载新工具链
multi-module reactorgo.workworkspace 用于本地同时开发多个 main modules
local repositoryGOMODCACHE缓存是只读内容缓存,不应手工发布进去
dependency substitutionreplace只对 main module/workspace 生效,不向下游传播
mvn install通常不需要module 从 VCS/proxy 获取,本地联调用 workspace
Maven Shade通常无直接等价Go 多数依赖静态链接进二进制,但不改变 import path 身份
enforcer / dependency insightgo mod graphwhygo list工具粒度不同
OWASP dependency checkgovulncheckgovulncheck 可结合调用可达性降低误报

一个很重要的思维转换是:Go 项目通常不维护一份手写的庞大依赖声明。源码 import 是 package 依赖事实,go.mod 记录 module 级最低版本和无法从 import 单独推导的配置,go mod tidy 负责让两者一致。

23. 常见误区

概念

  • 把 module、package、repository 当成同一个概念。
  • 每建一个 package 就新建 go.mod
  • 认为 import 的字符串一定就是 module path。
  • go 指令当作无实际作用的注释。
  • 把 Java 9 module system 的经验直接套到 Go Modules。

依赖文件

  • 不提交 go.sum
  • 手工删除所有 // indirect
  • go.sum 当精确 lock file。
  • 直接修改 module cache 中的源码。
  • 用 require 保存“以后可能会用”的依赖。
  • 忘记在依赖变更后运行 tidy 和测试。

版本

  • 认为 MVS 会自动选择网上最新版本。
  • 认为 require v1.2.3 是严格钉死,忽略其他依赖可抬高版本。
  • v2 发布时忘记给 module path 和 import path 加 /v2
  • 移动已经发布的 Git tag。
  • 手工编造 pseudo-version。
  • v0 依赖假定存在稳定兼容承诺。

replace 与 workspace

  • 发布的 module 依赖本地 replace 才能构建。
  • 认为下游会继承库中的 replace/exclude。
  • replace 了 fork 后把源码 import 也改成 fork path。
  • 本地一直启用 go.work,没验证单 module 构建。
  • 把个人目录布局的 go.work 提交给全团队。

私有依赖与安全

  • 拉取失败就全局 GOSUMDB=off
  • 在 URL 或 go.mod 中提交访问 token。
  • 在设置 GOPRIVATE 前请求私有 module,泄露路径。
  • checksum mismatch 时直接删除 go.sum。
  • 认为通过哈希校验就代表依赖没有漏洞或恶意代码。
  • CI 使用 @latest 安装会影响生成结果的工具。

24. 命令速查

初始化和同步

bash
go mod init example.com/acme/project
go mod tidy
go mod tidy -diff

添加和调整依赖

bash
go get example.com/lib@latest
go get example.com/lib@v1.2.3
go get example.com/lib@abcdef123456
go get example.com/lib@none

查看依赖

bash
go list -m all
go list -m -u all
go list -m -versions example.com/lib
go list -m -json example.com/lib
go mod graph
go mod why -m example.com/lib

下载、校验与缓存

bash
go mod download
go mod download -json example.com/lib@v1.2.3
go mod verify
go env GOMODCACHE
go clean -modcache

replace

bash
go mod edit -replace=example.com/lib=../lib
go mod edit -dropreplace=example.com/lib

workspace

bash
go work init ./module-a ./module-b
go work use ./module-c
go work use -r .
go work sync
go env GOWORK
GOWORK=off go test ./...

工具

bash
go get -tool golang.org/x/tools/cmd/stringer@v0.40.0
go tool stringer
go get tool
go install tool

go install golang.org/x/vuln/cmd/govulncheck@latest

vendor 与只读构建

bash
go mod vendor
go test -mod=vendor ./...
go test -mod=readonly ./...

环境诊断

bash
go version
go env GOMOD GOWORK GOPROXY GOSUMDB GOPRIVATE GOTOOLCHAIN
go env -changed
go version -m ./binary

一套稳妥的提交前检查

bash
go mod tidy -diff
go test ./...
go vet ./...
govulncheck ./...

25. 可运行示例

本节源码位于 examples/ch02。这些例子分别从运行时、导入路径和发布构建三个角度观察 module 与 package,不需要修改仓库的 go.mod

25.1 在程序中读取构建信息

先让二进制报告自己的来历。 程序运行后,怎样知道它由哪个 module、哪个 Go 工具链构建?

go
package main

import (
	"fmt"
	"runtime/debug"
)

func main() {
	info, ok := debug.ReadBuildInfo()
	if !ok {
		fmt.Println("没有可用的构建信息")
		return
	}

	// 本仓库由 module path 标识。直接运行本示例时,主模块版本通常是 "(devel)",
	// 因为它来自工作区源码而不是一个带版本号的已发布模块。
	fmt.Printf("模块路径: %s\n", info.Main.Path)
	fmt.Printf("模块版本: %s\n", info.Main.Version)
	fmt.Printf("Go 版本前缀正确: %t\n", len(info.GoVersion) >= 2 && info.GoVersion[:2] == "go")
}

运行:

bash
go run ./examples/ch02/build-info

在本仓库和 Go 1.26 工具链中,预期输出为:

text
模块路径: github.com/TyrantLucifer/go-study
模块版本: (devel)
Go 版本前缀正确: true

读取结果时要分清四类信息。

  1. debug.ReadBuildInfo 返回嵌入二进制的 module、依赖和构建设置;ok 为 false 时不能继续解引用结果。
  2. Main.Path 来自当前 module 的声明,而不是工作目录名。
  3. 本地源码直接构建通常显示 (devel);从有版本的 module 构建时才可能得到语义化版本。
  4. Go 的具体补丁版本会随构建机变化,示例只验证稳定的 go 前缀,避免把部署环境误当语言常量。

比较两种构建方式。 遍历 info.Settings,找出 GOOSGOARCH 和 VCS 相关键;分别用 go run 与先 go build 再执行二进制,比较结果。

25.2 用 module path 穿过 package 边界

顺着一次 import 看清 package 边界。 import path 从哪里来?导出标识符和包内标识符怎样形成 API 边界?

主程序:

go
package main

import (
	"fmt"

	"github.com/TyrantLucifer/go-study/examples/ch02/package-boundary/greeting"
)

func main() {
	// import 使用的是“module path + module 内相对目录”,而不是磁盘绝对路径。
	// greeting 包只暴露以大写字母开头的 Welcome,内部实现仍留在包边界内。
	fmt.Println(greeting.Welcome("Gopher"))
}

被导入的 package:

go
// Package greeting 演示一个最小的可导入包。
package greeting

import "fmt"

// Welcome 是导出的 API:标识符首字母大写,其他 package 才能访问。
func Welcome(name string) string {
	return fmt.Sprintf("欢迎,%s", normalize(name))
}

// normalize 以小写字母开头,只在 greeting 包内部可见。
// 包边界既是编译边界,也是 API 设计边界。
func normalize(name string) string {
	if name == "" {
		return "匿名读者"
	}
	return name
}

运行:

bash
go run ./examples/ch02/package-boundary

预期输出:

text
欢迎,Gopher

路径、目录和可见性在这里汇合。

  1. 导入路径由 go.mod 中的 module path 加上目录相对路径组成,与本机绝对路径无关。
  2. 一个目录通常对应一个 package;greeting.go 声明 package greeting,主程序通过 import 使用它。
  3. Welcome 首字母大写,是包的公开契约;normalize 首字母小写,只能由同包代码调用。
  4. package 边界不是“把文件放进文件夹”这么简单,它决定编译单元、可见性和调用方能依赖的 API。

故意越过一次边界。Welcome 改成小写并运行,阅读编译器的不可见错误;再给 greeting 增加第二个 .go 文件,验证同一目录内可以直接调用未导出函数。

25.3 在链接阶段注入版本号

版本信息不必写死在源码里。 发布版本和提交号怎样进入二进制,又不必在每次构建前改源码?

go
package main

import "fmt"

// 变量保留默认值,使普通 go run 仍然可用。
// 发布构建可通过 -ldflags -X 注入版本与提交号,而不必改动源码。
var (
	version = "dev"
	commit  = "unknown"
)

func main() {
	fmt.Printf("version=%s commit=%s\n", version, commit)
}

先运行开发版本,确认源码中的默认值可用:

bash
go run ./examples/ch02/version-injection

它会输出:

text
version=dev commit=unknown

发布时再带固定构建元数据运行:

bash
go run -ldflags "-X main.version=v1.2.3 -X main.commit=abc123" \
  ./examples/ch02/version-injection

预期输出:

text
version=v1.2.3 commit=abc123

链接阶段会完成这次替换。

  1. 包级 string 变量提供开发构建的默认值,普通 go run 不会因为缺少发布参数而失败。
  2. -X importpath.name=value 在链接阶段替换字符串变量;这里是 main.versionmain.commit
  3. 注入值属于构建产物,不改变源码和 module 版本选择。
  4. 变量名或包路径写错时,链接器可能不会替你发现业务错误,发布脚本应执行二进制并校验输出。

再补一项构建元数据。 不带 -ldflags 运行并比较默认值;再增加 buildTime 字符串变量,通过第三个 -X 注入固定时间。

26. 官方资料

遇到 Modules 问题时,优先查看:

bash
go help modules
go help mod
go help mod tidy
go help module-auth
go help private
go help work

这些帮助文本直接来自当前使用的 Go 工具链,最能反映本机版本的实际行为。如果一篇网络文章仍要求设置 GO111MODULE=on、把所有源码放进 $GOPATH/src,或者使用 go get 安装命令,基本可以判断它面向的是旧版 Go。

以 Go 官方规范与标准库文档为准,示例面向 Go 1.26。