• 简体中文
  • 深入理解 Go 中的 io.EOF

    io.EOF 是 Go I/O 中最容易被误解的值之一。

    很多人第一次看到它,会把它理解成:

    “读取失败了。”

    这并不准确。

    io.EOF 表示:

    输入已经结束,没有更多数据可以读取。

    它本身既不是成功,也不是失败。

    输入结束之后,这个结束是否正常,要看当前操作是否允许输入在这里结束。

    例如:

    • 读取一个文件,读到文件末尾:正常。
    • 读取一个完整消息,消息却提前结束:异常。
    • 读取一个固定长度的结构,只收到一半:异常。

    理解 io.EOF,关键就在这里。


    1. io.EOF 是什么

    Go 定义:

    var EOF = errors.New("EOF")

    它表示一个非常明确的状态:

    没有更多输入了。

    假设一个 Reader 中只有:

    hello

    不断读取:

    读取 "hel" → 还有数据
    读取 "lo"  → 数据读完
    继续读取  → EOF

    可以把输入想象成:

    data data data data | EOF
    
                      输入结束

    所以:

    err == io.EOF

    真正应该理解成:

    “输入到这里结束了。”

    而不是:

    “发生了一个错误。”


    2. io.Reader 为什么会返回 io.EOF

    io.Reader 的核心接口只有一个方法:

    type Reader interface {
        Read(p []byte) (n int, err error)
    }

    最基本的读取循环可以写成:

    for {
        n, err := r.Read(buf)
    
        if n > 0 {
            process(buf[:n])
        }
    
        if err == io.EOF {
            break
        }
    
        if err != nil {
            log.Fatal(err)
        }
    }

    这里最重要的一条规则是:

    先处理 n,再处理 err

    因为一次 Read 可以同时返回数据和 EOF:

    n > 0
    err == io.EOF

    意思是:

    “这是最后一批数据,后面没有了。”

    因此不能看到 err != nil 就直接丢弃数据。

    同样,也不要假定 EOF 一定单独出现在下一次 Read

    Reader 可以这样结束:

    最后数据 → nil
    下一次   → EOF

    也可以:

    最后数据 → EOF

    两种方式都合法。


    3. 为什么 io.Copyio.ReadAll 不返回 io.EOF

    如果直接调用 Read,需要自己处理 EOF。

    但更高层的 API 通常会替你处理。

    例如:

    _, err := io.Copy(dst, src)
    if err != nil {
        return err
    }

    io.Copy 读取到 EOF 后,会把它当作正常结束。

    所以复制成功时:

    err == nil

    而不是:

    err == io.EOF

    io.ReadAll 也是一样:

    data, err := io.ReadAll(r)

    正常读到 EOF:

    err == nil

    但如果读取过程中发生了其他错误,io.ReadAll 可能已经读到了一部分数据:

    data = 已经成功读取的数据
    err  = 读取过程中发生的错误

    这部分数据是否可以使用,要看具体协议和业务语义。

    因此,不要看到 err != nil 就机械地认为 data 没有价值。

    一个简单的判断是:

    if len(data) > 0 {
        // 确实已经读取了一些数据
    }

    这里判断数据是否为空应该看 len(data),而不是 data == nil

    所以可以记住:

    底层 Reader 把 EOF 交给调用者;高层 API 通常会消费正常 EOF。


    4. io.ReadFull 为什么返回 io.ErrUnexpectedEOF

    io.ReadFull 很适合说明 EOF 为什么有时代表异常。

    假设协议规定必须读取 10 个字节:

    buf := make([]byte, 10)
    
    n, err := io.ReadFull(r, buf)

    但输入只有 6 个字节:

    需要:10 bytes
    收到: 6 bytes
    结束:EOF

    输入确实结束了。

    但它不应该在这里结束

    所以 io.ReadFull 返回:

    n == 6
    err == io.ErrUnexpectedEOF

    注意两者的区别:

    io.EOF
        输入结束了
    
    io.ErrUnexpectedEOF
        输入结束得太早了

    这正是 EOF 最重要的语义:

    EOF 只告诉你输入结束了。这个结束是否正常,要由当前操作决定。

    边界情况下,如果请求长度是 0:

    io.ReadFull(r, make([]byte, 0))

    会直接返回:

    n == 0
    err == nil

    不会产生 EOF。


    5. 同一个 EOF,可以是正常结束,也可以是异常

    假设读取一个普通文件:

    hello world | EOF

    文件本来就只有这些内容。

    EOF 是正常的。

    但假设一个协议要求:

    Header
    Body
    Checksum

    现在收到:

    Header
    Body | EOF

    如果 Checksum 必须存在,那么这个 EOF 就意味着:

    输入不完整。

    所以:

                        输入结束
    
                         EOF
    
                 ┌─────────┴─────────┐
                 │                   │
            可以在这里结束        不能在这里结束
                 │                   │
              正常完成             输入不完整

    不要把:

    EOF = success

    或者:

    EOF = error

    当成规则。

    真正的规则是:

    EOF = 输入结束。


    6. io.EOFClose 是两回事

    EOF 表示:

    没有更多数据。

    Close 表示:

    不再使用这个资源。

    例如:

    f, err := os.Open("data.txt")
    if err != nil {
        return err
    }
    defer f.Close()
    
    data, err := io.ReadAll(f)

    io.ReadAll 读到 EOF,并不意味着文件已经关闭。

    仍然需要:

    f.Close()

    HTTP 也是一样:

    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    读到 EOF 和关闭 Body 是两件事。

    另外,在 HTTP/1.x 中,完整读取 Response Body 有利于底层连接复用。如果调用方不需要 Body 内容,但希望尽可能复用连接,可以在关闭前主动读完:

    if _, err := io.Copy(io.Discard, resp.Body); err != nil {
        return err
    }
    
    if err := resp.Body.Close(); err != nil {
        return err
    }

    Close 不能简单理解成“已经读到 EOF”。

    所以:

    EOF   → 输入结束
    Close → 资源生命周期结束

    7. bufio.Reader 为什么让 EOF 更容易被误解

    bufio.Reader 会在自己的缓冲区中保存数据。

    假设底层 Reader 返回:

    hello | EOF

    bufio.Reader 可能已经把 hello 放进自己的缓冲区。

    上层仍然可能先读到:

    hello

    之后才看到:

    EOF

    更重要的是,Read 本身允许:

    n > 0
    err == io.EOF

    此时 n 个字节可能就是缓冲区中最后剩下的数据,而 EOF 表示底层已经没有更多输入。

    这正是:

    先处理 n,再处理 err

    这条规则最实际的应用之一。


    8. bufio.Scanner 会替你处理 EOF

    使用 Scanner 时,通常不需要直接判断 io.EOF

    scanner := bufio.NewScanner(r)
    
    for scanner.Scan() {
        process(scanner.Text())
    }
    
    if err := scanner.Err(); err != nil {
        return err
    }

    如果输入正常结束:

    Scan() → false
    Err()  → nil

    所以:

    Scan() == false 不等于发生了错误。

    还需要注意,Scanner 默认的最大 token 大小是 64 * 1024 字节。处理可能出现超长行或超大 token 的输入时,应根据业务需要调用 scanner.Buffer 调整限制。


    9. == io.EOF 还是 errors.Is(err, io.EOF)

    如果 Reader 直接返回:

    io.EOF

    那么:

    err == io.EOF

    完全正确。

    但错误可能经过包装:

    return fmt.Errorf("read header: %w", io.EOF)

    此时:

    err == io.EOF

    会是 false

    应该使用:

    errors.Is(err, io.EOF)

    所以可以记住:

    直接处理 Reader 返回的 EOF → ==
    
    错误经过其他层传播、可能被包装 → errors.Is

    例如:

    switch {
    case errors.Is(err, io.EOF):
        // 输入结束
    case errors.Is(err, context.Canceled):
        // 操作被取消
    case errors.Is(err, context.DeadlineExceeded):
        // 操作超时
    default:
        // 其他错误
    }

    这里 errors.Is 的意义不是“更正确”,而是:

    允许错误经过 %w 包装后,仍然识别出它原来的语义。


    10. 自己实现 Reader 时,记住三件事

    实现 Reader 时,最重要的是遵守它的语义。

    不要丢掉已经读取的数据

    如果:

    n > 0

    这些数据就是有效数据。

    即使:

    err == io.EOF

    也必须先处理这 n 个字节。

    EOF 可以和最后的数据一起返回

    下面是合法的:

    return n, io.EOF

    也可以:

    return n, nil

    下一次再:

    return 0, io.EOF

    调用者不能依赖 EOF 一定单独出现。

    0, nil 不是 EOF

    return 0, nil

    表示这次没有读到数据,也没有遇到错误。

    它不能被简单理解成:

    “输入结束了。”


    11. 最后,只记住这一件事

    如果只记住一个关于 io.EOF 的概念,就记住:

    io.EOF 表示输入结束。

    然后再问一个问题:

    这个操作允许输入在这里结束吗?

    如果允许:

    EOF → 正常完成

    如果不允许:

    EOF → 输入不完整

    这就是 io.EOF 最核心的语义。

    它不是一个需要死记硬背的“错误值”。

    它只是 Go 用来告诉你:

    到这里,已经没有更多输入了。