http.Get 直接下载文件易出空文件、403、卡死或OOM,因未校验状态码、未关闭响应体、未处理Header拦截;需设User-Agent、检查StatusCode、用io.Copy流式写入、清理残缺文件、校验路径安全。

直接用 http.Get 下载文件大概率会出空文件、403、卡死或 OOM,真正能上线的逻辑必须绕过三类坑:没校验状态码、没关响应体、没处理 header 拦截。
为什么 http.Get 常返回空文件或 403
服务端不是在拒绝你的 Go 程序,是在拒绝“没有 User-Agent 的裸请求”。GitHub raw、CDN、Nginx 默认拦截这类请求,返回 403 或 HTML 错误页而非二进制内容。
- 必须手动设置
req.Header.Set("User-Agent", "Mozilla/5.0"),哪怕只是占位 - 下载 GitHub raw 链接时,额外加
Accept: application/octet-stream,否则可能拿到 HTML -
resp.StatusCode必须显式检查是否在 200–299 范围,不能只看err == nil - 重定向(如 302)默认会被
http.DefaultClient自动跟随,但最终resp.Request.URL才是真实地址,需校验它是否仍指向资源而非登录页
用 io.Copy 写入文件的正确姿势
io.Copy 是唯一推荐方式——它内部用 32KB 缓冲区流式传输,不加载全文进内存,适合任意大小文件。但几个关键点常被忽略:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 写入前必须调用
os.MkdirAll(filepath.Dir(destPath), 0755),否则路径不存在会失败 - 文件名若来自 URL,要用
filepath.Base提取并过滤非法字符(如 Windows 的\ / : * ? " < > |),防止路径遍历 -
os.Create会覆盖已有文件;如需断点续传,得先os.Stat获取已下载字节数,再用os.OpenFile(..., os.O_WRONLY|os.O_APPEND) - 别忘了
defer resp.Body.Close()和defer out.Close(),漏掉一个就可能耗尽连接或句柄
如何安全处理大文件与异常中断
大文件下载不是“启动→结束”的线性过程,网络抖动、服务端超时、磁盘满都可能导致中途失败,此时本地文件已存在但不完整。
立即学习“go语言免费学习笔记(深入)”;
-
io.Copy返回非 nil error 时,应主动os.Remove(destPath)清理残缺文件,否则下次调用会覆盖失败 - 不要依赖
Content-Length计算进度——chunked 编码下它为空,io.Copy仍能工作但无法预估总长 - 如需进度回调,得自己用固定缓冲区(如
buf := make([]byte, 64*1024))循环Read,每次累加并通知上层 - 生产环境必须设超时:
http.Client{Timeout: 30 * time.Second},否则一个卡住的连接会拖垮整个下载队列
最易被忽略的一点:所有路径拼接必须用 filepath.Join,且下载前做 filepath.Abs + strings.HasPrefix 校验,否则恶意 URL(如 ../../etc/passwd)可突破目录限制。这不是理论风险,而是真实攻击面。

















