scanner默认按\n分隔且吃掉换行符,只返回换行前内容;需自定义splitfunc才能保留换行符或按其他规则(如\r\n、|||)切分,函数必须严格匹配签名并正确处理ateof和不完整数据。

Scanner默认行为为什么读不到换行符
因为 bufio.Scanner 默认以 \n 为分隔符,每次调用 Scan() 都会吃掉换行符,并只返回换行前的内容。如果你需要保留换行符、或者按其他规则切分(比如按空格、按固定长度、甚至按正则),就不能依赖默认行为。
常见错误现象:读取日志行时发现末尾换行丢失;处理协议帧时无法按 \r\n 精确截断;想逐字符扫描却卡在第一行不动。
- 根本原因在于
Scanner.Split()的默认分割函数是bufio.ScanLines,它内部跳过换行符且不返回 - 必须显式调用
Split()并传入自定义分割函数,才能改变这一行为 - 自定义函数签名必须是
func(data []byte, atEOF bool) (advance int, token []byte, err error)
如何写一个按 \r\n 切分的 Split 函数
HTTP 报文、SMTP 协议等常用 \r\n 作行结束,而 ScanLines 只认 \n,直接导致读取不准确——比如把 "HTTP/1.1 200 OK\r\n" 拆成两段。
下面是一个安全、可复用的 \r\n 分割函数:
func scanCRLF(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.Index(data, []byte("\r\n")); i >= 0 {
return i + 2, data[0:i], nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
}
使用时只需:scanner.Split(scanCRLF)。注意三点:
- 必须在
Scan()调用前设置,之后设置无效 - 返回的
token不含\r\n,如需保留,改为data[0:i+2] - 如果数据中可能含二进制内容(非 UTF-8),
bytes.Index仍安全,但避免用strings包
ScanBytes 和 ScanRunes 为什么不能直接替代自定义 Split
有人试图用 bufio.ScanBytes 逐字节读,或用 bufio.ScanRunes 逐字符读,来绕过行切分逻辑——这容易引发缓冲区和性能问题。
关键区别:
-
ScanBytes每次返回一个字节的[]byte,但底层仍按默认缓冲区(4096 字节)填充,频繁小分配导致 GC 压力大 -
ScanRunes在遇到非法 UTF-8 序列时会返回U+FFFD并跳过,无法用于二进制协议解析 - 二者都不控制“语义边界”,比如你想要的是“一个完整的 JSON 对象”,它们完全无能为力
真正需要语义切分(如按 }\n、按固定包头长度、按 TLV 结构)时,唯一可靠方式仍是实现自己的 Split 函数。
Scanner 缓冲区大小不够导致 Scan() 返回 false 却无错误
这是最隐蔽的坑:Scan() 返回 false 时,很多人只检查 Err(),却发现它是 nil,以为读完了,实际是某一行超长被截断了。
默认缓冲区仅 64KB,超过即触发 bufio.ErrTooLong,但该错误**不会通过 Err() 暴露**,而是让 Scan() 直接返回 false,同时 Err() 仍为 nil。
- 务必在循环后加判断:
if err := scanner.Err(); err != nil && err != bufio.ErrTooLong { ... } - 若需支持长行,提前调大缓冲区:
scanner.Buffer(make([]byte, 64*1024), 1(上限 1MB) - 注意:第二个参数是最大令牌长度,设太大可能 OOM;设太小又容易误报
ErrTooLong
自定义 Split 函数里也要检查 atEOF 和数据长度,否则在缓冲区未填满但已到 EOF 时可能漏掉最后一段。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











