0%

go 库学习之 io

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
2
3
type Reader interface {
Read(p []byte) (n int, err error)
}

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
2
3
type Writer interface {
Write(p []byte) (n int, err error)
}

Write 方将 p 字节切片中的 len(p) 长的字节数据写入到 Writer 所表示的数据目的地中。它返回所写入的字节长度(0 <= n <= len(p) 以及遇到的错误(导致写入提前停止):

  • 如果返回值 n < len(p),必须返回一个 non-nil 错误
  • Write 操作不能修改 p 中的数据,即使临时性修改也不行

Closer 接口

Closer 接口类型用于表示一个 可关闭 的 I/O 对象,它的定义如下:

1
2
3
type Closer interface {
Close() error
}

Seeker 接口

Seeker 接口类型用于表示一个 可寻址 的 I/O 对象,即可以设置该 I/O 对象下次读写的位置。它的定义如下:

1
2
3
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}

whence 参数表示 offset 参数的参考点,它可以是如下值:

  • SeekStart:相对起始位置
  • SeekCurrent:相对当前位置
  • SeekEnd:相对结束位置

ReaderFrom 接口

ReaderFrom 接口定义了 ReadFrom 方法,表示可以从某个 Reader 中读取数据:

1
2
3
type ReaderFrom interface {
ReadFrom(r Reader) (n int64, err error)
}

ReadFrom 方法会一直从 r 中读取数据,直到 r 返回 EOF 或者其他错误。此时 ReadFrom 方法返回读取到的字节数,以及遇到的错误。如果是因为 EOF 错误而不再读取,ReadFrom 不会返回该错误

WriterTo 接口

WriterTo 接口定义了 WriteTo 方法,表示可以将数据写入到某个 Writer 中:

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

WriteTo 方法会一直将数据写入到 w 中,直到没有更多数据可以写入或者遇到错误。此时 WriteTo 方法返回写入的字节数以及遇到的错误。

ReaderAt 接口

ReaderAt 接口定义了 ReadAt 方法,实现了该接口的 I/O 对象支持从其指定位置读取数据:

1
2
3
type ReaderAt interface {
ReadAt(p []byte, off int64) (n int, err error)
}

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
2
3
type WriterAt interface {
WriteAt(p []byte, off int64) (n int, err error)
}

WriteAt 方法返回成功写入的字节数(0 <= n <= len(p))以及遇到的错误(导致写入提前停止)。如果 n < len(p) 时,WriteAt 方法必须返回一个 non-nil 错误来解释为什么无法继续写入数据。

ReadWriter 接口及其他组合接口类型

ReadWriter 接口类型表示一个 可读、可写 的 I/O 对象。它其实就是 ReaderWriter 接口的组合,它的定义如下:

1
2
3
4
type ReadWriter interface {
Reader
Writer
}

io 包还定义了一些组合接口类型,都比较简单,列举如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
type ReadCloser interface {
Reader
Closer
}

type WriteCloser interface {
Writer
Closer
}

type ReadWriteCloser interface {
Reader
Writer
Closer
}

type ReadSeeker interface {
Reader
Seeker
}

type ReadSeeker interface {
Reader
Seeker
}

type ReadSeekCloser interface {
Reader
Seeker
Closer
}

type WriteSeeker interface {
Writer
Seeker
}

type ReadWriteSeeker interface {
Reader
Writer
Seeker
}

每次一字节 I/O 操作

io 包定义了如下接口类型,以支持每次一字节的 I/O 操作:

1
2
3
4
5
6
7
8
9
10
11
12
type ByteReader interface {
ReadByte() (byte, error)
}

type ByteScanner interface {
ByteReader
UnreadByte() error
}

type ByteWriter interface {
WriteByte(c byte) error
}

每次一 rune I/O 操作

io 包定义了如下接口类型,以支持每次一 rune 的 I/O 操作:

1
2
3
4
5
6
7
8
type RuneReader interface {
ReadRune() (r rune, size int, err error)
}

