必须用os.Open+io.CopyN流式分块处理大文件,避免os.ReadFile一次性加载导致OOM;io.CopyN可精确控制每块字节数,自动处理最后一块不足的情况,且无需预知文件总大小。

用 os.Open 和 io.CopyN 分块读取大文件
直接 os.ReadFile 会把整个文件加载进内存,几 GB 的文件极易触发 OOM。必须流式处理:打开文件句柄后,每次只读固定字节数到缓冲区再写入新文件。
关键不是“分多少块”,而是“每块读多少字节”——推荐用 io.CopyN 而非手动 Read 循环,它能精确控制拷贝字节数,避免最后一块多读或少读:
src, _ := os.Open("big.log")
defer src.Close()
<p>buf := make([]byte, 10<em>1024</em>1024) // 10MB buffer
for i := 0; ; i++ {
dst, <em> := os.Create(fmt.Sprintf("chunk</em>%d.bin", i))
n, err := io.CopyN(dst, src, 10<em>1024</em>1024) // 精确拷贝 10MB
dst.Close()
if err == io.EOF || n < 10<em>1024</em>1024 {
break // 最后一块不足 10MB,或已读完
}
}注意:io.CopyN 在源数据不足时返回 io.EOF,但不会报错;若你用 Read + Write 手动拼接,容易因 Read 返回 n 却误判为错误而中断。
分块写入时文件名和边界要严格对齐
分块后若需合并回原文件,必须保证每块大小(除最后一块)完全一致,且写入顺序不可乱。常见陷阱是用 os.WriteFile 替代 os.Create + Write,后者会覆盖已有文件,前者可能因并发写入导致文件内容错位。
立即学习“go语言免费学习笔记(深入)”;
建议统一用以下模式生成文件名和校验逻辑:
- 文件名带零填充序号:
chunk_00001.bin,方便ls或glob按序读取 - 每块写入后调用
dst.Sync()确保落盘(尤其在 NFS 或云存储挂载点上) - 记录元信息到单独的
manifest.json,包含每块的size、md5和offset,而不是仅靠文件名推算偏移
处理超大文件(>100GB)时避免 os.Stat 获取总大小
os.Stat 对某些文件系统(如 CIFS、FUSE)可能阻塞数秒甚至失败,且对管道/设备文件不适用。不要依赖 fi.Size() 计算总块数——它只是个 hint,实际应以读取结束为准。
如果业务确实需要预知块数(比如进度条),可用 syscall.Stat_t 尝试获取,但必须加超时和 fallback:
var stat syscall.Stat_t
if err := syscall.Stat("/path/to/file", &stat); err == nil {
total := stat.Size
chunkSize := int64(10 * 1024 * 1024)
expectedChunks := (total + chunkSize - 1) / chunkSize
}但注意:stat.Size 在 ext4 上可靠,在某些网络文件系统上可能返回 0 或负值,此时应降级为边读边计数。
Windows 下注意 os.O_CREATE | os.O_TRUNC 的原子性缺失
在 Windows 上,os.Create 不是原子操作:先删旧文件,再新建,期间若进程崩溃,可能残留部分写入的空文件。生产环境若需强一致性,应写入临时文件(如 chunk_001.bin.tmp),写完调用 os.Rename 提交 —— 这个操作在 NTFS 上是原子的。
同时注意路径分隔符:filepath.Join 自动适配平台,别硬写 "\" 或 "/";还有文件句柄泄漏风险——每个 os.Create 后必须显式 Close,哪怕用了 defer,也要确保 defer 在正确作用域内(比如放在 for 循环内部,而非外部)。
最后一块的大小永远由实际读取结果决定,任何基于文件总大小的预分配都可能出错;分块逻辑越简单,越不容易在边缘 case(如空文件、权限变化、磁盘满)下静默失败。


















