用 io.Copy 直接写入文件最稳妥,因 resp.Body 和 *os.File 天然适配,内存友好且避免 OOM;需正确关闭资源、检查错误、处理哈希校验、超时重定向及 Windows 路径问题。

用 io.Copy 直接写入文件最稳妥
HTTP 响应体(resp.Body)是 io.ReadCloser,文件句柄(*os.File)是 io.WriteCloser,两者天然适配 io.Copy。这是最简洁、内存友好的方式,不会把整个响应加载进内存。
常见错误是手动读取再写入,比如用 io.ReadAll + os.WriteFile,这对大文件极易 OOM;或者忘记关闭 resp.Body 或文件,导致资源泄漏。
实操建议:
- 用
os.Create创建文件(自动O_TRUNC),或os.OpenFile指定os.O_CREATE|os.O_WRONLY|os.O_TRUNC - 务必用
defer file.Close(),且放在io.Copy之后(否则可能写不全) - 检查
io.Copy返回的err,它可能来自读取或写入任一端 -
resp.Body必须在复制完成后显式Close(),哪怕用了defer
resp, err := http.Get("https://example.com/data.zip")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close() // 必须关 resp.Body
<p>file, err := os.Create("data.zip")
if err != nil {
log.Fatal(err)
}
defer file.Close() // 注意:这里 defer 要在 Copy 后才生效</p><p>_, err = io.Copy(file, resp.Body)
if err != nil {
log.Fatal(err) // 可能是网络中断、磁盘满等
}需要校验或修改响应内容时,别绕开 resp.Body 的流式特性
如果要校验 Content-Length、计算 SHA256、或只保存特定状态码的响应,不要先 io.ReadAll —— 这会丢掉流式优势。应该用 io.TeeReader 或包装 resp.Body。
立即学习“go语言免费学习笔记(深入)”;
例如边下载边算哈希:
hash := sha256.New()
reader := io.TeeReader(resp.Body, hash)
<p>_, err := io.Copy(file, reader)
if err != nil {
log.Fatal(err)
}
fmt.Printf("SHA256: %x
", hash.Sum(nil))容易踩的坑:
- 调用
resp.Body.Read()多次会跳过已读部分,不能反复读取 - 若需多次使用响应体(比如先校验再保存),必须用
bytes.Buffer或临时文件缓存,但要注意内存/磁盘成本 -
http.Response没有内置重放机制,Body是一次性流
http.Client 超时和重定向会影响文件写入完整性
默认的 http.DefaultClient 没有超时,网络卡住时 io.Copy 会无限等待;重定向后 resp.Body 是最终响应体,但中间响应被丢弃 —— 这本身没问题,但如果你依赖原始 Location 头或想记录跳转链,就得自己控制 CheckRedirect。
实操建议:
- 总是设置
http.Client.Timeout,例如&http.Client{Timeout: 30 * time.Second} - 如需自定义重定向逻辑,传入
CheckRedirect函数,避免意外跳转到不可信域名 - 检查
resp.StatusCode再写入,4xx/5xx 响应体通常不是目标数据(除非你明确要保存错误页)
Windows 下文件路径和权限问题比 Linux 更容易出错
Go 的 os.Create 在 Windows 上对路径中的反斜杠、保留名(如 CON、AUX)、长路径(>260 字符)更敏感。同时,Windows 默认不支持 os.FileMode 的 Unix 权限位,0644 会被忽略。
关键点:
- 路径统一用正斜杠
/或filepath.Join,避免硬写 - 文件名避免
PRN、NUL等 DOS 设备名(Go 不拦截,但系统会拒绝创建) - 长路径需启用 Windows 组策略「启用 Win32 长路径」,或用
\?前缀(Go 标准库不自动加,需手动拼) - 写入前确保目录存在:
os.MkdirAll(filepath.Dir(filename), 0755)
真正麻烦的是并发写同一文件、杀进程导致文件残留、或 antivirus 软件锁住刚创建的文件 —— 这些没法靠 Go 代码完全规避,得靠外部健壮性设计。


















