bufio.scanner逐行扫描最稳,自动适配\n\r\n换行符、内存恒定,但需显式扩容缓冲区防超长行panic,并必须检查scanner.err()捕获i/o错误;小文件行数统计可用os.readfile+bytes.count提速20%~30%,但仅限≤10mb。

用 bufio.Scanner 逐行扫描最稳,但得防超长行 panic
直接 strings.Count 统计 "\n" 是常见误区:它漏掉末尾无换行符的那行,也不识别 \r\n。真正符合“人类行”语义的,还是 bufio.Scanner —— 它自动适配不同换行符,内存恒定,小到中等文件(几 MB 到几百 MB)首选。
但默认缓冲区 64KB,遇到嵌入 base64 或 minified JSON 的超长行会报 scanner: token too long。必须显式扩容:
scanner := bufio.NewScanner(f) scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 1MB 缓冲,上限同设
别只调 scanner.Scan() 就完事 —— 必须检查 scanner.Err(),否则 I/O 错误(如权限拒绝、磁盘损坏)会被静默吞掉。
os.ReadFile + bytes.Count 更快,但只适合小文件
行数统计本质是计数,不是流式处理。os.ReadFile 整文件加载后用 bytes.Count(b, []byte("\n")) 加上末尾判断,比 bufio.Scanner 少一次内存拷贝,实测快 20%~30%。
适用前提很明确:
- 文件大小建议 ≤ 10MB;超过 50MB 就容易触发 GC 压力或 OOM
- 必须先清除 UTF-8 BOM:
b = bytes.TrimPrefix(b, []byte("\xef\xbb\xbf")) - 末尾无换行符要补 1:
if len(b) > 0 && b[len(b)-1] != '\n' { count++ }
别用 strings.Split(string(b), "\n") —— 分配大量小字符串,GC 开销大,纯属拖慢速度。
过滤空行和注释行时,别碰多行注释状态机
Go 源码里 /* */ 跨行极少出现在函数体内部,真实项目中占比低于 0.1%。为这点边缘情况写完整状态机,反而引入 bug 风险和维护成本。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
实际只需处理两类:
- 空行:
strings.TrimSpace(line) == "" - 单行注释:
strings.HasPrefix(strings.TrimSpace(line), "//")
注意:第一行可能带 BOM,所以 TrimSpace 前不必单独切 BOM —— TrimSpace 已覆盖 \uFEFF;但若用 os.ReadFile 方案,BOM 还是得在 bytes.Count 前清除,否则会影响首行判断。
filepath.WalkDir 遍历时,过滤逻辑必须放回调里
想跳过 vendor、node_modules 或 *_test.go?不能靠外部 if 提前 return —— filepath.WalkDir 的递归控制权全在回调返回值手里。
正确姿势:
- 遇到不想进的目录(如
entry.Name() == "vendor"),回调里return filepath.SkipDir - 遇到不想统计的文件(如
!strings.HasSuffix(entry.Name(), ".go")),回调里return nil - 后缀判断永远用
entry.Name(),不是path—— 后者是全路径,strings.HasSuffix(path, "_test.go")会误判/a/b/c_test.go
软链接默认不跟随,如果真需要,得在 entry.Info() 后手动判断是否为 symlink 再决定是否继续 walk —— 大多数代码统计场景根本不需要,开了反而可能钻进循环链接或挂载点。
真正难搞的不是行数本身,而是“什么算一行有效代码”的定义边界 —— 空行、BOM、超长行、软链接、权限错误、混合换行符……每个点都可能让结果差一倍。别追求一次性覆盖所有 corner case,先按项目实际约定(比如“只统计非测试文件里的非空非注释行”)落地,再根据反馈迭代。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










