应使用 bufio.Scanner 流式逐行读取超大日志文件,配合 os.Open 和预分配缓冲区(如 scanner.Buffer(buf, 1MB))避免 OOM;关键词过滤优先用 strings.Contains,多词用 || 连接;匹配即刻写入 writer,避免内存累积;通过 file.Seek(0, io.SeekCurrent) 跟踪进度;需注意日志滚动导致的静默终止问题。

用 bufio.Scanner 逐行读取,别用 os.ReadFile
超大日志文件(比如几个 GB)直接加载进内存会 OOM,os.ReadFile 或 io.ReadAll 必须排除。核心是流式处理:用 bufio.Scanner 搭配 os.Open,它默认按行缓冲,内存占用稳定在 KB 级别。
注意默认扫描上限是 64KB 行长,如果日志里有超长行(比如带完整 stack trace 的单行错误),会报 scanner: too long 错误。得显式调大缓冲区:
scanner := bufio.NewScanner(file) buf := make([]byte, 64*1024) // 改成 64KB scanner.Buffer(buf, 1024*1024) // 最大支持 1MB 行长
- 第二个参数是最大行长度,设太小会 panic,设太大又浪费内存,按日志实际最长行预估
- 别在循环里反复
make([]byte),提前分配一次复用 - 文件打开后记得
defer file.Close(),否则句柄泄漏
过滤逻辑放循环内,用 strings.Contains 而非正则
关键词过滤多数场景是子串匹配(比如找 "ERROR"、"timeout"),strings.Contains 比 regexp.MatchString 快 5–10 倍,且无编译开销。只有当需要模糊匹配(如 "ERR.*timeout")才上正则,且务必预编译:
var errRegex = regexp.MustCompile(`(?i)error|timeout|panic`)
-
strings.Contains(line, keyword)区分大小写;需要忽略大小写就用strings.Contains(strings.ToLower(line), strings.ToLower(keyword)) - 多个关键词用
||连接比切片遍历快,尤其关键词少于 5 个时 - 避免在每次循环里调用
strings.ToLower多次——先转一次再查多个词
输出结果别拼接字符串,直接写到 io.Writer
如果把匹配行全存进 []string 再统一输出,又回到内存堆积的老路。正确做法是匹配到一行就立刻写出去:
if strings.Contains(line, "ERROR") {
_, _ = writer.Write([]byte(line + "\n"))
}
-
writer可以是os.Stdout、*os.File或bytes.Buffer,解耦输入输出 - 用
writer.Write而非fmt.Fprintln,减少格式化开销 - 错误检查不能省:
if _, err := writer.Write(...); err != nil { return err },尤其写磁盘时可能 ENOSPC
加进度提示?用 file.Stat().Size() 配合字节偏移
纯按行无法知道“读了百分之几”,因为行长短不一。可靠方式是跟踪当前文件偏移:
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, keyword) {
// ... 输出
}
offset, _ := file.Seek(0, io.SeekCurrent) // 获取当前读取位置
// 计算进度:float64(offset) / float64(totalSize)
}
-
file.Seek(0, io.SeekCurrent)是唯一轻量获取偏移的方法,比每次file.Stat()快得多 - 首次
file.Stat()拿总大小即可,别在循环里反复调用 - 进度打印频率要控制,比如每 1% 打一次,避免 IO 拖慢主流程
真正难的是日志滚动——函数运行中途文件被 logrotate 清空或重命名,scanner.Scan() 会静默结束。这种边界情况得靠外部信号或额外监控,Go 标准库本身不处理。


















