根本原因是go mod download并发写入同一模块缓存ZIP时未加跨进程锁,导致Linux/macOS出现text file busy、Windows出现Access is denied;解决方案包括使用-mod=readonly跳过下载、启用GOPROXY私有代理统一入口,或严格二选一采用vendor+固定flag或代理模式。

go mod download 时文件锁冲突的根本原因
Go 工具链在并发执行 go mod download 或 go build 时,多个 goroutine 可能同时尝试写入同一模块的缓存 ZIP(位于 $GOPATH/pkg/mod/cache/download/ 下),而 Go 默认使用 os.O_CREATE | os.O_WRONLY 打开目标文件,不加跨进程锁——这导致 Linux/macOS 上出现 text file busy 或 permission denied,Windows 上更常见 Access is denied(因句柄被占用)。这不是 Go 的 bug,而是设计使然:模块缓存写入未做并发保护,依赖底层文件系统原子性或外部协调。
用 -mod=readonly 强制跳过下载阶段
如果你已通过 go mod vendor 或 CI 预缓存好全部依赖,构建时完全不需要联网拉取,最简单且 100% 规避锁问题的方式就是禁用任何下载行为:
- 构建命令统一加
-mod=readonly参数:go build -mod=readonly ./... - 该模式下,Go 仅从本地
vendor/或$GOPATH/pkg/mod/读取模块,遇到缺失依赖直接报错missing module for dependency,而非尝试下载 - 配合
go mod verify可提前验证所有依赖哈希是否匹配go.sum,避免运行时才发现篡改
启用 GOPROXY + 私有代理缓存层
若必须动态拉取(如开发环境或灰度发布),靠本地锁解决不了根本问题——要让所有并发请求都打到同一个可序列化的服务端入口:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 设置
GOPROXY=https://proxy.golang.org,direct(或指向你自己的athens/goproxy.io实例) - 私有代理自身会对每个 module+version 做单例下载+缓存,下游所有
go mod download请求都变成只读 GET,彻底消除本地磁盘竞争 - 注意:不要混用
GOPROXY=direct和GOPROXY=自建地址,否则部分请求仍走本地下载,锁冲突照旧
为什么 vendor 目录不能完全替代 GOPROXY
go mod vendor 确实把代码复制进项目,但它的作用范围有限:
立即学习“go语言免费学习笔记(深入)”;
- 只影响
go build -mod=vendor场景;一旦漏掉这个 flag,Go 仍会回退到$GOPATH/pkg/mod并发写缓存 - vendor 不包含
go.sum校验所需的 module zip 哈希,go mod verify仍需访问原始缓存路径 - CI 中若先
go mod vendor再go mod download(例如某些模板脚本),反而触发双重写入,锁冲突概率翻倍
真正稳定的方案是二选一:要么全量 vendor + 固定 -mod=vendor 构建,要么全量代理 + GOPROXY 统一入口。混合使用最容易踩坑。

















