Go io.Reader 和 io.Writer 入门:理解标准库里的数据流

本文讲解 io.Reader、io.Writer、io.Copy、bytes.Buffer 和 strings.Reader,帮助初学者理解 Go 标准库的数据流抽象。

两个小接口撑起了很多标准库

Go 标准库里有两个非常重要的接口:io.Readerio.Writer。它们看起来小到不能再小,却贯穿文件、网络、压缩、HTTP、模板、JSON、命令行工具和测试。理解它们后,你会发现很多 API 的设计突然变得一致。

Reader 表示“能读出字节”,Writer 表示“能写入字节”。文件能读写,HTTP 响应体能读,请求体能读,内存 buffer 能读写,字符串也能包装成 reader。函数只要依赖这些接口,就能同时支持很多数据来源和目标。

这篇文章用几个小例子讲清楚数据流抽象。重点不是背接口定义,而是学会写不依赖具体文件路径的函数。

Reader 和 Writer 的定义

简化来看:

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

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

你很少需要手写实现这两个接口,但会经常接收它们作为参数。

读取全部内容:

func ReadText(r io.Reader) (string, error) {
	data, err := io.ReadAll(r)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

这个函数可以读文件:

file, err := os.Open("note.txt")
if err != nil {
	return err
}
defer file.Close()

text, err := ReadText(file)

也可以读字符串:

text, err := ReadText(strings.NewReader("hello"))

这就是接口的力量:函数只关心“能读”,不关心来源。

写入目标也可以抽象

func WriteLines(w io.Writer, lines []string) error {
	for _, line := range lines {
		if _, err := fmt.Fprintln(w, line); err != nil {
			return err
		}
	}
	return nil
}

写到标准输出:

err := WriteLines(os.Stdout, []string{"a", "b"})

写到文件:

file, err := os.Create("out.txt")
if err != nil {
	return err
}
defer file.Close()

err = WriteLines(file, []string{"a", "b"})

测试时写到内存:

var buf bytes.Buffer
err := WriteLines(&buf, []string{"a", "b"})
fmt.Println(buf.String())

这种设计让代码天然可测试。你不需要为了测试真的创建文件,也不需要捕获标准输出。

io.Copy 连接 Reader 和 Writer

复制数据:

written, err := io.Copy(dst, src)

比如复制文件:

func CopyFile(dstPath, srcPath string) error {
	src, err := os.Open(srcPath)
	if err != nil {
		return fmt.Errorf("open source: %w", err)
	}
	defer src.Close()

	dst, err := os.Create(dstPath)
	if err != nil {
		return fmt.Errorf("create destination: %w", err)
	}
	defer dst.Close()

	if _, err := io.Copy(dst, src); err != nil {
		return fmt.Errorf("copy data: %w", err)
	}
	return nil
}

io.Copy 内部会循环读取并写入。你不用自己写缓冲区循环。很多数据转发场景都能用它,比如 HTTP 下载保存到文件、解压流写入目标、请求体转发到另一个服务。

LimitReader 防止读太多

如果输入不可信,不要无限读取:

func ReadLimited(r io.Reader, max int64) ([]byte, error) {
	limited := io.LimitReader(r, max)
	return io.ReadAll(limited)
}

HTTP handler 里也常见类似思路。对外部请求体、上传内容、日志片段,都要考虑大小上限。

需要注意:io.LimitReader 只限制最多读多少,不会告诉你原始数据是否超过限制。如果你需要判断超限,可以多读一个字节:

func ReadWithLimit(r io.Reader, max int64) ([]byte, error) {
	var buf bytes.Buffer
	_, err := io.CopyN(&buf, r, max+1)
	if err != nil && err != io.EOF {
		return nil, err
	}
	if int64(buf.Len()) > max {
		return nil, fmt.Errorf("input too large")
	}
	return buf.Bytes(), nil
}

边界处理比最短代码更重要。

小结

io.Readerio.Writer 是 Go 标准库最重要的抽象之一。它们让函数不依赖具体文件、网络连接或内存对象,只依赖“能读”和“能写”这两个能力。io.Copy 把二者连接起来,bytes.Bufferstrings.Reader 让测试变得简单。

写 Go 程序时,如果一个函数要处理输入,先考虑接收 io.Reader;如果要输出内容,先考虑接收 io.Writer。这样代码会更通用、更容易测试,也更贴近标准库风格。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页