第29课 - IO 基础:Reader/Writer
学习目标
- 理解
io.Reader和io.Writer接口的核心定义与职责。 - 掌握使用标准库提供的
Reader和Writer实现进行基本IO操作。 - 学会实现自定义的
Reader和Writer接口。 - 了解
io.Reader和io.Writer在并发编程中的基础应用与安全考量。
核心概念
在编程中,IO(输入/输出)无处不在:读写文件、网络通信、处理标准输入输出。Go语言通过两个核心接口 io.Reader 和 io.Writer,将所有这些操作抽象为统一的“数据流”模型。
想象一下:
io.Reader就像一个水源。你向它“要水”(读取数据),它就从数据源(如文件、网络连接、内存缓冲区)流一些水(字节)给你,直到水流完(读到末尾)。io.Writer就像一个水槽。你向它“倒水”(写入数据),它就把水(字节)流向目的地(如另一个文件、网络连接、控制台)。
接口定义
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
Read(p []byte)方法尝试将数据读入切片p中,返回成功读取的字节数n和遇到的错误err。如果读到了数据源的末尾,错误通常是io.EOF。Write(p []byte)方法尝试将切片p中的数据写入目标,返回成功写入的字节数n和遇到的错误err。写入操作应尽量保证n == len(p),除非发生错误。
标准库(如 os.File, net.Conn, bytes.Buffer, strings.Reader)已经为许多类型实现了这两个接口。这意味着,你可以用相同的代码逻辑去处理文件、网络连接或内存中的数据,代码的复用性极高。
代码示例
示例 1:使用标准库 Reader 和 Writer
package main
import (
"bytes"
"fmt"
"io"
"strings"
)
func main() {
// --- 示例 1: 使用 strings.Reader 作为 Reader ---
// strings.Reader 是一个从字符串中读取数据的 Reader 实现。
data := "Hello, Go IO!"
reader := strings.NewReader(data)
// 准备一个用于接收数据的缓冲区。
buf := make([]byte, 8)
fmt.Println("开始从 Reader 读取数据:")
for {
n, err := reader.Read(buf) // 尝试读取最多 8 个字节
if n > 0 {
fmt.Printf("读取到: %q (字节数: %d)\n", buf[:n], n)
}
if err == io.EOF {
fmt.Println("已到达数据末尾 (EOF)")
break
}
if err != nil {
fmt.Println("读取错误:", err)
break
}
}
// --- 示例 2: 使用 bytes.Buffer 作为 Reader 和 Writer ---
fmt.Println("\n--- 使用 bytes.Buffer 进行读写 ---")
var buffer bytes.Buffer // buffer 同时实现了 Reader 和 Writer
// 将数据写入 buffer (Writer 接口)
_, err := buffer.WriteString("Writing to buffer, ")
if err != nil {
fmt.Println("写入错误:", err)
return
}
buffer.Write([]byte("and some bytes."))
// 从 buffer 读取所有数据 (Reader 接口)
readData, err := io.ReadAll(&buffer) // io.ReadAll 会持续读取直到 EOF
if err != nil {
fmt.Println("读取全部数据错误:", err)
return
}
fmt.Printf("从 Buffer 读出的内容: %q\n", string(readData))
// --- 示例 3: 使用 io.Copy 连接 Reader 和 Writer ---
fmt.Println("\n--- 使用 io.Copy 复制数据 ---")
source := strings.NewReader("Data from Source -> Destination")
var destination bytes.Buffer
// io.Copy 将 source (Reader) 的内容复制到 destination (Writer)
bytesCopied, err := io.Copy(&destination, source)
if err != nil {
fmt.Println("复制错误:", err)
return
}
fmt.Printf("复制了 %d 字节,目标内容: %q\n", bytesCopied, destination.String())
}
示例 2:实现自定义的 Writer
一个记录写入总字节数的 Writer 装饰器。
package main
import (
"fmt"
"io"
"os"
)
// ByteCounter 是一个自定义的 Writer,它包装了另一个 Writer,
// 并记录总共写入了多少字节。
type ByteCounter struct {
Writer io.Writer // 被包装的 Writer
Count int64 // 记录写入的总字节数
}
// Write 方法实现了 io.Writer 接口。
func (bc *ByteCounter) Write(p []byte) (n int, err error) {
// 将数据实际写入被包装的 Writer。
n, err = bc.Writer.Write(p)
// 累加写入的字节数。
bc.Count += int64(n)
return n, err // 返回原始 Writer 的返回值
}
func main() {
// 创建一个 ByteCounter,它的底层 Writer 是标准输出。
counter := &ByteCounter{Writer: os.Stdout}
// 通过 counter 写入数据。数据会先被 os.Stdout 输出,然后计数器更新。
fmt.Fprintln(counter, "Hello, ByteCounter!")
fmt.Fprintf(counter, "The answer is %d.\n", 42)
fmt.Fprintf(counter, "A longer line to see the count increase properly.\n")
fmt.Printf("\n总共写入了 %d 字节。\n", counter.Count)
}
示例 3:并发安全的 Writer
一个简单的并发安全 Writer,使用 Mutex 保护。
package main
import (
"fmt"
"os"
"sync"
)
// ConcurrentWriter 是一个并发安全的 Writer。
type ConcurrentWriter struct {
mu sync.Mutex
w io.Writer
}
func NewConcurrentWriter(w io.Writer) *ConcurrentWriter {
return &ConcurrentWriter{w: w}
}
func (cw *ConcurrentWriter) Write(p []byte) (n int, err error) {
cw.mu.Lock() // 加锁
defer cw.mu.Unlock() // 函数返回时解锁
return cw.w.Write(p) // 在锁的保护下进行写入
}
func main() {
// 创建一个基于标准输出的并发安全 Writer。
safeWriter := NewConcurrentWriter(os.Stdout)
var wg sync.WaitGroup
// 启动 10 个 goroutine 并发写入。
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Fprintf(safeWriter, "Goroutine %d: writing concurrently.\n", id)
}(i)
}
wg.Wait()
fmt.Println("所有 goroutine 写入完成。")
}
实践练习
练习 1:基础读取
编写一个函数 readAllAndPrint(r io.Reader),它接受任意 Reader,读取其中所有内容并打印为字符串。测试用例:传入一个 strings.NewReader(“Practice makes perfect.”)。
预期输出:
Practice makes perfect.
练习 2:限制写入量
实现一个 LimitWriter 结构体,它包装一个 io.Writer,但允许最多写入 n 个字节。当总写入字节数超过 n 时,后续的 Write 调用应返回 io.ErrShortWrite 错误。
要求: 实现 io.Writer 接口。
提示: 你需要维护一个已写字节计数器。
练习 3:并发日志写入器
基于示例3的 ConcurrentWriter,设计一个 AsyncLogger。
- 它有一个内部的
ConcurrentWriter。 - 提供一个
Log(message string)方法,该方法将格式化后的日志(如 “[时间戳] 消息”)异步地写入 Writer。为了简单,可以使用time.Now()和字符串拼接。 - 编写主函数,启动多个 goroutine 并发调用
Log方法,模拟多模块并发记录日志。 - 确保最终程序正常退出且日志没有交错混乱。
常见错误
-
忽略错误处理:
Read和Write都可能返回错误。初学者常只关注返回的字节数n,而忽略err,这可能导致数据丢失或程序在遇到io.EOF等情况时行为异常。// 错误示范:忽略错误 n, _ := reader.Read(buf) // 正确示范:始终检查错误 n, err := reader.Read(buf) if err != nil { // 处理错误(特别是 io.EOF 通常需要特殊处理) } -
误解
io.EOF:io.EOF是一个预期的“错误”,它表示流的结束,而不是一个真正的失败。在循环读取时,通常需要在检查到n > 0的数据后,再单独判断err == io.EOF来决定是否结束循环,如示例1所示。 -
在并发环境下直接使用非并发安全的 Writer:很多 Writer 实现(如
os.File,bytes.Buffer)本身不是并发安全的。如果多个 goroutine 同时调用它们的Write方法,会导致数据竞争。必须像示例3那样添加同步机制(如sync.Mutex)。 -
未完全处理
Write的返回值:Write方法的n可能小于len(p),即使没有返回错误(虽然规范不鼓励这种情况)。健壮的代码应该检查n,并在必要时尝试写入剩余的部分。
小结
io.Reader和io.Writer是Go语言IO系统的基石,它们定义了数据流动的统一接口。Reader的核心是Read([]byte)方法,它向缓冲区填充数据;Writer的核心是Write([]byte)方法,它将缓冲区数据流出。- 标准库提供了大量现成的实现(如
os.File,net.Conn,bytes.Buffer,strings.Reader),也可以轻松实现自定义版本以满足特定需求。 io包提供了强大的工具函数(如io.Copy,io.ReadAll),极大简化了常见的数据流转操作。- 在并发环境中使用
Reader和Writer时,必须考虑其并发安全性,通常需要通过包装(Decorator)模式并加入同步原语(如sync.Mutex)来实现。
理解并熟练运用 Reader 和 Writer 接口,将为你处理文件操作、网络通信以及构建高效的数据处理管道打下坚实的基础。下一课,我们将深入探讨如何使用这些接口进行具体的文件操作。