io — Reader & Writer 接口深入
Go 的 I/O 设计围绕两个接口展开:io.Reader 和 io.Writer。理解它们的精确契约是正确处理 I/O 的前提。
1. io.Reader — 读取的契约
type Reader interface {
Read(p []byte) (n int, err error)
}1.1 🔬 Read 的行为规范(必须牢记)
Read最多读len(p)个字节,存入p,返回实际字节数n。n可能小于len(p),即使还有数据可读。调用者不能假设Read会一次填满p。- 错误时
n可能 > 0。读到最后一批数据时,同时返回n>0和io.EOF。调用者必须先处理buf[:n],再检查err。 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 的行为规范
Write必须写入所有数据,如果n < len(p),必须返回非 nil 错误。- 写入后不保留
p,调用者可以立即重用。 - 并发写:
*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 是否实现了 ReaderFrom,src 是否实现了 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 的同时,数据也进入了 hashWriter5. 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) // ← 未定义行为