type RuneScanner interface {
RuneReader
UnreadRune() error
}

其他接口类型

StringWriter 接口类型定义了 WriteString 方法,用于将字符串写入到目的地中。

1
2
3
type StringWriter interface {
WriteString(s string) (n int, err error)
}

工具函数

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
2
3
func ReadFull(r Reader, buf []byte) (n int, err error) {
return ReadAtLeast(r, buf, len(buf))
}

Copy 函数从 src 中读取数据,写入到 dst 中,直到遇到 EOF(此时认为拷贝成功)或者出现错误。它返回所拷贝的字节数以及对应的错误信息。拷贝成功 err 总是返回 nil,而不是 EOF。

1
2
3
func Copy(dst Writer, src Reader) (written int64, err error) {
return copyBuffer(dst, src, nil)
}

CopyBuffer 函数和 Copy 类似,只不过它使用用户指定缓冲区 buf 来保存中间数据,而不是临时申请 buf。

  • 如果指定的 buf 为 nil,则还是会临时申请 buf
  • 如果指定的 buf 为 0 长度,则直接 panic
1
func CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error)

CopyCopyBuffer 函数内部都是基于 copyBuffer 函数实现的:

  • 首先判断 src 是否实现了 WriterTo 接口。如果实现了,直接调用其 WriteTo 方法即可
  • 再判断 dst 是否实现了 ReaderFrom 接口。如果实现了,直接调用其 ReadFrom 方法即可
  • 最后才尝试 读取-写入 操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
// 如果 src 实现了 WriterTo 接口,直接调用 WriteTo 即可
// 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)
}
// 如果 dst 实现了 ReaderFrom 接口,直接调用 ReadFrom 即可
// Similarly, if the writer has a ReadFrom method, use it to do the copy.
if rf, ok := dst.(ReaderFrom); ok {
return rf.ReadFrom(src)
}
// 申请 buffer
if buf == nil {
size := 32 * 1024
// 如果 src 是一个 LimitedReader,那么每次最多只能读取 l.N 大小
if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
if l.N < 1 {
size = 1
} else {
size = int(l.N)
}
}
buf = make([]byte, size)
}
for {
// 读取数据到 buf 中
nr, er := src.Read(buf)
// 先判断 nr 是否大于 0,处理已经读取的数据
if nr > 0 {
nw, ew := dst.Write(buf[0:nr])
if nw < 0 || nr < nw {
nw = 0
if ew == nil {
ew = errInvalidWrite
}
}
written += int64(nw)
// 写入发生错误,直接终止
if ew != nil {
err = ew
break
}
// 读取的数据和写入数据长度不一致,也终止
if nr != nw {
err = ErrShortWrite
break
}
}
// 再处理读取的 err
if er != nil {
// 读取时非 EOF 才认为是错误
if er != EOF {
err = er
}
break
}
}
return written, err
}

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
2
3
4
5
6
type LimitedReader struct {
// 底层 Reader
R Reader // underlying reader
// 当前还剩余的可读字节数
N int64 // max bytes remaining
}

LimitedReader 实现的 Read 方法会根据 LimitedReader.N 判断是否可以继续读取。如果 N <= 0,Read 直接返回 0, EOF,否则返回读取的字节数(读取时会限制本次能够读取的最大长度),以及对应的错误信息。

TeeReader

TeeReader 接收一个 Reader 和 Writer 作为参数,并返回一个新的 teeReader。当从该 teeReader 中读取数据时,它会负责从底层的 r 中读取数据,并将读取到的数据写入到 w 中,然后才返回读取到的数据。

1
2
3
4
5
6
7
8
9
func TeeReader(r Reader, w Writer) Reader {
return &teeReader{r, w}
}

// teeReader 内部类型
type teeReader struct {
r Reader
w Writer
}

SectionReader

