大文本文件无法全量加载进内存时,sort.Strings 会直接 panic;必须采用外部排序:分块读取、本地排序、写临时文件、归并排序;需处理 bufio.Scanner 超长行误判 EOF 问题。

大文本文件无法全量加载进内存时,sort.Strings 会直接 panic
Go 标准库的 sort.Strings 要求所有行必须一次性读入内存,一旦文件超过几百 MB(取决于可用 RAM),就会触发 runtime: out of memory 或 fatal error: runtime: cannot allocate memory。这不是排序逻辑的问题,而是内存模型的硬限制——你不能对一个 10GB 的 []string 调用 sort.Strings。
真正可行的路径只有一条:外部排序(External Sort),即分块读取 → 本地排序 → 写临时文件 → 归并多个已排序的临时文件。
- 每块大小建议控制在 50–200MB(根据机器内存动态估算,比如
runtime.MemStats.Alloc可作参考) - 临时文件名必须唯一,推荐用
os.CreateTemp("", "sort-*.tmp"),避免并发冲突或残留文件 - 每块排序后立即写入磁盘并
close()+sync(),防止缓冲区堆积或崩溃丢数据
用 bufio.Scanner 分块读取时,Scan() 失败不等于 EOF
bufio.Scanner 默认缓冲区只有 64KB,遇到超长行(比如日志中带 base64 的单行记录)会直接返回 false 并把错误设为 bufio.ErrTooLong。此时 scanner.Err() 不为 nil,但 scanner.Scan() 已停止,容易误判为文件结束。
- 务必在每次
Scan()后检查scanner.Err(),而不仅是判断!Scan() - 超长行处理策略要提前约定:截断?跳过?报错退出?例如用
scanner.Buffer(make([]byte, 64*1024), 1 把最大行长提到 1MB - 逐行读取过程中,每积累约 100 万行(或达到预设内存阈值)就触发一次分块排序落盘,不要等
Scan()返回 false 才行动
归并多个已排序临时文件,别手写堆逻辑 —— 用 heap.Init 管理文件游标
归并 N 个已排序文件,核心是维护 N 个文件的当前读取位置,并每次取出最小值。手动轮询比较效率低且易错;标准库 container/heap 是更稳的选择,但要注意它要求实现 heap.Interface,且元素必须是可修改的指针或结构体。
立即学习“go语言免费学习笔记(深入)”;
典型结构体示例:
type LineReader struct {
scanner *bufio.Scanner
line string
done bool
}
func (lr *LineReader) Less(other *LineReader) bool { return lr.line < other.line }
// ……还需实现 Push/Pop/Swap
- 每个
LineReader对应一个临时文件,初始化时先Scan()一次预读首行 - 归并循环中,每次从堆顶取最小
line,输出后对该LineReader再次Scan(),失败则标记done = true并从堆中移除 - 注意:
heap.Pop会调用Swap和Down,所以LineReader必须是指针类型,否则修改无效
临时文件清理和错误恢复几乎总是被忽略
外部排序中途崩溃(如磁盘满、kill -9)会导致临时文件残留,下次运行可能因同名冲突或磁盘空间不足失败。更隐蔽的问题是:归并阶段某个临时文件损坏或提前 EOF,程序若没做校验,会静默输出乱序结果。
- 所有临时文件路径统一由
os.RemoveAll在 defer 中注册清理,但需加if !finalSuccess { ... }判断,避免成功后误删 - 每个临时文件写入后执行
os.Stat+os.File.Sync(),确保落盘完整 - 归并前对每个临时文件做轻量校验:读前 3 行确认是否升序(
line[i] ),异常则报错退出,不继续归并
外部排序真正的复杂点不在算法本身,而在边界控制:内存水位、行长度、文件完整性、中断恢复。这些地方松一点,结果就不可靠;严一点,性能又掉得厉害。平衡点得靠实测,不是靠理论估算。


















