将数据在不同地方倒腾是 golang 最常见的应用模式了,IO 相关的话题是如何写好 golang 避不开的话题。接下来,本文将对 golang 生态中的 IO 模块进行介绍。
io.Reader
io.Reader 接口的定义非常简单:
|
|
这个数据源可以是任意来源,从文件到网络。Reader 方法的语义是:从底层数据源中读取数据,并将其填充到用户提供的字节切片 p 中。
调用时,Read 会将最多 len(p) 字节的数据读入 p 中,返回实际读取的字节数(n)以及遇到的错误(err)。在错误返回时,官方文档允许两种行为:错误随读取的字节数一同返回、错误在下一次调用时返回。为此,官方文档推荐优先处理已读取的字节,再解决错误,以便兼容这两种错误行为。
以下是一个简单的 Reader 示例,os.File 实现了 io.Reader 接口,因此可以将 os.File 当作一个简单的 Reader 进行读取:
|
|
io.ReadAll:io.ReadAll 会一次性从流中读取所有数据并自动扩充 slice,因此不应用其处理大文件。
io.Copy:
|
|
io.Copy 是 copyBuffer 的包装,其功能是使用一个大小固定为 32KB 的缓冲区将数据从 src 搬运至 dst。copyBuffer 实现了一个小优化,如果 src 实现了 WriterTo,复制操作通过调用 src.WriteTo(dst) 来实现。或者,如果 dst 实现了 ReaderFrom,复制操作通过调用 dst.ReadFrom(src) 来实现。以此来避免额外的内存分配以及数据复制。
|
|
bufio.Reader:
|
|
bufio.Reader 作为一个 Reader 的封装,将预先申请一大块缓冲区,读取时首先将数据尽可能载入缓冲区。之后,每次调用 bufio.Reader 时将从缓冲区提供数据,以此避免直接读取数据源导致潜在的内核态和用户态的转换。
io.Writer
io.Writer 与 io.Reader 非常相似,其功能将 slice p 中的数据写入目标。返回值 n 表示实际写入了多少字节。理想情况下, n 应该与 len(p) 相同——表示整个切片都已写入。如果 n 小于 len(p) ,表示只有部分数据被写入,而 err 将返回具体出了什么问题。
|
|
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 中。
|
|
注意,WriterTo 是由数据源实现,它和我们熟知的模式(接收方主动读取数据源并处理)不一样,WriterTo 是数据源主动推送到接收方。同时,WriterTo 并不关心接收方具体是什么设备,只要接收方实现了 Writer 接口即可。
与 WriterTo 完全对应的是 ReaderFrom。ReaderFrom 的核心语义是:实现该接口的对象(数据目标)知道如何以最有效的方式,从给定的 io.Reader 中读取(拉取)所有可用的数据,并存储到自身内部。
以 *bytes.Buffer 为例,当调用 buffer.ReadFrom(reader) 时,bytes.Buffer 会高效地分配或扩展自己的内部切片,并将其作为缓冲区直接传递给 reader.Read(),从而避免了用户态的缓存。