不能直接用http.get+io.readall,因为io.readall会将整个响应体加载进内存,下载500mb文件易导致oom;真实场景需支持超时、中断、进度反馈与断点续传,必须采用io.copy流式写入、context控制生命周期、自定义reader统计字节,并通过临时文件+原子重命名保障写入安全。

为什么不能直接用 http.Get + io.ReadAll
因为 io.ReadAll 会把整个响应体读进内存,下载 500MB 文件就可能吃光服务端内存;而真实场景中文件大小不可预知,还常伴随超时、重试、进度回调等需求。封装下载器的第一步,就是彻底放弃“全量加载”模式。
核心结构:io.Copy + 自定义 Reader + Context 控制
一个可复用的下载器必须能中断、能反馈、能限流。关键不是写死逻辑,而是把三件事解耦:
-
http.Client带Context.WithTimeout或Context.WithCancel,控制请求生命周期 - 响应体
resp.Body包装成带计数的io.Reader(比如用io.TeeReader或自定义类型),每次Read都更新已读字节数 - 目标文件用
os.OpenFile以os.O_CREATE | os.O_WRONLY | os.O_APPEND模式打开,支持断点续传
别提前写 Content-Length 到响应头——流式下载器不负责 HTTP 响应,它只管把 bytes 从网络搬到磁盘。
断点续传的两个硬性检查点
客户端续传失败,90% 是卡在这两处:
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
- 本地文件末尾是否完整?
os.Stat返回的Size()只是字节数,但若上次写入中断在中间(比如磁盘满),实际末尾数据可能损坏;建议加简单校验(如读最后 1KB 是否可解析)或强制从头重下 - 服务端是否真支持
Range?发HEAD请求检查响应头是否有Accept-Ranges: bytes,没有就别设Range头,否则可能返回 200 + 全量内容,导致重复写入
设置 req.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset)) 后,必须检查响应码是 http.StatusPartialContent(206),不是 200 —— 否则说明服务端忽略 Range,得 fallback。
临时文件与原子替换的坑
写入中途崩溃,不能留个半成品覆盖原文件。正确做法是:
- 临时文件路径必须和目标路径在同一目录:
tempPath := filepath.Join(filepath.Dir(dest), "."+filepath.Base(dest)+".tmp") - 写完后调
tempFile.Close(),再os.Rename(tempPath, dest) - 如果
os.Rename返回syscall.EXDEV,说明跨设备(比如/tmp是 tmpfs,而目标在/home),此时退化为io.Copy+os.Chmod+os.Chtimes手动复制元数据
别省这一步:生产环境里,一次非原子写入可能让下游系统加载到截断文件,问题比慢更难排查。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










