io.eof 是正常结束信号而非错误,应显式判断 err == io.eof 来退出循环;bufio.scanner 默认吞掉 io.eof 且行超 64kb 会静默截断,建议显式设置缓冲区大小。

遇到 io.EOF 别 panic,它不是异常而是正常信号
Go 的 io.Read 类操作(比如 Read、ReadByte、ReadLine)在读到文件末尾时返回 io.EOF,这不是错误,是流式读取的“完成通知”。很多人一看到 error != nil 就直接 log.Fatal 或 panic,结果程序在读完最后一行就崩了。
正确做法是显式判断是否等于 io.EOF,再决定是否退出循环:
for {
n, err := r.Read(buf)
if err == io.EOF {
break // 正常结束,不报错
}
if err != nil {
return err // 其他真实错误才返回
}
// 处理 buf[:n]
}
-
io.EOF是一个变量,类型是error,值固定,可直接用==比较(标准库保证单例) - 不要用
strings.Contains(err.Error(), "EOF")—— 既慢又不可靠,且可能误判其他含 EOF 的错误信息 - 某些封装函数(如
bufio.Scanner.Scan())内部已处理io.EOF,返回false而非 error,这时你根本收不到io.EOF
bufio.Scanner 默认行为会吞掉 io.EOF,但超长行会静默失败
bufio.Scanner 是最常用的逐行读取方式,它把 io.EOF 当作扫描终止条件,不暴露给调用方。看起来很省心,但容易忽略两个关键点:
- 默认最大行长度是 64KB(
bufio.MaxScanTokenSize),超过就直接返回false,且Err()返回nil—— 你以为读完了,其实某行被截断丢了 - 如果真想捕获底层
io.EOF,得自己用bufio.Reader+ReadString('\n'),但要注意ReadString在末尾无换行时返回io.EOF和已读内容,不是错误
安全起见,初始化 scanner 时建议显式设置缓冲区大小:
立即学习“go语言免费学习笔记(深入)”;
scanner := bufio.NewScanner(f) scanner.Buffer(make([]byte, 4096), 1<<20) // 最大 1MB 行
用 ReadString('\n') 读行时,io.EOF 可能和数据一起返回
bufio.Reader.ReadString('\n') 的行为比 scanner 更透明,但也更易出错:当文件最后一行没结尾换行符时,它会返回已读内容 + io.EOF;而如果文件为空,它直接返回 "" + io.EOF。
- 必须检查
err == io.EOF后再看str是否非空,否则可能漏掉最后一行 - 不能只靠
err != nil就跳过处理 ——io.EOF时str很可能有有效数据 - 如果文件以
\r\n结尾,ReadString('\n')会把\r留在结果里,需要手动strings.TrimSuffix(str, "\r")
HTTP 响应体、网络连接等非文件场景也适用同一套 io.EOF 判断逻辑
只要底层实现了 io.Reader,比如 http.Response.Body、net.Conn、gzip.Reader,它们的 Read 方法语义完全一致:io.EOF 表示“数据源已关闭/耗尽”,不是故障。
- HTTP 流式响应中,服务端主动 close 连接会触发
io.EOF,客户端应正常退出读循环,而不是重试或报错 - 用
io.Copy时完全不用管io.EOF—— 它内部已经处理,只在发生真实 I/O 错误时返回非 nil error - 自定义 reader(比如解密包装器)若要兼容标准行为,必须在数据耗尽时返回
io.EOF,不能返回nil或其他 error
真正容易被绕晕的是嵌套 reader 场景:比如 gzip.NewReader(io.MultiReader(a,b)),一旦某层提前 EOF,上层可能返回不同 error 类型。这时候别猜,用 errors.Is(err, io.EOF) 更稳妥 —— 它能穿透包装器识别底层 EOF。










