最省内存的方式是用 io.Copy 流式中转 resp.Body 到文件,避免 io.ReadAll 导致内存暴涨;需预建目录、检查错误、合理使用 bufio.Writer 缓冲,并注意并发安全与超时控制。

用 io.Copy 直接写入文件,避免内存堆积
爬虫抓取的响应体(如 resp.Body)本质是 io.ReadCloser,而文件句柄(*os.File)是 io.Writer。最直接、最省内存的方式就是用 io.Copy 流式中转,不经过中间缓冲或字符串解码。
常见错误是先 io.ReadAll(resp.Body) 再写入,这对大响应(比如几十 MB 的 PDF 或 JSONL)会瞬间吃光内存。
- 务必在
defer file.Close()前检查io.Copy返回值,失败时file可能已部分写入 - 如果目标路径父目录可能不存在,需提前调用
os.MkdirAll(filepath.Dir(filename), 0755) -
io.Copy默认使用 32KB 缓冲区,对大多数场景足够;极端吞吐需求可换io.CopyBuffer自定义
file, err := os.Create("data.html")
if err != nil {
log.Fatal(err)
}
defer file.Close()
_, err = io.Copy(file, resp.Body)
if err != nil {
log.Fatal("写入失败:", err) // 注意:此时 file 已打开,但内容可能不完整
}
需要边解析边写入时,用 bufio.Writer 手动 flush
比如爬取的是分块 JSON(每行一个对象),你想过滤字段后再写入,就不能全量读取——得用 scanner 或 json.Decoder 流式处理,再逐条写入。
这时别直接往 *os.File 写,否则每次 WriteString 都是一次系统调用,性能差。套一层 bufio.Writer 能显著提升吞吐。
立即学习“go语言免费学习笔记(深入)”;
- 记得在最后调用
wr.Flush(),否则末尾数据可能滞留在缓冲区未落盘 - 缓冲区大小设为 64KB(
bufio.NewWriterSize(file, 65536))比默认 4KB 更适合网络流场景 - 若写入过程中出错(如磁盘满),
Flush()会返回错误,需检查
file, _ := os.Create("filtered.jsonl")
defer file.Close()
wr := bufio.NewWriterSize(file, 65536)
defer wr.Flush() // 注意:这行不能省,且要放在 defer 链靠后位置
dec := json.NewDecoder(resp.Body)
for {
var item map[string]interface{}
if err := dec.Decode(&item); err == io.EOF {
break
} else if err != nil {
log.Fatal(err)
}
if _, ok := item["valid"]; ok && item["valid"] == true {
json.NewEncoder(wr).Encode(item) // Encode 自动换行
}
}
wr.Flush() // 显式 flush 确保最后一段写入
并发写入多个文件时,每个文件必须独立打开和关闭
Go 的 *os.File 是线程安全的,但如果你让多个 goroutine 共享同一个 *os.File 并发写,虽然不会 panic,但内容会交错、错乱——因为 write 操作不是原子的,且无内部锁保证顺序。
正确做法是:每个 goroutine 拿到自己的 URL 和目标路径后,自己调用 os.Create,写完立刻 Close。
- 不要用
sync.Pool复用*os.File,文件句柄是操作系统资源,复用反而易导致bad file descriptor - 大量小文件写入时,注意系统
ulimit -n限制,必要时加runtime.GC()配合file.Close()释放 fd - 若需按目录归类,路径拼接务必用
filepath.Join(dir, name),避免手动拼"/"导致跨平台问题
HTTP 响应未指定 Content-Length 时,io.Copy 仍可靠
有些服务(尤其是流式 API 或 chunked transfer)不带 Content-Length,甚至用 Transfer-Encoding: chunked。这时 resp.Body 是一个自动处理 chunk 的 reader,io.Copy 完全兼容——它只依赖 Read 方法返回 io.EOF 作为结束信号,不关心底层是否分块。
唯一要注意的是超时控制:如果服务端卡住不发 EOF,io.Copy 会一直阻塞。所以必须设置 http.Client 的 Timeout 或 Context。
- 推荐用带 cancel 的 context:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - 创建 request 时传入:
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) - 一旦超时,
resp.Body.Read会立即返回context.DeadlineExceeded错误
流式写入这件事本身很简单,难的是边界情况:中断恢复、编码识别、重试策略、磁盘空间预估。别在写文件这一步做额外假设,把责任交给上游(HTTP client)和下游(OS 文件系统)各自处理好。


















