应使用 os.open + bufio.scanner 边读边处理日志,避免 os.readfile 全量加载导致 oom;需调用 scanner.buffer 设置足够大的缓冲,scanner.text() 安全而 bytes() 需注意底层共享;禁止在 scan 循环中阻塞,应发至 channel 交由独立 goroutine 处理;写入须用 bufio.writer 批量刷盘并显式 flush;并发处理时统一 waitgroup 控制、固定写入协程数,并为关键日志直写同步文件。

别用 os.ReadFile 读日志文件
它会把整个文件 malloc 到内存,400 万行日志轻松吃掉 1–2GB 堆空间,Go 进程直接被系统 OOM kill。这不是 bug,是设计使然——os.ReadFile 只适合 config.json 这类几 KB 的小文件。
- 真实场景要的是“边读边处理”,不是“全量加载完再干活”
- 用
os.Open+bufio.Scanner是最省心的起点:每行只保留当前副本,内存恒定在几 MB 级别 - 默认单行上限 64KB,遇到含 base64 或超长 traceID 的日志会 panic 报
scanner: token too long,必须提前调scanner.Buffer(make([]byte, 64*1024), 10*1024*1024) -
scanner.Text()安全(已拷贝),scanner.Bytes()快但共享底层缓冲——若存进 map 或 slice,整块缓冲区都活不过 GC
bufio.Scanner 里别做阻塞操作
在 for scanner.Scan() 循环里同步发 HTTP 请求、直写数据库或没设 timeout 的 time.Sleep,会导致整条流水线卡死——后续所有行都堵在 Scanner 缓冲区里,吞吐归零。
- 真正该做的:把解析后的行发到
chan *LogEntry,交给独立 goroutine 处理 - channel 缓冲大小设为 100~1000,太小易阻塞,太大吃内存(每行字符串 × 缓冲数)
- 处理逻辑涉及全局计数器?必须用
sync/atomic或sync.Mutex,bufio.Scanner本身非并发安全 - 记得检查
scanner.Err():文件权限错误、磁盘损坏等不会触发 panic,但会静默跳过剩余内容
写入日志别每行一次 os.File.Write
每行调一次 fmt.Fprintln(f, line),等于每行触发一次系统调用,实测百万行写入耗时翻 3–5 倍,CPU 全耗在 syscall 上。
- 正确姿势:包一层
bufio.NewWriterSize(f, 1
- 批量刷盘后必须显式
w.Flush(),否则最后一块数据永远滞留缓冲区 - 若目标是实时看日志(如
os.Stderr),可定期log.Writer().(*bufio.Writer).Flush(),别等程序退出 - 不要用
defer w.Flush()—— 它在函数 return 后才执行,而写入失败可能发生在中间,需逐块检查错误
并发处理多个大日志文件时,别让 WaitGroup 和 channel 互相锁死
常见错误是:在一个文件处理 goroutine 里启动一堆 processRow,又用另一个 WaitGroup 等它们,最后在循环里 wg2.Wait() —— 这会让整个文件处理串行化,彻底废掉并发。
- 只用一个
sync.WaitGroup控制“所有行处理完成”,然后close(ch)通知写入协程退出 - 每个输入文件启动独立 goroutine 调
processFileByPath(),并在其内部打开/关闭文件(别传*os.File出去) - 写入协程数量固定为 4–16 个,再多无益——磁盘 I/O 是瓶颈,不是 CPU;可通过压测调整
numWriters - 关键错误日志(如 DB 连接中断)不能走异步 channel,必须绕过缓冲直写
os.O_SYNC文件并强制落盘
真正难的不是开多少 goroutine,而是让读、算、写三阶段不互相拖累,且每一步的资源边界清晰可控——比如 channel 缓冲大小、writer 缓冲大小、scanner 最大行长,这些数字一旦设错,轻则性能抖动,重则内存失控或静默丢日志。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











