Go 标准库的 io 包定义了 Go 语言基本的 I/O 模型,它提供了各种与 I/O 相关的接口类型,同时也提供了一些工具类型和函数,以提供一些扩展功能。
核心接口类型
Go io 包的一个核心功能就是提供对各种 I/O 功能的抽象。io 包通过定义各种接口类型来实现对可读、可写等对象的抽象,这样 I/O 相关的代码逻辑只需要与这些接口类型进行交互,而不用关心具体的 I/O 对象,这些具体的 I/O 对象可以是磁盘文件、网络连接、或者是内存中的一块 Buffer。
另外这种抽象也方便 Go 实现包裹函数模式,可以基于某个 I/O 对象创建一个新的 I/O 对象,新 I/O 对象在原有 I/O 对象的基础上添加了某些新的特性,例如在文件 I/O 对象上提供缓冲特性。
下面就列出 io 包定义的核心接口类型。
Reader 接口
Reader 接口类型用于表示一个 可读对象,即可以从 Reader 所表示的数据源中读取数据。Reader 接口类型定义如下:
1 | type Reader interface { |
Read 方法从数据源中读取最多 len(p) 字节的数据到字节切片 p 中。它返回所读取到的字节长度(0 <= n <= len(p) 以及遇到的错误。
- 如果当前存在可读数据(即使不是 len(p) 长度),Read 操作也会立即读取并返回,而不是等待更多数据
- 当 Read 成功读取了 n 字节(n > 0)后,遇到了错误或者遇到 EOF(end-of-file,EOF),本次调用应该返回成功读取到的字节数。至于 err 返回值,可以在本次调用返回 non-nil 错误,也可以在下一次调用返回错误(以及 n == 0)
- 调用者在考虑 error 返回值之前,应该始终先处理
n > 0的情况 - 如果
len(p) == 0,Read 函数应该总是返回 n == 0。如果某些错误情况已经发生(例如 EOF),它也可以返回 non-nil error - 不鼓励
实现返回0,nil error(除非 len(p) == 0)。调用者应该把0, nil返回值当作什么都没发生,而不代表 EOF
Writer 接口
Writer 接口类型用于表示一个 可写对象,即可以将数据写入到 Writer 所表示的数据目的地中。Writer 接口类型定义如下:
1 | type Writer interface { |
Write 方将 p 字节切片中的 len(p) 长的字节数据写入到 Writer 所表示的数据目的地中。它返回所写入的字节长度(0 <= n <= len(p) 以及遇到的错误(导致写入提前停止):
- 如果返回值 n < len(p),必须返回一个 non-nil 错误
- Write 操作不能修改 p 中的数据,即使临时性修改也不行
Closer 接口
Closer 接口类型用于表示一个 可关闭 的 I/O 对象,它的定义如下:
1 | type Closer interface { |
Seeker 接口
Seeker 接口类型用于表示一个 可寻址 的 I/O 对象,即可以设置该 I/O 对象下次读写的位置。它的定义如下:
1 | type Seeker interface { |
whence 参数表示 offset 参数的参考点,它可以是如下值:
- SeekStart:相对起始位置
- SeekCurrent:相对当前位置
- SeekEnd:相对结束位置
ReaderFrom 接口
ReaderFrom 接口定义了 ReadFrom 方法,表示可以从某个 Reader 中读取数据:
1 | type ReaderFrom interface { |
ReadFrom 方法会一直从 r 中读取数据,直到 r 返回 EOF 或者其他错误。此时 ReadFrom 方法返回读取到的字节数,以及遇到的错误。如果是因为 EOF 错误而不再读取,ReadFrom 不会返回该错误。
WriterTo 接口
WriterTo 接口定义了 WriteTo 方法,表示可以将数据写入到某个 Writer 中:
1 | type WriterTo interface { |
WriteTo 方法会一直将数据写入到 w 中,直到没有更多数据可以写入或者遇到错误。此时 WriteTo 方法返回写入的字节数以及遇到的错误。
ReaderAt 接口
ReaderAt 接口定义了 ReadAt 方法,实现了该接口的 I/O 对象支持从其指定位置读取数据:
1 | type ReaderAt interface { |
ReadAt 方法返回成功读取的字节数(0 <= n <= len(p)),以及遇到的错误。当 ReadAt 返回 n < len(p) 时,ReadAt 方法必须返回一个 non-nil 错误来解释为什么无法进一步读取数据。这方面 ReadAt 比 Read 更严格。另外,如果当前可读的数据不足 len(p),ReadAt 会一直阻塞直到所要求长度的数据被读取成功或者遇到错误。ReadAt 的这个行为也和 Read 不同。
WriterAt 接口
WriterAt 接口定义了 WriteAt 方法,实现了该接口的 I/O 对象支持在指定位置写入数据:
1 | type WriterAt interface { |
WriteAt 方法返回成功写入的字节数(0 <= n <= len(p))以及遇到的错误(导致写入提前停止)。如果 n < len(p) 时,WriteAt 方法必须返回一个 non-nil 错误来解释为什么无法继续写入数据。
ReadWriter 接口及其他组合接口类型
ReadWriter 接口类型表示一个 可读、可写 的 I/O 对象。它其实就是 Reader 和 Writer 接口的组合,它的定义如下:
1 | type ReadWriter interface { |
io 包还定义了一些组合接口类型,都比较简单,列举如下:
1 | type ReadCloser interface { |
每次一字节 I/O 操作
io 包定义了如下接口类型,以支持每次一字节的 I/O 操作:
1 | type ByteReader interface { |
每次一 rune I/O 操作
io 包定义了如下接口类型,以支持每次一 rune 的 I/O 操作:
1 | type RuneReader interface { |
其他接口类型
StringWriter 接口类型定义了 WriteString 方法,用于将字符串写入到目的地中。
1 | type StringWriter interface { |
工具函数
io 包基于这些接口类型,定义了一些基础的工具函数,以提供一些通用的 io 功能。这些 io 工具函数都以接口类型作为参数,使得这些函数可以作用于各种 io 实现。
ReadAtLeast 函数从 reader 中至少读取 n 字节数据,它返回所读取的字节数,以及错误信息。
- 只有当读取的字节数小于所要求的 min 字节时,才会返回 non-nil 错误。这也意味着如果返回了 n,则肯定不会有错误
- 如果没读取任何字节,就遇到了 EOF 错误,此时 ReadAtLeast 才返回 EOF 错误
- 如果读取了部分数据,才遇到 EOF 错误,此时 ReadAtLeast 返回 ErrUnexpectedEOF
1 | func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error) |
ReadFull 从 Reader 中读取数据,直至 buf 缓冲区满或者遇到错误。它返回所读取的字节数,以及错误信息。由于它只是对 ReadAtLeast 的封装,所以返回逻辑与 ReadAtLeast 一致。
1 | func ReadFull(r Reader, buf []byte) (n int, err error) { |
Copy 函数从 src 中读取数据,写入到 dst 中,直到遇到 EOF(此时认为拷贝成功)或者出现错误。它返回所拷贝的字节数以及对应的错误信息。拷贝成功 err 总是返回 nil,而不是 EOF。
1 | func Copy(dst Writer, src Reader) (written int64, err error) { |
CopyBuffer 函数和 Copy 类似,只不过它使用用户指定缓冲区 buf 来保存中间数据,而不是临时申请 buf。
- 如果指定的 buf 为 nil,则还是会临时申请 buf
- 如果指定的 buf 为 0 长度,则直接 panic
1 | func CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) |
Copy 和 CopyBuffer 函数内部都是基于 copyBuffer 函数实现的:
- 首先判断 src 是否实现了
WriterTo接口。如果实现了,直接调用其 WriteTo 方法即可 - 再判断 dst 是否实现了
ReaderFrom接口。如果实现了,直接调用其 ReadFrom 方法即可 - 最后才尝试
读取-写入操作
1 | func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) { |
CopyN 从 src 拷贝 N 个字节到 dst 中,它返回实际拷贝的字节数以及错误信息
- 如果返回的 written 为 n,则 err 总是 nil
- 如果是因为 Reader 返回 EOF 而导致拷贝提前终止,则 err 为 nil
- 否则 err 为实际的错误信息
1 | func CopyN(dst Writer, src Reader, n int64) (written int64, err error) |
ReadAll 函数从 Reader 中读取数据,直到遇到 EOF 或者发生错误,之后返回读取到的数据以及错误信息。如果是因为遇到 EOF 而终止读取,ReadAll 应该返回 nil 错误。
1 | func ReadAll(r Reader) ([]byte, error) |
工具 I/O 类型
io 包还提供了一些工具类型,也是为了给库的使用者提供一些通用的 io 功能。
LimitedReader
LimitedReader 可以限制从底层 Reader 中读取数据的长度。它的定义如下:
1 | type LimitedReader struct { |
LimitedReader 实现的 Read 方法会根据 LimitedReader.N 判断是否可以继续读取。如果 N <= 0,Read 直接返回 0, EOF,否则返回读取的字节数(读取时会限制本次能够读取的最大长度),以及对应的错误信息。
TeeReader
TeeReader 接收一个 Reader 和 Writer 作为参数,并返回一个新的 teeReader。当从该 teeReader 中读取数据时,它会负责从底层的 r 中读取数据,并将读取到的数据写入到 w 中,然后才返回读取到的数据。
1 | func TeeReader(r Reader, w Writer) Reader { |
SectionReader
SectionReader 可以从底层 ReaderAt 的指定 offset 处开始读取指定数量的数据,它的定义如下:
1 | type SectionReader struct { |
SectionReader 中的 base 和 limit 字段就限制了其从底层 Reader 中读取的数据范围。通过 NewSectionReader 来创建一个 SectionReader 对象。其实现如下:
1 | func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader { |
SectionReader 实现了 Reader、ReaderAt、Seeker 接口。
OffsetWriter
OffsetWriter 可以从底层的 WriterAt 的指定 offset 处开始写入数据。它的定义如下:
1 | type OffsetWriter struct { |
NewOffsetWriter 函数用于创建一个 OffsetWriter 对象:
1 | func NewOffsetWriter(w WriterAt, off int64) *OffsetWriter { |
每次通过 OffsetWriter.Write 写入数据时,它会从底层 WriterAt OffsetWriter.off 处开始写入,写入完成后更新 OffsetWriter.off。
1 | func (o *OffsetWriter) Write(p []byte) (n int, err error) { |
而 OffsetWriter.WriteAt 写入数据时,它会基于 OffsetWriter.base 实时计算本次写入的偏移,写入完成后也不会对 OffsetWriter.off 进行更改:
1 | func (o *OffsetWriter) WriteAt(p []byte, off int64) (n int, err error) { |
OffsetWriter 也实现了 Seeker 接口,用来更新 OffsetWriter.off。
MultiReader
MultiReader 函数会创建一个 multiReader 的 Reader 对象,它可以实现从多个 Reader 中读取数据,逻辑上就相当于将多个 Reader 中的数据串联起来。
1 | func MultiReader(readers ...Reader) Reader { |
multiReader 实现了 Read 方法,会尝试按顺序从底层的 readers 中读取数据,如果某个 reader 读取完成,则从下一个 reader 读取。
MultiWriter
MultiWriter 函数会创建一个 multiWriter 的 Writer 对象,它可以实现将一份数据同时写入多个 Writer。
1 | func MultiWriter(writers ...Writer) Writer { |
Pipe
Pipe 用于创建一个同步的内存型管道,通过它可以将读(需要一个 io.Reader)、写(需要一个 io.Writer)两端连接起来。通过 Pipe 函数创建一个管道,并返回该管道的读写两端:
1 | func Pipe() (*PipeReader, *PipeWriter) |
创建的管道是同步型的,这意味着 PipeWriter 写入数据会被阻塞,直到有消费者从 PipeReader 中读取数据。写入的数据是直接拷贝到读端,中间不会使用内部 buffer。Pipe 是并发安全的。
其他工具
io包还提供了一个DiscardWriter 对象,往Discard中写入数据不会做任何实际操作。NopCloser用于包装一个 Reader 对象,并返回一个ReadCloser对象,新的ReadCloser对象的Close方法不做任何实际操作
io/fs
io/fs 包定义了与文件系统相关的接口类型,用于实现对文件系统的抽象。其中 FS 接口类型用于表示一个层次化文件系统的最小实现:
1 | type FS interface { |
File 接口类型则用于表示一个文件的最小实现,这里的 文件 可以是常规文件、目录文件、符号链接等等:
1 | type File interface { |
DirEntry 接口类型用于表示一个 目录项,即目录里的某个子项(或者称为子文件):
1 | type DirEntry interface { |
ReadDirFile 用于表示一个 目录文件,可以通过它的 ReadDir 方法获取该目录文件下的所有 目录项:
1 | type ReadDirFile interface { |
FileInfo 接口类型用于表示 Stat 函数返回的文件信息:
1 | type FileInfo interface { |
FS 接口类型只是表示文件系统的最小接口类型,io/fs 基于 FS 接口类型还定义了一系列接口类型,用于表示支持不同功能的文件系统。这些接口类型包裹:
1 | // GlobFS 是一个提供了 Glob 方法的文件系统 |
1 | // ReadDirFS 是一个提供 ReadDir 方法的文件系统 |
1 | // ReadFileFS 是一个提供了 ReadFile 方法的文件系统 |
1 | // StatFS 表示一个提供 Stat 方法的文件系统 |
1 | // SubFS 是一个提供 Sub 方法的文件系统 |
基于这些接口类型,io/fs 包也提供了一系列工具函数,用于实现文件系统的基本操作。这些函数包括:
1 | func Glob(fsys FS, pattern string) (matches []string, err error) |
我们可以看其中 Stat 函数的实现,其他函数的实现思路都是类似的:
1 | // Stat 函数返回指定 name 的文件信息 |
小结
本篇文章学习了 Go 标准库的 io 包和 io/fs 包,学习了 Go 对其 I/O 模型的基本抽象,这也是后续进一步学习 Go 标准库其他包所需的基础知识。