Go的IO入门:Reader与Writer

将数据在不同地方倒腾是 golang 最常见的应用模式了,IO 相关的话题是如何写好 golang 避不开的话题。接下来,本文将对 golang 生态中的 IO 模块进行介绍。

io.Reader

io.Reader 接口的定义非常简单:

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

这个数据源可以是任意来源,从文件到网络。Reader 方法的语义是:从底层数据源中读取数据,并将其填充到用户提供的字节切片 p 中。

调用时,Read 会将最多 len(p) 字节的数据读入 p 中,返回实际读取的字节数(n)以及遇到的错误(err)。在错误返回时,官方文档允许两种行为:错误随读取的字节数一同返回、错误在下一次调用时返回。为此,官方文档推荐优先处理已读取的字节,再解决错误,以便兼容这两种错误行为。

以下是一个简单的 Reader 示例,os.File 实现了 io.Reader 接口,因此可以将 os.File 当作一个简单的 Reader 进行读取:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
	f, err := os.Open("demo.txt")
	if err != nil {
		fmt.Printf("%s", err.Error())
		return
	}
	defer f.Close()
	bytes := make([]byte, 100)
	for {
		n, err := f.Read(bytes)
		if n > 0 {
			fmt.Printf("%s", string(bytes[:n]))
		}
		if err != nil {
			if err == io.EOF {
				break
			} else {
				fmt.Printf("%s", err.Error())
				return
			}
		}
	}
}

io.ReadAll:io.ReadAll 会一次性从流中读取所有数据并自动扩充 slice,因此不应用其处理大文件。

io.Copy

1
func Copy(dst Writer, src Reader) (written int64, err error)

io.CopycopyBuffer 的包装,其功能是使用一个大小固定为 32KB 的缓冲区将数据从 src 搬运至 dst。copyBuffer 实现了一个小优化,如果 src 实现了 WriterTo,复制操作通过调用 src.WriteTo(dst) 来实现。或者,如果 dst 实现了 ReaderFrom,复制操作通过调用 dst.ReadFrom(src) 来实现。以此来避免额外的内存分配以及数据复制。

1
2
3
4
5
6
7
8
9
// If the reader has a WriteTo method, use it to do the copy.
// Avoids an allocation and a copy.
if wt, ok := src.(WriterTo); ok {
	return wt.WriteTo(dst)
}
// Similarly, if the writer has a ReadFrom method, use it to do the copy.
if rf, ok := dst.(ReaderFrom); ok {
	return rf.ReadFrom(src)
}

bufio.Reader

1
func NewReader(rd io.Reader) *Reader

bufio.Reader 作为一个 Reader 的封装,将预先申请一大块缓冲区,读取时首先将数据尽可能载入缓冲区。之后,每次调用 bufio.Reader 时将从缓冲区提供数据,以此避免直接读取数据源导致潜在的内核态和用户态的转换。

io.Writer

io.Writerio.Reader 非常相似,其功能将 slice p 中的数据写入目标。返回值 n 表示实际写入了多少字节。理想情况下, n 应该与 len(p) 相同——表示整个切片都已写入。如果 n 小于 len(p) ,表示只有部分数据被写入,而 err 将返回具体出了什么问题。

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

bufio.Writer

与 bufio.Reader 类似,bufio.Writer 将使用缓冲区以合并多次写入,以此减少实际系统调用。值得一提的是,bufio.Writer 会在缓冲区填满或手动刷新之前不会向文件写入任何内容,可以通过 Flush() 以强制数据发送至操作系统内核。

实际上,操作系统本身也会建立一个缓冲区以合并写入操作,或者数据还停留在内存的脏页中。即便使用 io.Writer 也无法保证数据立刻写入实际位置,需要手动使用 *os.File 的 Sync() 方法确保操作系统持久化数据。

io.WriterTo 与 io.ReaderFrom

正如前文所述,io.Copy 会优先尝试使用 io.WriterTo 以避免用户态的缓存调用。WriterTo 的核心语义是:实现该接口的对象(数据源)知道如何将它自身的全部数据或剩余数据,以最有效的方式直接写入到目标 io.Writer 中。

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

注意,WriterTo 是由数据源实现,它和我们熟知的模式(接收方主动读取数据源并处理)不一样,WriterTo 是数据源主动推送到接收方。同时,WriterTo 并不关心接收方具体是什么设备,只要接收方实现了 Writer 接口即可。

与 WriterTo 完全对应的是 ReaderFrom。ReaderFrom 的核心语义是:实现该接口的对象(数据目标)知道如何以最有效的方式,从给定的 io.Reader 中读取(拉取)所有可用的数据,并存储到自身内部。

以 *bytes.Buffer 为例,当调用 buffer.ReadFrom(reader) 时,bytes.Buffer 会高效地分配或扩展自己的内部切片,并将其作为缓冲区直接传递给 reader.Read(),从而避免了用户态的缓存。

参考资料

updatedupdated2025-10-152025-10-15