登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go textproto.DotReader 怎样还原点转义正文

来源:17golang原创

时间:2026-11-13 22:16:11 457浏览 收藏

Go 的 textproto.Reader.DotReader() 会把点编码正文还原成普通字节流:删除行首用于转义的一个点,把 CRLF 行尾转换为 LF,并吃掉单独一行的结束点。读取完这个正文后,继续使用同一个 textproto.Reader 解析后续响应,避免绕过它共享的缓冲区。

如果你看到正文里多了一个点,或后续响应被误当成正文,先检查代码是否把协议分帧当成了普通字符串处理。下面用一段原创的内存输入说明这个问题;场景用于解释故障,不代表真实线上事故,也没有声称执行过示例。

官方文档:https://pkg.go.dev/net/textproto#Reader.DotReader

症状:正文多一个点,下一条响应却读不到

假设一个按行传输的服务返回报表正文,正文之后还有状态行。业务原本想保留 .note 这样的行首字符,却直接拼接了网络中收到的文本,于是界面显示为 ..note。有人又改成按点切分,结果内容中的普通句点被截断,末尾的状态行也难以定位。

这个示例中的问题可以按阅读顺序拆开:先是把原始行当作正文,随后尝试手工删点,最后发现读取位置已经错开。要修复的是“当前正文的终点和转义规则”,不是删除整篇文本里的点。

根因:传输分帧和正文字符混在了一起

点编码把正文组织成若干以 CRLF 结束的行。单独一行 .\r\n 是正文结束标记;真实正文如果以点开头,发送方会再补一个点。结束标记属于传输层,解码后的正文不应包含它。普通行内的句点没有行首转义含义。

线上的一行(转义写法)还原后的内容含义
report\r\nreport\n仅转换行尾
..note\r\n.note\n去掉一个转义点
...\r\n..\n保留正文原有的两个点
\r\n\n空行仍属于正文
.\r\n无正文内容结束当前文本块

DotReader 的解码职责包括行首转义、换行归一和结束行识别。这三个规则不能由全局字符串替换代替;它也不保留原始 CRLF 字节,因此不适合直接用于要求字节完全一致的原始报文归档。

点编码正文与行首转义、行尾、结束标记的静态关系
原创结构图:点编码的三类组成及解码职责。具体转义规则以正文表格为准,图示不表示执行顺序或运行结果。

修复:让 DotReader 独占当前正文

先创建一个 bufio.Reader,再交给 textproto.NewReader。取得 DotReader() 后,把它读到当前正文的结束,再调用协议读取器的其他方法。下面采用小型固定输入和 io.ReadAll,便于观察正文与后续响应的分界。

package main

import (
    "bufio"
    "fmt"
    "io"
    "log"
    "net/textproto"
    "strings"
)

func main() {
    // 仅构造协议文本:正文结束后还保留一条响应。
    wire := "report\r\n..note\r\n...\r\n\r\n.\r\n250 ready\r\n"
    buffered := bufio.NewReader(strings.NewReader(wire))
    protocol := textproto.NewReader(buffered)

    // 当前正文只能通过该解码器读取,结束标记不会进入 body。
    body, err := io.ReadAll(protocol.DotReader())
    if err != nil {
        log.Fatalf("正文不完整,停止继续解析:%v", err)
    }

    // 必须沿用原来的缓冲读取器,不能另包一层读取底层输入。
    reply, err := protocol.ReadLine()
    if err != nil {
        log.Fatalf("读取后续响应失败:%v", err)
    }
    fmt.Printf("正文=%q\n后续响应=%q\n", body, reply)
}

按规则推导,正文应是字符串 "report\n.note\n..\n\n",后续响应应为 "250 ready"。这里给的是示例输入对应的预期内容,不是本机执行日志。正常的正文结束由 DotReader 返回 io.EOF;io.ReadAll 将这种正常结束表现为成功的 nil 错误。

这里的 protocol 和 buffered 共享同一个缓冲区。底层输入可能已经被预读,另建一个读取器直接访问原始连接会绕过尚在缓冲区里的数据。DotReader 返回的是受协议读取器管理的 io.Reader,不是独立副本,也没有可供调用的 Close 方法。

底层输入、共享缓冲区、协议读取器和正文解码接口的依赖结构
原创结构图:正文接口和后续行解析共用一份缓冲输入。连线仅表示归属与依赖;必须先完成当前正文再读后续响应。

防复发:把完整读取与异常退出分开

最容易遗漏的限制是生命周期:官方文档明确,返回的正文 Reader 只在下一次调用原 textproto.Reader 的方法之前有效。不要保存旧的 DotReader,同时又调用 ReadLine、ReadMIMEHeader 或另一个 DotReader;也不要让多个 goroutine 竞争同一个输入游标。

读取到正常结束行,才说明当前点编码块完整。若底层输入在结束行之前耗尽,官方实现会把这种底层 EOF 转成 io.ErrUnexpectedEOF。这时即使已经收到一部分正文,也不应把它标记为完整,或直接把当前位置视为下一条响应的起点。其他网络或下游写入错误同样要向上返回。

真实的大正文可以使用 io.Copy(destination, protocol.DotReader()) 避免整块驻留内存,但仍要检查复制错误。若目标写入提前失败,当前正文可能尚未读完;应按业务策略终止会话,或在明确允许的情况下受限地读完剩余正文,不能无条件继续解析后续响应。对网络输入同时设置合理的连接读取期限和消息大小限制,避免无限等待或无限接收。

不要把“只读前 N 字节”当作“已经消费完整正文”。在解码器外层设置的读取上限只能限制交付给业务的正文量,触及上限可能早于结束点;位于底层输入的限制则限制所覆盖的原始流量,作用范围可能还包含后续响应。大小预算和分帧完成是两个独立条件。

相关问题

DotReader 和 ReadDotBytes 怎么选?

小文本需要一次拿到解码后的字节切片时,可以使用 ReadDotBytes。需要边读边写或自行限制处理量时,选择 DotReader;是否完整仍由读取错误和结束标记决定。

ReadDotLines 会保留每行换行符吗?

不会。它返回按行拆分的字符串切片,每个元素末尾的 CRLF 或 LF 被移除。若正文里有空行,对应元素可以是空字符串。

正文中的单独一个点怎样传输?

正文行若只有一个点,线上应发送两个点再跟 CRLF。单个点加 CRLF 已被用作结束行,接收方无法把同一段字节同时解释成正文。

可以先读一小段,然后直接调用 ReadLine 吗?

不要把它当成保存了剩余正文的切换方式。旧 DotReader 在调用原读取器的其他方法后失效;需要完整正文时应显式读完,提前退出时应明确处理剩余数据和会话状态。

官方实现参考:https://go.dev/src/net/textproto/reader.go

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>