SectionReader 可以从底层 ReaderAt 的指定 offset 处开始读取指定数量的数据,它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
type SectionReader struct {
// 底层的 Reader
r ReaderAt // constant after creation
// 创建时设置的初始偏移
base int64 // constant after creation
// 当前的读取偏移
off int64
// 结束位置,到达该位置后就不能再读取
limit int64 // constant after creation
// 需要读取的字节数
n int64 // constant after creation
}

SectionReader 中的 baselimit 字段就限制了其从底层 Reader 中读取的数据范围。通过 NewSectionReader 来创建一个 SectionReader 对象。其实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader {
var remaining int64
const maxint64 = 1<<63 - 1
if off <= maxint64-n {
// 没有溢出,设置正确的结束位置
remaining = n + off
} else {
// 如果溢出,直接设置结束位置为 maxint64
remaining = maxint64
}
return &SectionReader{r, off, off, remaining, n}
}

SectionReader 实现了 ReaderReaderAtSeeker 接口。

OffsetWriter

OffsetWriter 可以从底层的 WriterAt 的指定 offset 处开始写入数据。它的定义如下:

1
2
3
4
5
6
7
8
type OffsetWriter struct {
// 底层的 WriterAt
w WriterAt
// 创建时设置的初始偏移
base int64 // the original offset
// 当前的写入偏移
off int64 // the current offset
}

NewOffsetWriter 函数用于创建一个 OffsetWriter 对象:

1
2
3
func NewOffsetWriter(w WriterAt, off int64) *OffsetWriter {
return &OffsetWriter{w, off, off}
}

每次通过 OffsetWriter.Write 写入数据时,它会从底层 WriterAt OffsetWriter.off 处开始写入,写入完成后更新 OffsetWriter.off

1
2
3
4
5
func (o *OffsetWriter) Write(p []byte) (n int, err error) {
n, err = o.w.WriteAt(p, o.off)
o.off += int64(n)
return
}

OffsetWriter.WriteAt 写入数据时,它会基于 OffsetWriter.base 实时计算本次写入的偏移,写入完成后也不会对 OffsetWriter.off 进行更改:

1
2
3
4
5
6
7
8
9
10
func (o *OffsetWriter) WriteAt(p []byte, off int64) (n int, err error) {
if off < 0 {
return 0, errOffset
}

// Off 需要基于 o.base 实时计算
off += o.base
// 写入完成后,也不会对 o.off 进行更改
return o.w.WriteAt(p, off)
}

OffsetWriter 也实现了 Seeker 接口,用来更新 OffsetWriter.off

MultiReader

MultiReader 函数会创建一个 multiReader 的 Reader 对象,它可以实现从多个 Reader 中读取数据,逻辑上就相当于将多个 Reader 中的数据串联起来。

1
2
3
4
5
func MultiReader(readers ...Reader) Reader {
r := make([]Reader, len(readers))
copy(r, readers)
return &multiReader{r}
}

multiReader 实现了 Read 方法,会尝试按顺序从底层的 readers 中读取数据,如果某个 reader 读取完成,则从下一个 reader 读取。

MultiWriter

MultiWriter 函数会创建一个 multiWriter 的 Writer 对象,它可以实现将一份数据同时写入多个 Writer。

1
2
3
4
5
6
7
8
9
10
11
12
13
func MultiWriter(writers ...Writer) Writer {
allWriters := make([]Writer, 0, len(writers))
for _, w := range writers {
// 如果当前 writer 自己就是 multiWriter
if mw, ok := w.(*multiWriter); ok {
// 将其所有元素添加进来
allWriters = append(allWriters, mw.writers...)
} else {
allWriters = append(allWriters, w)
}
}
return &multiWriter{allWriters}
}

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
2
3
type FS interface {
Open(name string) (File, error)
}

File 接口类型则用于表示一个文件的最小实现,这里的 文件 可以是常规文件、目录文件、符号链接等等:

1
2
3
4
5
type File interface {
Stat() (FileInfo, error)
Read([]byte) (int, error)
Close() error
}

