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

Go bufio.Reader处理超长行而不截断的读取方法

来源:17golang原创

时间:2026-09-15 19:09:50 107浏览 收藏

我在接收日志导出流时遇到过一个很容易误判的现象:短行读取正常,碰到一条几十 KB 甚至更长的 JSON 行,结果却只拿到前半段。问题通常不在 bufio.Reader 把数据“截断”了,而在调用方把一次缓冲区片段误当成完整的一行。处理超长行时,最稳妥的办法是用 ReadLine 查看 isPrefix 并拼接片段;如果只想得到完整字符串,则可以直接用 ReadString('\n')

要点速览
  • ReadLine 返回的 isPrefix=true 表示当前只是长行的一段,不是错误。
  • 片段在下一次读取后可能失效,需要复制到自己的缓冲区再继续循环。
  • 简单按行读取优先用 ReadStringReadBytes,生产环境仍要设置单行大小上限。

先分清 Scanner、ReadSlice 和 ReadLine

bufio.Scanner 默认 token 上限是 64 KiB 左右,超长 token 会以 ErrTooLong 停止;即使调用 Scanner.Buffer 扩大上限,也要为最大输入负责。Reader.ReadSlice('\n') 则在内部缓冲区装满且没找到换行时返回 bufio.ErrBufferFull,返回的字节还会在后续读取时被覆盖。

Reader.ReadLine 专门提供了“这一段是不是前缀”的信号。isPrefix 为真时,当前切片只是同一行的开头或中间片段;为假才表示这一行已经结束。它适合流式处理,但调用方必须自行决定如何拼接和限制长度。

Go bufio.Reader、底层 io.Reader 与超长行片段之间的缓冲区边界说明图
图1:bufio.Reader 的缓冲区边界说明图,展示片段与完整行的关系,不是运行截图。

用 ReadLine 拼接超长行且不丢字节

下面的函数把一条完整行放入 bytes.Buffer。每次拿到片段后立即复制,避免下一次 ReadLine 复用内部缓冲区造成内容变化;同时用 maxLineBytes 防止异常输入让内存无界增长。

package main

import (
    "bufio"
    "bytes"
    "fmt"
    "io"
    "strings"
)

// readLongLine 读取一条完整逻辑行,并用上限保护调用方内存。
func readLongLine(r *bufio.Reader, maxLineBytes int) (string, error) {
    var line bytes.Buffer
    for {
        part, isPrefix, err := r.ReadLine()
        // ReadLine 的返回切片会被后续读取复用,所以这里必须复制。
        if len(part) > 0 {
            if line.Len()+len(part) > maxLineBytes {
                return "", fmt.Errorf("line exceeds %d bytes", maxLineBytes)
            }
            _, _ = line.Write(part)
        }
        if err != nil {
            // 没有完整行时的 EOF 交给上层判断;其他错误直接返回。
            if err == io.EOF && line.Len() == 0 {
                return "", io.EOF
            }
            return "", err
        }
        if !isPrefix {
            return line.String(), nil
        }
    }
}

func main() {
    input := strings.NewReader("短行\n" + strings.Repeat("x", 70000) + "\n")
    reader := bufio.NewReaderSize(input, 16)
    for {
        line, err := readLongLine(reader, 128*1024)
        if err == io.EOF {
            break
        }
        if err != nil {
            panic(err)
        }
        fmt.Println(len(line)) // 输出每条完整行的字节数,而不是片段数。
    }
}

这个示例中缓冲区故意设得很小,长行会被拆成多个片段,但最终仍按完整行返回。真实服务还应根据协议决定超限后的动作:拒绝该记录、丢弃到下一个换行,或改为流式解析 JSON;不能只把上限改成一个很大的数字。

Go Reader.ReadLine 的 isPrefix 片段与 bytes.Buffer 拼接关系说明图
图2:ReadLine 的前缀片段和业务缓冲区拼接结构说明图,不是运行证据。

不需要片段控制时直接读取完整行

如果业务只关心完整行,ReadString('\n') 往往更容易写对。它会持续读取直到找到分隔符,并返回包含分隔符的字符串;输入在末尾没有换行时,最后一次可能返回剩余数据和 io.EOF,所以不能看到 EOF 就立即丢弃非空数据。

func readStrings(r *bufio.Reader) ([]string, error) {
    var lines []string
    for {
        line, err := r.ReadString('\n')
        // 先处理非空数据,避免无换行的最后一行被 EOF 丢掉。
        if len(line) > 0 {
            lines = append(lines, strings.TrimSuffix(strings.TrimSuffix(line, "\n"), "\r"))
        }
        if err == io.EOF {
            return lines, nil
        }
        if err != nil {
            return nil, err
        }
    }
}

ReadBytes 与它的语义类似,但返回字节切片,适合后续仍按字节处理的协议。两者都会把完整行交给调用方,内存开销随行长度增长;若单行可能来自不可信客户端,建议在外层计数或改用有界的流式解析。

方法超长行行为适合场景
ReadLine通过 isPrefix 分片,需自行拼接需要控制内存、逐片段判断或接入流式解析
ReadString持续读取到分隔符,返回完整字符串普通文本、日志和简单协议
ReadSlice缓冲区装满时返回 ErrBufferFull只接受有界短行且希望少一次复制

上线前检查换行、编码和长度边界

长度上限应按字节而不是按中文字符估算,因为 bufio.Reader 面向字节流。若输入是 Windows 文本,ReadLine 会处理行尾,但业务自己拼接片段时不要再把中间片段当作独立记录。若行内容是 UTF-8 JSON,先保证字节完整,再交给 JSON 解码器,不能按片段直接转字符串后拼接来掩盖非法边界。

我的选择通常很简单:普通日志用 ReadString,需要拒绝超大记录或逐段解析时用 ReadLine,明确要求固定上限的短协议才考虑 ReadSlice。无论选哪种,都要把“无换行 EOF”“超出上限”和“底层读取错误”分别记录,排查时才不会把正常文件尾误报成截断。

常见问题

ReadLine 返回 isPrefix=true 是报错吗?

不是。它说明当前片段后面仍有同一行内容,继续读取并拼接即可;真正的读取错误看返回的 err

为什么不直接把 Reader 的 Size 调得很大?

扩大缓冲区只能改变一次可保留的数据量,不能替代输入上限。多个并发连接都发送超长行时,固定大缓冲区会放大内存压力。

最后一行没有换行会丢失吗?

使用 ReadStringReadBytes 时先处理返回的非空数据,再判断 io.EOF;只有空数据加 EOF 才表示真正读完。

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