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
包还提供了一个Discard
Writer 对象,往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 标准库其他包所需的基础知识。