默认的 bufio.scanner 读不到换行符以外的协议边界,因其默认使用 bufio.scanlines,仅识别 \n 或 \r\n;需自定义 split 函数(如支持 smtp/pop3 的 \r\n.\r\n),注意处理跨缓冲区匹配与行首敏感逻辑。

为什么默认的 bufio.Scanner 读不到换行符以外的协议边界
因为 bufio.Scanner 默认用 bufio.ScanLines 作分隔逻辑,它只认 \n(或 \r\n),对自定义帧头、长度前缀、特殊结束标记(比如 "\r\n.\r\n")完全无感。你看到 scanner.Token() 总是截断在第一个换行,不是 bug,是设计如此。
解决办法是传入自定义的分割函数——但注意:Scan() 内部会缓存数据,如果分隔符跨缓冲区(比如分隔符横跨两个 4KB 块),你的函数必须能处理「不完整匹配」,否则直接丢数据。
- 用
scanner.Split()注册函数,该函数签名必须是func(data []byte, atEOF bool) (advance int, token []byte, err error) -
atEOF为true时,表示底层Reader已无新数据,此时若还没找到分隔符,得决定是返回剩余数据还是报错 - 别在分割函数里做字符串解码(如
string(data)),避免重复分配;用bytes.Index或手动字节比对更稳
怎么写一个支持 "\r\n.\r\n" 的邮件协议分隔函数
SMTP/POP3 协议常用 "\r\n.\r\n" 标记消息体结束,这个分隔符有 5 字节,且中间含点号,不能简单用 bytes.Contains 扫描——它可能误触发在正文里的孤立 "\r\n.\r\n"(比如用户发了一行只有英文句点的内容)。
正确做法是:只在行首位置检查 "\r\n." 后跟 \r\n,也就是模拟真实协议解析器的「行边界敏感」行为。
立即学习“go语言免费学习笔记(深入)”;
func dotCRLF(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
for i := 0; i < len(data)-4; i++ {
if data[i] == '\r' && data[i+1] == '\n' && data[i+2] == '.' && data[i+3] == '\r' && data[i+4] == '\n' {
return i + 5, data[0:i], nil
}
}
if atEOF {
return len(data), data, nil // 返回剩余未结束的数据
}
return 0, nil, nil // 暂不推进,等更多数据
}
- 这个函数不拷贝原始
data,token是切片引用,零分配 - 没找到分隔符且
atEOF==false时返回(0, nil, nil),scanner 会继续读;这是关键,漏掉就卡死 - 如果协议要求严格校验「点转义」(如
".."表示真实句点),那得在提取 token 后额外做一次解码,分割函数本身不负责
bufio.Scanner 的缓冲区大小和超限错误怎么调
默认缓冲区是 64KB,遇到超长行(比如 base64 编码的大附件)会直接报 bufio.ErrTooLong,而不是继续扫描。这不是分隔逻辑问题,是内存保护机制。
调缓冲区要两步:先用 scanner.Buffer() 设上限,再确保底层 io.Reader 能持续供数(比如不要用 strings.NewReader 模拟大文本,它不体现流式压力)。
-
scanner.Buffer(make([]byte, 4096), 10*1024*1024)表示初始 4KB、上限 10MB - 设太大可能 OOM;设太小会导致频繁 realloc,实测 1–4MB 在多数协议解析中较平衡
-
ErrTooLong是error类型,不是 panic,必须显式检查scanner.Err(),否则静默失败 - 如果协议本身禁止超长帧(如 HTTP/2 帧最大 16KB),那应该在业务层校验,而非靠 scanner 缓冲区硬扛
用 bufio.Scanner 解析二进制协议容易踩哪些坑
它本质是面向文本设计的,所有分隔逻辑都基于字节比较,但二进制协议常含 \x00、控制字符、不定长字段——这时 scanner.Split() 仍可用,只是你写的分割函数得更小心。
- 别依赖
strings包函数(如strings.Index),它们把[]byte转string会复制且对\x00不友好;坚持用bytes.Index或手写循环 - 如果分隔符本身是变长的(如 TLV 中的 length 字段后跟 payload),
scanner.Split就不适用了——它只能按「已知字节模式」切,无法动态读取长度再跳;此时应直接用bufio.Reader.Read()+ 手动状态机 - 注意字节序:如果你的分隔符含多字节整数(如 0x0000FFFF),别直接 memcmp,先用
binary.BigEndian.Uint16()解出来再比
真正难的从来不是写对一个分隔函数,而是确认协议文档里那个「结束标记」到底有没有例外场景——比如是否允许嵌套、是否区分大小写、是否在注释块里失效。这些细节不厘清,代码写得再漂亮也白搭。










