构建脚本必须放在 scripts 目录下,使用 #!/usr/bin/env bash 开头,通过 $(dirname $(dirname $(realpath "$0"))) 获取项目根目录;go build 必须显式指定 -mod=vendor 或 -mod=readonly,版本信息通过 -ldflags 注入,产物统一输出至 bin/ 目录并隔离。

构建脚本必须放在 scripts 目录下,而非根目录或 cmd
Go 项目一旦规模上升,手动敲 go build 或拼接 CGO_ENABLED=0 go build -o bin/app 就会失控。真实团队协作中,构建逻辑必须收敛、可复用、可审计。scripts 是社区事实标准路径(见 Go 标准项目布局),不是约定俗成,而是被 goreleaser、act、CI 模板和 make 文件普遍识别的入口点。
常见错误是把 build.sh 放在项目根目录,导致:
– CI 配置里硬编码路径,迁移成本高
– 新成员 clone 后不知道该跑哪个脚本
– git grep "go build" 扫出十几处,无法统一维护
- 所有构建相关脚本统一放在
scripts/下,例如:scripts/build-linux-amd64.sh、scripts/test-cover.sh - 脚本第一行必须是
#!/usr/bin/env bash,禁止用sh—— macOS 的/bin/sh不支持[[和数组语法 - 脚本内避免写死 GOPATH 或 module path;用
$(dirname $(dirname $(realpath "$0")))获取项目根目录,再cd进去
go build 必须显式指定 -mod=vendor 或 -mod=readonly
不加 -mod 参数时,go build 默认行为是:如果存在 vendor/ 目录就用它,否则 fallback 到 go.mod + proxy。这在 CI 环境中极危险——某次本地 go mod vendor 后忘记提交 vendor/,CI 仍能成功构建(因走 proxy),但结果与开发环境不一致。
真实踩坑场景:
– 开发机有缓存,go.sum 被悄悄更新,但未提交
– CI 使用干净容器,go build 自动拉取新版依赖,导致 panic 或行为变更
立即学习“go语言免费学习笔记(深入)”;
- CI 构建一律使用
go build -mod=vendor -o bin/app ./cmd/app(前提是已执行go mod vendor并提交vendor/) - 本地开发可放宽为
go build -mod=readonly,强制校验go.sum,失败即报错,不自动修改 - 永远不要在构建脚本里写
go mod tidy—— 它会改写go.mod和go.sum,破坏可重现性
版本信息注入要用 -ldflags,而不是运行时读文件
很多团队在 main.go 里写 os.ReadFile("VERSION") 或调 git describe,这导致二进制不自包含、无法离线运行、且破坏构建可重现性(同一 commit 在不同机器上可能因 git 状态不同而生成不同版本号)。
正确做法是在构建时通过链接器参数注入,Go 原生支持,无需额外工具链。
- 定义变量:
var version string(注意必须是包级变量,且不能是 const) - 构建命令:
go build -ldflags "-X main.version=$(git describe --tags --always --dirty)" -o bin/app ./cmd/app - CI 中若禁用 git 命令,可用环境变量替代:
-ldflags "-X main.version=${CI_COMMIT_TAG:-dev}" - 避免注入过多字段(如编译时间、用户名),这些信息应由日志系统或部署平台提供,而非污染二进制元数据
构建产物路径必须隔离,且禁止覆盖已有文件
直接 go build -o app 会把输出放在当前目录,极易误操作覆盖源码(尤其当 app 是目录名时)。更严重的是,并行构建时多个 job 可能写入同一路径,产生竞态。
典型现象:
– bin/app 有时是 Linux 版,有时是 Windows 版,CI 日志却显示 success
– ls -l bin/ 发现文件 mtime 是 3 天前,但明明刚跑过构建
- 所有构建输出强制写入
bin/(非./或dist/),该目录应加入.gitignore - 脚本中先
rm -rf bin/ && mkdir -p bin/,不依赖上次残留状态 - 跨平台构建必须带目标标识:
go build -o bin/app-linux-amd64,而非重命名或覆盖 - 禁止在
Makefile或脚本里用cp复制构建产物到其他位置——构建就是构建,分发是另一层职责


















