Skip to content
Go
io — Reader & Writer 接口深入

io — Reader & Writer 接口深入

Go 的 I/O 设计围绕两个接口展开:io.Readerio.Writer。理解它们的精确契约是正确处理 I/O 的前提。


1. io.Reader — 读取的契约

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

1.1 🔬 Read 的行为规范(必须牢记)

  1. Read 最多读 len(p) 个字节,存入 p,返回实际字节数 n
  2. n 可能小于 len(p),即使还有数据可读。调用者不能假设 Read 会一次填满 p
  3. 错误时 n 可能 > 0。读到最后一批数据时,同时返回 n>0io.EOF。调用者必须先处理 buf[:n],再检查 err
  4. io.EOF 不是错误,而是正常结束信号。
// ✅ 正确的读取循环
buf := make([]byte, 1024)
for {
    n, err := r.Read(buf)
    if n > 0 {
        process(buf[:n])   // 先处理数据
    }
    if err == io.EOF {
        break              // 正常结束
    }
    if err != nil {
        return err         // 真正的错误
    }
}

1.2 常见 Reader 实现

实现 来源 场景
*os.File os 文件读取
*bytes.Buffer bytes 内存缓冲区
*strings.Reader strings 从字符串读取
*bufio.Reader bufio 带缓冲的读取
net.Conn net 网络连接
io.LimitedReader io 限制读取长度
io.MultiReader io 串联多个 Reader

2. io.Writer — 写入的契约

type Writer interface {
    Write(p []byte) (n int, err error)
}

2.1 Write 的行为规范

  1. Write 必须写入所有数据,如果 n < len(p),必须返回非 nil 错误。
  2. 写入后不保留 p,调用者可以立即重用。
  3. 并发写*os.File 的 Write 在 POSIX 系统上是线程安全的,但多个 Write 可能交错。

3. io.ReaderFrom 与 io.WriterTo — 零拷贝接口

type ReaderFrom interface {
    ReadFrom(r Reader) (n int64, err error)
}

type WriterTo interface {
    WriteTo(w Writer) (n int64, err error)
}

io.Copy(dst, src) 被调用时,它会检查 dst 是否实现了 ReaderFromsrc 是否实现了 WriterTo。如果是,直接调用这些方法(可能使用 sendfile 系统调用实现零拷贝)。

// 典型场景:文件复制
src, _ := os.Open("input.txt")
dst, _ := os.Create("output.txt")
io.Copy(dst, src) // 自动利用 sendfile(如果 OS 支持)

4. io 包的实用函数

函数 作用 场景
io.ReadAll(r) 读取直到 EOF 小数据全量读取
io.ReadFull(r, buf) 读取恰好 len(buf) 固定长度消息
io.Copy(dst, src) 复制到 EOF 文件传输、代理
io.CopyN(dst, src, n) 复制 N 字节 限制传输量
io.WriteString(w, s) 写入字符串 便捷写入
io.LimitReader(r, n) 限制读取 ≤ N 字节 防止 OOM
io.MultiReader(...) 串联多个 Reader 合并文件头+体
io.MultiWriter(...) 一对多广播写入 同时写文件和 stdout
io.TeeReader(r, w) 读取时同时写入 w 日志/校验和
io.NopCloser(r) 给 Reader 加无操作 Close 适配接口
io.Discard 黑洞(/dev/null) 丢弃不需要的数据

使用示例

// 限制读取以防止大请求
limited := io.LimitReader(r.Body, 1<<20) // 1 MB
data, _ := io.ReadAll(limited)

// 同时写入文件和 stdout
writer := io.MultiWriter(file, os.Stdout)
fmt.Fprint(writer, "log message")

// 合并多个文件
readers := []io.Reader{header, body, footer}
combined := io.MultiReader(readers...)
io.Copy(dst, combined)

// TeeReader:读取时记录日志
r := io.TeeReader(originalReader, hashWriter)
io.ReadAll(r) // 读取 originalReader 的同时,数据也进入了 hashWriter

5. io.Pipe — 同步内存管道

r, w := io.Pipe()

// 写入方
go func() {
    defer w.Close()  // ← 必须关闭,否则读取方会永久阻塞
    fmt.Fprint(w, "hello from pipe")
}()

// 读取方
data, _ := io.ReadAll(r)
fmt.Println(string(data)) // "hello from pipe"

🚨 陷阱

  • Pipe 是同步的:写入方和读取方必须在不同 goroutine,否则会死锁。
  • 记得 Close:写入方必须调用 w.Close(),否则读取方永远等不到 EOF。
  • Pipe 不可用于网络传输:它只是一个内存管道。

6. 🚨 最常见的 I/O 错误

错误 1:假设 Read 一次返回所有数据

// ❌ 假设 Read 会填满 buf
buf := make([]byte, 1024)
conn.Read(buf)           // 可能只读了几十个字节
process(buf)             // 其他字节是零值 → bug

// ✅ 用 io.ReadFull 或循环
buf := make([]byte, 1024)
io.ReadFull(conn, buf)   // 读满 1024 字节或报错

错误 2:忽略 Read 返回的 n>0 且 err!=nil

// ❌ 错误顺序
n, err := r.Read(buf)
if err != nil {
    return err  // ← 丢失了 buf[:n] 的数据
}

// ✅ 正确顺序
n, err := r.Read(buf)
if n > 0 {
    process(buf[:n])
}
if err == io.EOF {
    return nil
}
if err != nil {
    return err
}

错误 3:关闭后继续使用

file.Close()
file.Read(buf) // ← 未定义行为