go.mod 是依赖的单一事实 — MVS 最小版本选择, go.sum 哈希校验, GOPRIVATE 私有仓, GOOS/GOARCH 交叉编译一行命令出单二进制
Go 的依赖管理核心是一条铁律: go.mod 是单一事实。它用 MVS (最小版本选择) 解析依赖 —— 每个模块选"所有需求里声明的最高版本", 而不是 registry 上的 latest, 所以同一份 go.mod 在任何机器上解出同一棵树, 构建天然可复现; go.sum 再用两行哈希锁死"拿到的就是当初那个版本", 防投毒防漂移。私有仓库用 GOPRIVATE 分流, 跳过公网代理与校验库走内网 git; 发布侧则是 Go 的招牌: GOOS/GOARCH 交叉编译加 -ldflags "-s -w -X main.version=...", 一行命令产出一个自带运行时的二进制 —— 这是相对 JVM 系语言最舒服的部署体验。
go get mod@v1.4.0。# A 要 C v1.1.0; B 要 C v1.2.0 → MVS 选中 C v1.2.0 go list -m all # → 每模块 = 所有需求里声明过的最高版本 # 关键: 不查 latest 不自动升级, 升级必须显式: go get corp.example.com/lib@v1.4.0
/v2 后缀 (如 mod/v2/pkg) —— import 路径变了才算"新模块", 这是拿到大版本升级的钥匙。// 关键: v2+ 模块路径必须带 /v2 后缀 require corp.example.com/authlib/v2 v2.1.0 // import "corp.example.com/authlib/v2/pkg" // 路径变了才算"新模块" — 大版本升级的钥匙
security: checksum mismatch。它是锁文件, 不是可手编的注释。head -2 go.sum # → golang.org/x/text v0.14.0 h1:... # → golang.org/x/text v0.14.0/go.mod h1:... # 关键: zip+go.mod 两行哈希, 下载即校验防投毒
GOPRIVATE=corp.example.com/*。go env -w GOPRIVATE=corp.example.com/* # 关键: 匹配路径不走 GOPROXY 也不走 sumdb go env GOPRIVATE # → corp.example.com/*
direct 表示直连源站; off 只用本地缓存, none 强制直连 —— 排查依赖来源时切着用。go env GOPROXY # → https://proxy.golang.org,direct # 关键: off=只用缓存, none=强制直连, 排查来源切着用 GOPROXY=off go build ./...
// 联调神器: 换成本地路径或另一版本 replace corp.example.com/authlib => ../authlib // 关键: 只有主模块的 replace 生效, 别提交到生产
exclude 在解析时拉黑某个版本; retract 是模块作者在自家 go.mod 里声明"某版本我撤回", 提示新用户别选。exclude corp.example.com/lib v1.9.1 // 解析时拉黑 // retract 由模块作者写在自家 go.mod: retract v1.9.1 // "这版有 bug, 新用户别选"
go mod tidy # 关键: 按当前 tag 集判断, 冷门 build tag 的 import # 可能被误删 — 带对应 tag 跑: go mod tidy -tags=integration
go mod vendor 把依赖拷进 vendor/ 目录, 构建用 -mod=vendor 完全离线 —— 内网构建机与供应链审计的标配。go mod vendor go build -mod=vendor ./... # 关键: 依赖拷进 vendor/ 完全离线 — 内网构建标配 git add vendor go.mod go.sum
GOOS/GOARCH 环境变量切目标平台; CGO_ENABLED=0 禁 CGO 出纯静态二进制, 否则交叉编译要带目标平台 C 工具链。CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build ./... # 关键: 一行命令切平台; CGO 关掉出纯静态二进制 # 否则交叉编译要带目标平台 C 工具链
-X main.version=1.2.3 在链接期改写包级变量, 只对 string 类型生效; Go 1.18+ 还可 debug.ReadBuildInfo() 读嵌入的 VCS 信息。go build -ldflags "-s -w -X main.version=1.47.2" . # 关键: 链接期改写包级变量, 只对 string 生效 # Go 1.18+ 可 debug.ReadBuildInfo() 读嵌入 VCS 信息
// go.work: 多模块圈进一个联调空间 use ( ./gateway ./authlib ) // 关键: 互相走本地代码, go.mod 一个字不改
go install golang.org/x/vuln/cmd/govulncheck@latest govulncheck ./... # 关键: 只报"代码真的调到漏洞函数"的项 # 比纯版本号 CVE 扫描噪音小得多
go get 私仓报 404 或 checksum mismatch, 九成是走了公网代理; 本地走 ssh, CI 走 token:
go env -w GOPRIVATE=corp.example.com/* go env -w GONOSUMDB=corp.example.com/* go env GOPRIVATE git config --global \ url."git@corp.example.com:".insteadOf "https://corp.example.com/" go mod tidy go build ./...
CI 上把 insteadOf 换成 url."https://oauth2:${GIT_TOKEN}@corp.example.com/" 即可用 token 拉取; GOPRIVATE 同时解决了"私仓路径被送去公网 sumdb 校验"的信息泄露。
authlib 改一行就要发版半天; 联调期用 replace 直连工作区副本:
module corp.example.com/gateway go 1.23 require corp.example.com/authlib v0.4.1 replace corp.example.com/authlib => ../authlib
网关立刻用上本地未发布的 authlib; 联调完删掉这行再 tidy —— 或者干脆用场景 9 的 go.work, 更干净。
线上排查第一个问题永远是"这二进制是哪个版本"; 链接期注入 + 构建信息兜底:
var version = "dev" func main() { if bi, ok := debug.ReadBuildInfo()(); ok && bi.Main.Version != "(devel)" { version = bi.Main.Version // go install mod@v 时自动有版本 } fmt.Printf("gateway %s (go %s)\n", version, runtime.Version()) } // go build -ldflags "-X main.version=1.47.2" 注入生效 // -X 只认 string; 路径必须 包名.变量名, 大小写敏感
单阶段把整个 GOPATH 打进镜像动辄 1GB; 分层构建 + 静态编译一步到位:
FROM golang:1.23-alpine AS builder WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -trimpath \ -ldflags "-s -w -X main.version=1.47.2" \ -o /out/gateway ./cmd/gateway FROM scratch COPY --from=builder /out/gateway /gateway COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ ENTRYPOINT ["/gateway"]
要点: go.mod/go.sum 先拷、源码后拷, 依赖层命中缓存重建只要 8s; scratch 里必须带上 ca-certificates 否则 HTTPS 全挂; 镜像从 1.1GB 降到 11MB。
Go 的招牌能力, 打 tag 即出全平台产物与校验和:
set -euo pipefail VER=${GITHUB_REF##*/tags/} for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do GOOS=${target%/*}; GOARCH=${target#*/} CGO_ENABLED=0 go build -trimpath \ -ldflags "-s -w -X main.version=$VER" \ -o "dist/gateway-${GOOS}-${GOARCH}" ./cmd/gateway tar -czf "dist/gateway-${GOOS}-${GOARCH}.tgz" -C dist "gateway-${GOOS}-${GOARCH}" done sha256sum dist/* > dist/checksums.txt
有人手动改 go.mod 加依赖忘了 tidy, 或升级没锁版本, 下次构建全体遭殃; PR 门禁直接拦:
name: mod-hygiene on: [pull_request] jobs: tidy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: { go-version: '1.23' } - run: go mod tidy - run: git diff --exit-code go.mod go.sum
tidy 后 git diff 非空就说明提交者忘了同步 —— 门禁用差异说话, 比 code review 盯着看可靠。
依赖漏洞是长期债, 每周一扫, 按真实调用路径降噪:
name: vuln-scan on: schedule: [{ cron: '0 3 * * 1' }] jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: { go-version: '1.23' } - run: go install golang.org/x/vuln/cmd/govulncheck@latest - run: govulncheck ./...
govulncheck 只报"你的代码真的调用了漏洞符号"的项, 比纯版本号匹配少 90% 噪音; 扫出高危按工单升级。
金融内网构建机不允许出公网, GOPROXY 全被防火墙拦; 冻结一份依赖副本进仓库:
go mod verify go mod vendor go build -mod=vendor ./... du -sh vendor/ git add vendor go.mod go.sum git commit -m "vendor: freeze deps for intranet builder"
构建时 -mod=vendor 强制从 vendor/ 读依赖, 完全不碰网络; 代价是每次升依赖要重新 vendor 并提交一个巨型 diff, 适合强隔离环境。
网关、authlib、billing 三个仓库联动改 bug, 反复发版太痛; workspace 一文件圈住:
go 1.23 use ( ./gateway ./authlib ./billing )
目录下 go build 自动把三个模块互相解析成本地代码, go.mod 一个字不用改; 团队约定 go.work 提交与否要统一, 别一半人有一半人没有。
产物莫名巨大, 先问依赖图"这条边是谁画的":
go mod graph | wc -l go mod graph | grep ' golang.org/x/text@' | sort -u go mod why -m golang.org/x/text go list -m all | wc -l go list -deps ./... | wc -l go version -m dist/gateway du -h dist/gateway
go mod why 打印出引入链: gateway/ui → x/text/encoding —— 是 UI 库为一个小函数拖进全家桶; 换成轻量实现后依赖数从 147 降到 89, 二进制 28MB → 9MB。
go mod tidy 重新生成并完整提交, 绝不手编。# 错: 手编 go.sum 里的哈希"改到匹配" rm go.sum # 对: 可以删, 但必须紧跟 tidy go mod tidy # → 重新生成, 完整提交
go get mod@v2.0.0 报 "invalid version"。原因: v2+ 语义化导入版本要求路径带 /v2。正解: import 路径与 go get 都改成 mod/v2。# 错: go get corp.example.com/lib@v2.0.0 → invalid version go get corp.example.com/lib/v2@v2.0.0 # 对: 路径带 /v2 # import 同步改: "corp.example.com/lib/v2/pkg"
go env -w GOPRIVATE=corp.example.com/* + git insteadOf, 见场景 1。# 错: 未配 GOPRIVATE — 404 / checksum mismatch go env -w GOPRIVATE=corp.example.com/* # 对 git config --global \ url."git@corp.example.com:".insteadOf "https://corp.example.com/"
replace ... => ../lib 提交了, CI 机器没有这个目录直接构建失败。正解: 联调用 go.work 或临时改, 合并前 git diff go.mod 人工过一眼。# 错: replace corp.../authlib => ../authlib 提交进仓库 # → CI 没有这个目录, 构建直接失败 # 对: 联调用 go.work 或临时改, 合并前 git diff go.mod
# 错: go get 不带版本 — 今天好明天坏 go get corp.example.com/lib@v1.4.0 # 对: 显式版本 git add go.mod go.sum # 对: 必须进版本库
go mod graph 看连带变化, 大升级单独一个 PR。go get corp.example.com/a@v1.5.0 # 错: 只跑受影响包的测试, B 行为变了没发现 go test ./... # 对: 全量测试 go mod graph | grep ' lib/b@' # 对: 看连带变化
//go:build integration 的测试 import 被 tidy 判定"没人用"删掉。正解: 用对应 tag 跑 go mod tidy -tags=integration, 或给依赖留一个无条件引用文件。# 错: 裸 go mod tidy 把 //go:build integration 下的 # import 判定"没人用"删掉 go mod tidy -tags=integration # 对: 带对应 tag 跑
CGO_ENABLED=0, 或用 zig/cc 交叉工具链。# 错: mac 上直接 go build → gcc: no such file or directory # (sqlite/user 等依赖触发 CGO) CGO_ENABLED=0 GOOS=linux go build ./... # 对: 静态
CGO_ENABLED=0 静态编译, 或换 debian-slim 基础镜像。# 错: glibc 链接的 CGO 二进制跑在 musl 的 alpine 上 # → 时间/浮点/DNS 偶发怪错 FROM debian:bookworm-slim # 对: 换 glibc 基础镜像 # 或 CGO_ENABLED=0 静态编译后用 scratch
version 没带包名/大小写错。正解: -X main.version=... 三要素: 包路径、变量名、string 类型。# 错: -X version=1.2 (没带包名) / 变量非 string go build -ldflags "-X main.version=1.47.2" # 对 # 三要素: 包路径.变量名 + string 类型, 大小写敏感
go mod tidy, 或开 go mod edit -require。# 错: import 新包后直接 build → missing go.sum entry # (1.17 起 go get 不再自动 tidy) go mod tidy # 对: import 完显式跑 go build ./...
go mod vendor 同步, 两个文件一起提交。# 错: 只改 go.mod, vendor/ 还是旧的 # → inconsistent vendoring go mod vendor # 对: 同步 git add go.mod go.sum vendor/ # 对: 一起提交
# 错: 强拉 v2.3.1+incompatible — 升级/校验常有坑 go get legacy.corp.com/old@v2.3.1+incompatible # 对: 推动上游转 module, 或 fork 自己发 module 版本
GOWORK=off 兜底。# 错: 一半人提交 go.work 一半人没有 → 解析行为不同 git add go.work go.work.sum # 对: 都提交保持一致 echo 'go.work' >> .gitignore # 或: 忽略+CI GOWORK=off
go.sum 的 hash(每个依赖行) 细粒度键, 或用 actions/cache 的 restore-keys 前缀匹配。# 错: 缓存键 = 整个 go.sum 的 hash — 一行变全量重拉 - uses: actions/cache@v4 with: path: ~/go/pkg/mod key: mod-${{ hashFiles('go.sum') }} restore-keys: mod- # 对: 前缀匹配兜底
# 错: 祈福上游库 go.mod 里的 replace 生效 — 被忽略 # (只有主模块的 replace 有效, 设计如此) # 对: 在自己主模块 go.mod 里 replace 目标版本 replace golang.org/x/crypto => golang.org/x/crypto v0.17.0
# 错: 半年后重建 — direct 拉老版本 404 # 对: 公网模块走 proxy.golang.org 不可变缓存; # 私有依赖 fork + 自建 proxy (Athens) 缓存 GOPROXY=https://athens.corp.example.com,direct
# 错: go env -w GOFLAGS=-mod=vendor — 全局污染 # 切到非 vendor 项目全报错 go env -u GOFLAGS # 对: 清掉全局 make build # 对: 项目内显式传 -mod=vendor
retract v1.9.1 // 已撤回的坏版本 # 错: 以为老用户会自动降级 — 不会, 只拦"新选择" go get corp.example.com/lib@v1.9.2 # 对: 发修复版+广播
go mod tidy 让工具重新生成 —— 人不跟机器抢活。# 错: 手工拼 go.sum 冲突块 → checksum mismatch 随机出现 rm go.sum # 对: 冲突整块删掉 go mod tidy # → 工具重新生成, 人不跟机器抢活