DirEntry 接口类型用于表示一个 目录项,即目录里的某个子项(或者称为子文件):

1
2
3
4
5
6
type DirEntry interface {
Name() string
IsDir() bool
Type() FileMode
Info() (FileInfo, error)
}

ReadDirFile 用于表示一个 目录文件,可以通过它的 ReadDir 方法获取该目录文件下的所有 目录项

1
2
3
4
5
6
7
type ReadDirFile interface {
// ReadDirFile 也是一个 File
File

// n 用于设置读取最多 n 个目录项,如果 n <= 0,则读取所有目录项
ReadDir(n int) ([]DirEntry, error)
}

FileInfo 接口类型用于表示 Stat 函数返回的文件信息:

1
2
3
4
5
6
7
8
type FileInfo interface {
Name() string // base name of the file
Size() int64 // length in bytes for regular files; system-dependent for others
Mode() FileMode // file mode bits
ModTime() time.Time // modification time
IsDir() bool // abbreviation for Mode().IsDir()
Sys() any // underlying data source (can return nil)
}

FS 接口类型只是表示文件系统的最小接口类型,io/fs 基于 FS 接口类型还定义了一系列接口类型,用于表示支持不同功能的文件系统。这些接口类型包裹:

1
2
3
4
5
6
7
// GlobFS 是一个提供了 Glob 方法的文件系统
type GlobFS interface {
FS

// 可以对文件进行模式匹配(即支持我们所说的文件通配符),返回匹配 pattern 的文件名称列表
Glob(pattern string) ([]string, error)
}
1
2
3
4
5
6
7
// ReadDirFS 是一个提供 ReadDir 方法的文件系统
type ReadDirFS interface {
FS

// 读取 name 目录,返回一系列的目录项
ReadDir(name string) ([]DirEntry, error)
}
1
2
3
4
5
6
7
// ReadFileFS 是一个提供了 ReadFile 方法的文件系统
type ReadFileFS interface {
FS

// 读取 name 文件,返回文件内容。成功读取应该总是返回 nil 错误,而不是 io.EOF
ReadFile(name string) ([]byte, error)
}
1
2
3
4
5
6
7
// StatFS 表示一个提供 Stat 方法的文件系统
type StatFS interface {
FS

// 返回 name 文件的 FileInfo
Stat(name string) (FileInfo, error)
}
1
2
3
4
5
6
7
// SubFS 是一个提供 Sub 方法的文件系统
type SubFS interface {
FS

// Sub 方法返回一个根目录在 dir 的 Sub 文件系统
Sub(dir string) (FS, error)
}

基于这些接口类型,io/fs 包也提供了一系列工具函数,用于实现文件系统的基本操作。这些函数包括:

1
2
3
4
5
6
7
8
9
10
11
func Glob(fsys FS, pattern string) (matches []string, err error)

func ReadDir(fsys FS, name string) ([]DirEntry, error)

func ReadFile(fsys FS, name string) ([]byte, error)

func Stat(fsys FS, name string) (FileInfo, error)

func Sub(fsys FS, dir string) (FS, error)

func WalkDir(fsys FS, root string, fn WalkDirFunc) error

我们可以看其中 Stat 函数的实现,其他函数的实现思路都是类似的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Stat 函数返回指定 name 的文件信息
func Stat(fsys FS, name string) (FileInfo, error) {
// 如果 FS 实现了 StatFS,直接调用其自己的方法实现
if fsys, ok := fsys.(StatFS); ok {
return fsys.Stat(name)
}

// 提供一个通用实现
// 调用 file 自己的 Stat 方法,
file, err := fsys.Open(name)
if err != nil {
return nil, err
}
defer file.Close()
return file.Stat()
}

小结

本篇文章学习了 Go 标准库的 io 包和 io/fs 包,学习了 Go 对其 I/O 模型的基本抽象,这也是后续进一步学习 Go 标准库其他包所需的基础知识。