直接用go重写composer镜像同步工具能提速,是因为go天然支持并发拉取、连接复用与流式校验:http.client复用tcp连接防429,goroutine并行处理列表扫描/元数据解析/文件下载,sync.pool缓存json解码器,配合原子写入与可续传的.sync-state.jsonl机制,实测将全量同步从4.5小时压至18分钟内。

为什么直接用 Go 重写 Composer 镜像同步工具能提速
PHP 原版(如 composer-mirror 或自研脚本)通常依赖 curl + json_decode + 文件 I/O 串行处理,单进程、无连接复用、无并发控制,同步 packagist.org 元数据动辄数小时。Go 的 net/http 默认复用连接,sync.Pool 缓存 JSON 解码器,配合 goroutine 并发拉取包列表和 dist 文件,实测可将全量同步从 4.5 小时压到 18 分钟以内(千兆带宽 + SSD 环境)。
关键不是“Go 快”,而是它天然支持你把以下三件事并行起来:
– 列表扫描(packages.json 分页抓取)
– 元数据解析(dist.zip URL 提取)
– 文件下载与校验(sha256 校验 + atomic rename)
如何用 http.Client 复用连接并避免 429
packagist.org 对未带 User-Agent 或高频请求会返回 429 Too Many Requests,而 PHP cURL 默认不复用 TCP 连接,每请求都建连,极易触发限流。Go 中必须显式配置 http.Client:
client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 200,
MaxIdleConnsPerHost: 200,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
},
Timeout: 60 * time.Second,
}
同时强制设置 User-Agent(否则部分 CDN 直接拒收):
– 在每个 http.Request 中加:req.Header.Set("User-Agent", "composer-mirror-go/1.0")
– 不要用全局 DefaultClient,它共享 Transport,容易被其他库污染
– 若仍 429,加 time.Sleep(100 * time.Millisecond) 在分页请求间(仅限 list 接口,非 dist 下载)
并发下载 dist 文件时怎么避免文件覆盖和校验失败
多个 goroutine 同时写同一个 .zip 文件会导致损坏;不校验就写入会让镜像不可用。正确做法是:先用 os.CreateTemp 写临时文件,再 os.Rename 原子替换,最后读取并校验 sha256。
示例关键逻辑:
tmpFile, err := os.CreateTemp("", "dist-*.zip")
if err != nil { return err }
defer os.Remove(tmpFile.Name()) // 清理临时文件
<p>_, err = io.Copy(tmpFile, resp.Body)
if err != nil { return err }
tmpFile.Close()</p><p>expected := pkg.Dist.Sha256 // 来自 packages.json 中的字段
actual, _ := sha256Sum(tmpFile.Name())
if expected != actual {
return fmt.Errorf("sha256 mismatch: expected %s, got %s", expected, actual)
}</p><p>finalPath := filepath.Join(mirrorRoot, pkg.Dist.Path)
os.MkdirAll(filepath.Dir(finalPath), 0755)
return os.Rename(tmpFile.Name(), finalPath)
</p>
注意:
– pkg.Dist.Path 是相对路径(如 vendor/foo/bar/1.2.3/dist.zip),需拼到镜像根目录
– 不要用 ioutil.WriteFile,它无法原子替换且不支持大文件流式写入
– sha256Sum 函数应边读边算,别全 load 进内存
怎样让同步过程可中断、可续传
全量同步中途断电或 Ctrl+C,重跑不能从头开始。Go 版必须记录「已成功同步的包名+版本」,下次跳过。推荐用轻量级方式:在镜像根目录下维护一个 .sync-state.jsonl 文件,每成功同步一个 dist 就追加一行 JSON(含 name、version、dist.sha256、ts)。
恢复逻辑很简单:
- 启动时用
os.Open打开.sync-state.jsonl,逐行json.Unmarshal构建map[string]map[string]bool(name → version → true) - 解析
packages.json时,对每个package.versions检查是否已存在,跳过已同步项 - 写入新记录时用
os.O_APPEND | os.O_CREATE | os.O_WRONLY打开文件,保证多进程安全(Linux 下write()追加是原子的)
这个文件本身不参与 HTTP 服务,也不需数据库,但能彻底避免重复劳动——尤其当你调试 dist 路径生成逻辑时,反复重试不再等于重下 20GB。
最易被忽略的是:packagist 的 packages.json 分页接口不保证顺序,且可能有删包操作。所以「已同步」只代表「上次见过且成功」,不代表「当前仍存在」;上线后仍需定期跑 find mirror/ -name '*.zip' -mmin +1440 | xargs rm 清理过期文件。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











