29·标准库入门

IO 基础:Reader/Writer

ioreaderwriterinterface

第29课 - IO 基础:Reader/Writer

学习目标

  1. 理解 io.Readerio.Writer 接口的核心定义与职责。
  2. 掌握使用标准库提供的 ReaderWriter 实现进行基本IO操作。
  3. 学会实现自定义的 ReaderWriter 接口。
  4. 了解 io.Readerio.Writer 在并发编程中的基础应用与安全考量。

核心概念

在编程中,IO(输入/输出)无处不在:读写文件、网络通信、处理标准输入输出。Go语言通过两个核心接口 io.Readerio.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

  1. 它有一个内部的 ConcurrentWriter
  2. 提供一个 Log(message string) 方法,该方法将格式化后的日志(如 “[时间戳] 消息”)异步地写入 Writer。为了简单,可以使用 time.Now() 和字符串拼接。
  3. 编写主函数,启动多个 goroutine 并发调用 Log 方法,模拟多模块并发记录日志。
  4. 确保最终程序正常退出且日志没有交错混乱。

常见错误

  1. 忽略错误处理ReadWrite 都可能返回错误。初学者常只关注返回的字节数 n,而忽略 err,这可能导致数据丢失或程序在遇到 io.EOF 等情况时行为异常。

    // 错误示范:忽略错误
    n, _ := reader.Read(buf)
    // 正确示范:始终检查错误
    n, err := reader.Read(buf)
    if err != nil {
        // 处理错误(特别是 io.EOF 通常需要特殊处理)
    }
    
  2. 误解 io.EOFio.EOF 是一个预期的“错误”,它表示流的结束,而不是一个真正的失败。在循环读取时,通常需要在检查到 n > 0 的数据后,再单独判断 err == io.EOF 来决定是否结束循环,如示例1所示。

  3. 在并发环境下直接使用非并发安全的 Writer:很多 Writer 实现(如 os.Filebytes.Buffer)本身不是并发安全的。如果多个 goroutine 同时调用它们的 Write 方法,会导致数据竞争。必须像示例3那样添加同步机制(如 sync.Mutex)。

  4. 未完全处理 Write 的返回值Write 方法的 n 可能小于 len(p),即使没有返回错误(虽然规范不鼓励这种情况)。健壮的代码应该检查 n,并在必要时尝试写入剩余的部分。

小结

  • io.Readerio.Writer 是Go语言IO系统的基石,它们定义了数据流动的统一接口。
  • Reader 的核心是 Read([]byte) 方法,它向缓冲区填充数据;Writer 的核心是 Write([]byte) 方法,它将缓冲区数据流出。
  • 标准库提供了大量现成的实现(如 os.File, net.Conn, bytes.Buffer, strings.Reader),也可以轻松实现自定义版本以满足特定需求。
  • io 包提供了强大的工具函数(如 io.Copy, io.ReadAll),极大简化了常见的数据流转操作。
  • 在并发环境中使用 ReaderWriter 时,必须考虑其并发安全性,通常需要通过包装(Decorator)模式并加入同步原语(如 sync.Mutex)来实现。

理解并熟练运用 ReaderWriter 接口,将为你处理文件操作、网络通信以及构建高效的数据处理管道打下坚实的基础。下一课,我们将深入探讨如何使用这些接口进行具体的文件操作

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「文件操作」 以巩固所学知识。