Go标准库net/http可下载文件,但需用自定义http.Client控制超时、重试与重定向,边读边写防OOM,校验状态码,支持断点续传,并安全提取文件名。

Go 标准库 net/http 完全能胜任文件下载,但直接用 http.Get 容易内存爆满或中断后无法续传——关键在控制流、响应体处理和错误恢复。
用 http.Client 发起带超时的下载请求
硬写 http.Get 会丢失连接控制权,超时、重试、重定向都难干预。必须用自定义 http.Client:
-
Timeout和Transport的IdleConnTimeout都要设,否则小文件没事,大文件卡住就僵死 - 下载大文件时建议关闭重定向(
CheckRedirect返回http.ErrUseLastResponse),避免跳转后丢掉原始Content-Length - 加
User-Agent头,部分服务(如 GitHub Releases)会拦截无 UA 的请求
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
IdleConnTimeout: 60 * time.Second,
},
}
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("User-Agent", "golang-downloader/1.0")
resp, err := client.Do(req)
边读边写避免 OOM:用 io.Copy 而非 io.ReadAll
调 io.ReadAll(resp.Body) 会把整个响应体加载进内存,下载 1GB 文件就占 1GB RAM——生产环境绝对禁止。
- 直接用
io.Copy把resp.Body流式写入本地*os.File,内存占用恒定在几 KB - 务必检查
resp.StatusCode是否为200,否则io.Copy会静默把 404 HTML 写进文件 - 若需校验完整性,可在
io.Copy时用io.TeeReader同时计算sha256,不额外遍历文件
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad status: %s", resp.Status)
}
f, _ := os.Create("out.zip")
defer f.Close()
_, err := io.Copy(f, resp.Body) // 不会吃光内存
支持断点续传:用 Range 请求头 + os.OpenFile 追加模式
网络抖动或用户中断后,重新下载整个文件既慢又费流量。HTTP 断点续传依赖服务端支持 Accept-Ranges: bytes,客户端需:
立即学习“go语言免费学习笔记(深入)”;
一套面向小企业用户的企业网站程序!功能简单,操作简单。实现了小企业网站的很多实用的功能,如文章新闻模块、图片展示、产品列表以及小型的下载功能,还同时增加了邮件订阅等相应模块。公告,友情链接等这些通用功能本程序也同样都集成了!同时本程序引入了模块功能,只要在系统默认模板上创建模块,可以在任何一个语言环境(或任意风格)的适当位置进行使用!
- 先
HEAD请求获取Content-Length和是否支持Range - 检查本地文件是否存在且非空,用
os.Stat获取已下载字节数done - 发起
GET时加请求头:Range: bytes=done-,并用os.OpenFile(..., os.O_WRONLY|os.O_APPEND)打开文件 - 注意:服务端返回状态码是
206 Partial Content,不是200
不校验 Content-Range 响应头就写入,可能造成文件错位——比如服务端实际从 1000 字节开始发,但你从 800 字节处追加,后半截就乱了。
文件名提取别信 Content-Disposition,优先 fallback 到 URL 路径
Content-Disposition: attachment; filename="foo.pdf" 看似可靠,但大量服务(尤其是 CDN 或反向代理)根本不返回该头,或返回非法字符(如中文未编码、含路径分隔符)。
- 先尝试解析
Content-Disposition,用mime.ParseMediaType提取filename*(RFC 5987)或filename(RFC 2231) - 失败或为空时,从 URL 路径取最后一段(
path.Base(url.Path)),再用strings.TrimSuffix去掉查询参数 - 最终文件名必须过滤掉
/ \ : * ? " |等危险字符,Windows 和某些文件系统不认
没做路径清洗就直接 os.Create(filename),遇到 ../../etc/passwd 这类构造名可能覆盖系统文件——哪怕只是测试也要防住。









