
本文介绍使用 bufio.scanner 高效读取文件并逐行匹配目标字符串,同时准确返回每一处匹配的行号;支持多匹配场景,并给出并发处理海量文件的扩展思路。
本文介绍使用 bufio.scanner 高效读取文件并逐行匹配目标字符串,同时准确返回每一处匹配的行号;支持多匹配场景,并给出并发处理海量文件的扩展思路。
在 Go 中,直接将整个文件读入内存(如用 ioutil.ReadFile 或 os.ReadFile)再用 strings.Contains 检查,虽简单但无法获取行号,且对大文件不友好、内存开销高。更合理的方式是流式逐行扫描——借助 bufio.Scanner 按行读取,配合手动行号计数,实现轻量、准确、可扩展的字符串定位。
以下是一个完整、健壮的实现示例,支持返回所有匹配行号(而不仅是首个):
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
// FindStringLines 返回文件中所有包含 target 的行号(从 1 开始计数)
func FindStringLines(path, target string) ([]int, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to open %s: %w", path, err)
}
defer f.Close()
var lines []int
scanner := bufio.NewScanner(f)
lineNum := 1
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, target) {
lines = append(lines, lineNum)
}
lineNum++
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading %s: %w", path, err)
}
return lines, nil
}
// 示例用法
func main() {
matches, err := FindStringLines("example.txt", "TODO")
if err != nil {
panic(err)
}
for _, ln := range matches {
fmt.Printf("Found at line %d\n", ln)
}
}
✅ 关键优势说明:
- ✅ 内存友好:无论文件大小,仅缓存当前行,避免 O(N) 内存占用;
- ✅ 行号精确:lineNum 严格按 \n 分割逻辑递增,与编辑器显示一致(首行为 1);
- ✅ 多匹配支持:使用切片 []int 收集全部命中行号,而非提前 return;
- ✅ 错误可追溯:scanner.Err() 显式检查 I/O 错误,并包装路径信息便于调试。
⚠️ 注意事项:
- Scanner 默认按行分割(bufio.ScanLines),自动剥离 \n 或 \r\n,因此 scanner.Text() 不含换行符,适合纯字符串匹配;
- 若需区分大小写或正则匹配,可替换 strings.Contains 为 strings.ContainsFold 或 regexp.MustCompile(...).MatchString(line);
- 对超长行(> 64KB),需调用 scanner.Buffer(make([]byte, 0), maxLineSize) 手动扩容,否则会报 bufio.Scanner: token too long 错误。
? 进阶:批量扫描千级文件?
当需在成百上千个文件中搜索时,可构建goroutine 工作池(Worker Pool),将每个文件的 FindStringLines 调用作为任务分发,结合 sync.WaitGroup 与 channel 汇总结果。这能显著提升 I/O 密集型任务吞吐量,同时避免 goroutine 泛滥——具体实现可参考 Go by Example: Worker Pools。
总之,bufio.Scanner + 行号计数是 Go 中文本行定位的标准实践:简洁、高效、可控。摒弃“全量加载+全局搜索”的惯性思维,拥抱流式处理,才能写出真正生产就绪的文件搜索逻辑。











