直接用http.Get读全部内容会崩,因为大文件(如几百MB)将整个响应体加载进内存,触发大量堆分配导致OOM;须用http.Client设置超时、检查StatusCode、defer关闭Body,并用io.Copy流式写入文件。

为什么直接用 http.Get 读全部内容再写入会崩
因为大文件(比如几百MB)会把整个响应体加载进内存,resp.Body.Read 返回的 []byte 会触发大量堆分配,容易 OOM。Go 的 http.Client 默认不自动解压、不限制超时,还可能卡在慢连接上。
- 务必用
resp.Body流式读取,配合io.Copy或分块Read+Write - 设置
http.Client.Timeout,至少包含Timeout、Transport.IdleConnTimeout - 检查
resp.StatusCode,不是200就别往下读了 - 用
filepath.Join拼路径,避免硬编码/或\
用 io.Copy 实现最简流式下载
这是最推荐的起点:底层复用 io.Copy 的缓冲逻辑(默认 32KB),零内存拷贝,代码短且健壮。
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Get("https://example.com/large.zip")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
<p>if resp.StatusCode != http.StatusOK {
log.Fatal("HTTP", resp.StatusCode)
}</p><p>out, err := os.Create("large.zip")
if err != nil {
log.Fatal(err)
}
defer out.Close()</p><p>_, err = io.Copy(out, resp.Body) // 自动分块读写,不占额外内存
if err != nil {
log.Fatal(err)
}-
io.Copy内部用固定大小缓冲区循环Read/Write,无需手动管理[]byte - 不要用
out.Write直接写resp.Body—— 它不是[]byte,是io.Reader - 如果要显示进度,得换成
io.CopyN或自定义io.Reader包装器
需要进度回调?用 io.TeeReader + 自定义计数器
想打印“已下载 12.4MB / 156MB”,就不能只靠 io.Copy。核心是把原始 resp.Body 包一层,每次读都触发回调。
type ProgressReader struct {
reader io.Reader
total int64
written int64
onProgress func(int64, int64)
}
<p>func (r *ProgressReader) Read(p []byte) (int, error) {
n, err := r.reader.Read(p)
r.written += int64(n)
r.onProgress(r.written, r.total)
return n, err
}</p><p>// 使用时:
resp, _ := client.Get(url)
defer resp.Body.Close()
pr := &ProgressReader{
reader: resp.Body,
total: resp.ContentLength, // 注意:可能为 -1(chunked)
onProgress: func(written, total int64) {
fmt.Printf("\r%v / %v", humanize.Bytes(uint64(written)), humanize.Bytes(uint64(total)))
},
}
io.Copy(out, pr)-
resp.ContentLength可能是-1(服务端用Transfer-Encoding: chunked),此时只能显示“已下载 X MB” - 别在
onProgress里做耗时操作(如频繁fmt.Println),建议加锁+节流或用 channel 异步处理 - 不用自己实现
Read?可直接用io.TeeReader(resp.Body, writerThatCounts),但回调粒度更粗
写磁盘慢?关掉 O_SYNC,用 os.O_CREATE | os.O_WRONLY
默认 os.Create 等价于 os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666),不强制刷盘。但某些 SSD 或 NFS 挂载点仍可能因内核缓冲策略变慢。
立即学习“go语言免费学习笔记(深入)”;
- 绝对不要加
os.O_SYNC—— 它会让每次Write都等磁盘物理写入,速度暴跌 10 倍以上 - 确保目标目录有足够空间和写权限;
os.Stat检查父目录是否存在,不存在就os.MkdirAll - 如果并发下载多个文件,注意
os.File是非线程安全的,每个 goroutine 必须用独立*os.File
流式下载的本质就是让内存占用和文件大小解耦——只要网络和磁盘 I/O 跟得上,GB 级文件也只吃几 KB 内存。最容易被忽略的是错误传播:任何一处 err != nil 没检查,就可能静默丢数据或创建空文件。


















