Go统计文件行数应优先用bufio.Scanner(自动处理换行符、内存恒定),大文件改用bufio.NewReaderSize+ReadLine提升性能,内存字符串才可用strings.Count配合后缀判断;须注意wc -l与人类行定义差异及错误处理。

Go 语言没有内置的 CountLines 函数,直接用 strings.Count 统计 "\n" 是常见误区——它会漏掉末尾无换行符的那行,且不处理 \r\n。真正靠谱的统计必须结合换行语义,而非单纯字符计数。
用 bufio.Scanner 逐行扫描(小到中等文件首选)
这是最符合“文本行”语义的做法:自动识别 \n、\r\n、\r(取决于系统和 bufio.Scanner 的底层行为),且内存恒定、不易出错。
- 适用于文件大小在几 MB 到几百 MB 级别(比如日志、配置、源码)
- 每行内容调用
scanner.Text()后,scanner.Scan()内部已跳过换行符,所以只需累加计数器 - 必须检查
scanner.Err(),否则 I/O 错误(如权限不足、磁盘损坏)会被静默忽略 - 不要用
scanner.Bytes()后自己找\n——这绕过了 scanner 的换行处理逻辑,退化为原始字节操作
示例片段:
func CountLinesScanner(path string) (int, error) {
f, err := os.Open(path)
if err != nil {
return 0, err
}
defer f.Close()
<pre class="brush:php;toolbar:false;">var count int
scanner := bufio.NewScanner(f)
for scanner.Scan() {
count++
}
return count, scanner.Err()}
用 io.Readline + 自定义缓冲(大文件提速关键)
当文件超过 1GB,bufio.Scanner 默认缓冲区(64KB)可能引发大量系统调用,拖慢速度。此时应显式增大缓冲区,并改用 reader.ReadLine() ——它比 Scan() 更轻量,不拷贝行内容,只返回字节切片视图。
立即学习“go语言免费学习笔记(深入)”;
- 推荐缓冲区设为
1 * 1024 * 1024(1MB),实测对 SSD 上 60GB 文件可将耗时从 81 秒降至 37 秒 -
reader.ReadLine()返回的isPrefix == true表示该行超长被截断,需拼接;但纯行数统计时可忽略(因为截断本身不产生新行) - 遇到
io.EOF时,最后一行已计入,无需额外判断 - 避免用
bufio.NewReader(f)不带 size 参数——它会回退到默认 4KB 缓冲,性能损失明显
示例片段:
func CountLinesReadLine(path string) (int, error) {
f, err := os.Open(path)
if err != nil {
return 0, err
}
defer f.Close()
<pre class="brush:php;toolbar:false;">reader := bufio.NewReaderSize(f, 1024*1024)
var count int
for {
_, isPrefix, err := reader.ReadLine()
if err == io.EOF {
break
}
if err != nil {
return 0, err
}
if !isPrefix { // 只有非前缀行才算完整一行(实际中绝大多数情况都满足)
count++
}
}
return count, nil}
用 strings.Count + 后缀判断(仅限内存内字符串)
如果你已经把整个文件读进内存(比如用 os.ReadFile),且确定内容是纯 Unix 换行(\n),可用此法——它比逐行扫描快,但前提是“已加载”。直接对大文件这么做等于 OOM。
- 核心逻辑:
strings.Count(s, "\n") + (0 或 1),其中加 1 的条件是!strings.HasSuffix(s, "\n") && len(s) > 0 - 空字符串
""返回 0,单字符"\n"返回 1,"a"返回 1,"a\n"返回 1——全部符合直觉 - 若原始数据含
\r\n,必须先strings.ReplaceAll(s, "\r\n", "\n"),再替换残留\r,否则多算 - 不要对
[]byte直接用strings.Count(string(b), ...)——小文件无所谓,大文件会触发一次不必要的内存分配
容易被忽略的边界细节
多数人卡在“为什么我的统计比 wc -l 少一行”。根本原因不是代码写错,而是对“行”的定义不一致:
-
wc -l统计的是\n个数,不管文件是否以\n结尾;而人类认为“最后一行即使没换行也是一行”——Go 程序应按后者实现 - Windows 文件末尾若有
\r\n,bufio.Scanner会正确识别为一行;但若用strings.Count(..., "\n")前没做\r\n → \n归一化,就会少算 - 文件开头或中间含空行(仅
\n或空白符+\n),它们都是合法行,不应跳过——除非你明确要统计“非空行”,那就得在循环里加strings.TrimSpace(line) != "" - 符号链接、权限拒绝、设备文件等场景下,
os.Open报错必须透出,不能吞掉或 fallback 到 0


















