原文

Go 是一种被设计为处理bytes数据的编程语言。无论你拥有的是bytes数组,bytes数据流,或者是单个byte,Go 使它易于处理。自那些简洁的原语,我们构建我们的抽象概念和服务。

io 包是标准库中最基础的包之一。它为处理bytes数据流提供了一个接口和辅助函数的集合。

Reading bytes

当处理bytes时,有两个基础的操作:reading 和 writing。让我们先看一下读取bytes。

Reader interface

为从数据流中读取bytes的基础结构是 Reader 接口:

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

这个接口的实现遍及标准库,从 network connectionsfiles, 再到 wrappers for in-memory slices

Reader 实现是通过传递一个buffer(p)到 Read 方法,因此我们能够复用同一个bytes。如果 Read() 返回一个byte切片而不是接受一个byte切片作为参数,那么,每次调用Read()时,reader将不得不分配一个新的byte切片。那将会在垃圾回收器上肆虐。

Reader 接口的一个问题是它带来了一些微妙的规则。首先,当数据流被读完时,它将返回一个io.EOF错误作为通常使用的一部分。这可能会使初学者感到不解。第二,它不保证你的buffer被填满。如果你传递一个8个byte大小的切片,你可以收到0到8个字节。处理部分读取可能是凌散和容易出错的。幸运的是,io 包为这个问题提供了一些辅助函数。

Improving reader guarantees

比如说,你有一个正在解析的的协议,并且你知道你需要从一个reader中读取8个byte大小的uint64类型值。在这种情况下,最好是使用io.ReadFull(),一位你要读取一断固定大小的数据:

1
func ReadFull(r Reader, buf []byte) (n int, err error)

这个函数确保你的buffer在返回之前被数据完全填满。如果你的buffer是部分读取,那么你将收到一个io.ErrUnexpectedEOF。如果没有读取到bytes,那么它将返回一个io.EOF。这个简单的保证极大地简化了你的代码。读取8个bytes,你只需要这样做:

1
2
3
4
5
6
buf := make([]byte, 8)
if _, err := io.ReadFull(r, buf); err == io.EOF {
return io.ErrUnexpectedEOF
} else if err != nil {
return err
}

也有许多高层解析器,比如 binary.Read(), 它能用来解析特殊类型数据。在将来的的 go walkthroughs 中,我们将会涉及那些在不同包中的解析器。

另一个相对更少用到的辅助函数是 ReadAtLeast

1
func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)

如果有额外数据能够读取,这个函数将读取它们到你的buffer中, 但是这个函数总会返回最小数量的bytes。我个人从未发现过对这函数的需求,但是我能看到,如果你需要去尽可能少地调用 Read() 并且你乐意去缓存额外数据,它会很有用。

Concatenating streams

很多时候,你会遇到需要合多个reader为一的情况。你能够用 MultiReader 将它们组合成单个reader:

1
func MultiReader(readers ...Reader) Reader

举个例子,你可能正在发送一个HTTP请求体,这个请求体由存在内存中头部和储存在硬盘上的数据组成。许多人将尝试拷贝头部和文件到一块内存中的缓存区,但是这样比较慢并且会使用许多内存。

这里有一条捷径:

1
2
3
4
5
r := io.MultiReader(
bytes.NewReader([]byte("...my header...")),
myFile,
)
http.Post("http://example.com", "application/octet-stream", r)

MultiReader 让 http.Post() 把连个reader当成一个连接在一起的reader。

Duplicating streams

当使用readers时,你可能遇到的一个问题是,一旦一个reader被读取,那些已被读取的数据就不能再次被读取。举个例子,你的应用可能解析HTTP请求体失败,然而你不能去debug修复这个问题,因为解析器已经消耗完了所有的数据。

在不妨碍reader读取数据时,TeeReader 是一个获得reader的数据的好选择。

1
func TeeReader(r Reader, w Writer) Reader

这个函数构建了一个新的reader,它封装了你的reader,r。任何从新的reader中读取的数据将会被写到w中。这个writer可以是任何东西,从一个内存缓冲区到一个日志文件输出到标准错误。

举个例子,你肯能获得像这样的糟糕请求:

1
2
3
4
5
6
7
var buf bytes.Buffer
body := io.TeeReader(req.Body, &buf)
// ... process body ...
if err != nil {
// 检查buf
return err
}

然而,有一点很重要:限制你正在获取数据的请求体以便你不会用完内存。

Restricting stream length

因为数据流是无边无际的,在某些脚本中,他们可能会造成内存和磁盘的问题。最常见的例子是一个文件上传端点。典型的,端点有大小限制,为了防止磁盘塞满,然而,手动实现这个可能会很乏味。

LimitReader 提供这个功能通过产生一个封装的reader,它能限制bytes读取的总数:

1
func LimitReader(r Reader, n int64) Reader

LimitReader的一个问题是:如果你的底层reader数据量超过n,它将不会告诉你。它将简单地返回io.EOF,一旦从r中读取了n个byte。有一个你能使用的小技巧,设置限制为n+1,然后在检查最终是否你已经读取了超过n个byte。

Writing bytes

现在,我们已经涉及了从数据流当中读取byte,让我们看看如何将它们写进数据流里面。

writer interface

Writer 接口是一个简单的Reader的倒转。我们将一个byte缓冲区推到一个数据流中。

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

一般来说,写入byte比读取它们更简单。Readers 使处理数据更加复杂,因为他们允许部分读取,然而,部分写入将总是返回错误。

Duplicating writes

有时候,你将想在多个数据流中写入数据,也许是写到日志文件或错误日志。这与TeeReader相似,除了我们想复制写入而不是复制读取。

MultiWriter 在这种情况下就派上了用处:

1
func MultiWriter(writers ...Writer) Writer

这个名字是有点混乱的,因为它不是 MultiReader 的 writer 版本。MultiWriter返回一个writer,它重复每一个写入到多个writer中, 而MultiReader把多个reader联结成一个。

我广泛地使用MultiWriter在那些我需要去断言一个服务程序日志打印得正确的单元测试中。

1
2
3
4
5
6
7
type MyService struct {
LogOutput io.Writer
}
...
var buf bytes.Buffer
var s MyService
s.LogOutput = io.MultiWriter(&buf, os.Stderr)

当看着全部日志打印在我的终端来debug时,使用一个MultiWriter允许我去检验缓存区的内容。

Optimizing string writes

在标准库中有许多有WriteString()方法的writer能被用于提高写入效率,通过当转换字符串到byte切片时不需要一个内存分配。你能通过使用io.WriteString()函数来利用这些优化。

io.WriteString() 是简单的。它先检查writer是否实现了一个WriteString(), 如果实现了就使用它。否则它会回头去复制字符串到一个byte切片并且使用Write()方法。

Copying bytes

现在,我们能够读取字节,也能吸入字节,我们有理由希望将这两者接在一起,并且在reader和writer之间复制字节。

Connecting readers & writers

从reader中复制bytes到writer的最基本方法是命名贴切的 Copy function:

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

这个函数使用一个 32kb 的缓冲区来从src中读入bytes然后写到dst中。如果在读取和写入的过程中有任何除了io.EOF以外的错误发生,那么复制过程会被停止并且返回错误信息。

Copy 的一个问题是你不能保证复制字节的最大数量。举个例子,你也许想要复制一个log文件中的数据,从头到当前大小。如果在你复制的过程中,log持续增长,那么你将以复制超乎预期数量的字节结束。在这种情况下,你能够使用 [CopyN](https://golang.org/pkg/io/#CopyN) 函数来指定一个准确的被写入的字节数量。

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

Copy 的另一个问题是它每一次被调用时都需要分配一个 32kb 的缓冲区。如果你正在执行一大堆复制,那么相反你能通过使用 [CopyBuffer](https://golang.org/pkg/io/#CopyBuffer) 复用你自己的缓冲区:

func CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error)

我从未发现 Copy() 的开销非常高, 所以我个人不使用 CopyBuffer()。

Optimizing copy

为了完全避免使用中间的缓冲区,数据类型能实现一些接口来直接读写。当数据类型实现了这些接口,Copy 函数将避免中间的缓冲区并且直接使用那些实现的方法。

WriterTo 接口提过给那些想要直接写出它们的数据的数据类型:

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

我已经在BoltDB的 Tx.WriteTo() 中使用这个,它允许用户从一个事务中给数据库快照。

在读取这一边,ReaderFrom 允许一个数据类型直接从一个reader中读取数据:

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

Adapting reader & writers

有些时候,你发现你有一个接受一个Reader作为参数的函数,但是你只有一个Writer。也许你需要动态地写出数据到一个 HTTP 请求中,但是 http.NewRequest() 只接受一个Reader。

你能通过 io.Pipe() 转化一个writer:

1
func Pipe() (*PipeReader, *PipeWriter)

这个函数提供给你一个新的reader和writer。任何写入到新的 PipeWriter 的数据会到 PipeReader中。

我很少直接使用这个函数,然而,exec.Cmd 使用这个实现 Stdin,Stdout,和 Stderr 管道,当处理命令执行时它们非常有用。

Closing streams

所有好事都必须走到尽头,处理byte数据流也不例外。Closer 接口被提供作为一种关闭数据流的通用方法:

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

关于 Closer 没什么好说的,因为它非常简单,然而,我发现总是从我的 Close() 函数返回一个错误是非常有用的,因为这样我的数据类型能在需要时实现 Closer 。 Closer 一般都不直接使用,但是它有时与其他接口结合,比如 ReadCloser, WriteCloser 和 ReadWriteCloser。

Moving around within streams

数据流通常是字节从开始到结尾的一个连续流动,但是也有很多例外。举个例子,一个文件能被当作数据流操作,但是你也能跳转到一个文件中的特定位置

Seeker 接口被提供来在数据流中跳跃:

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

有三种跳跃方式:相对当前位置移动,相对开头位置移动和相对结尾位置移动。你通过使用 whence 参数指定移动的方式。offset 参数指定移动多少byte距离。

Seeker 会很有用,如果你在文件中使用固定长度的字节块,或者你的文件包含一个偏移量的引索。有时候这个引索数据被保存在文件头部,所以有道理相对开头位置移动,但是有时候这个数据在尾部被指定,所以你将需要相对结尾位置移动。

Optimizing for Data Types

如果你需要的是一个单个byte或rune,在一大块字节中读写是一件很乏味的事情。Go 提供了一些简化它的接口。

working with individual bytes

ByteReaderByteWriter 接口提供一个读写单个字节的简单接口:

1
2
3
4
5
6
type ByteReader interface {
ReadByte() (c byte, err error)
}
type ByteWriter interface {
WriteByte(c byte) error
}

你会注意到上面两个方法中没有 length 参数,因为长度总是0或1。如果没有一个字节被读写,那么它会返回一个错误。

ByteScanner 接口被提供给处理缓存字节的reader:

1
2
3
4
type ByteScanner interface {
ByteReader
UnreadByte() error
}

这个接口允许你把之前读的字节推回到reader以便于它能在下一次被读取。当写 LL解析器 时,这个是非常有用,因为它允许你看一眼下一个可用的字节。

working with individual runes

如果你正在解析Unicode数据,那么你将需要去处理runes而不是单个字节。在那种情况下,RuneReaderRuneScanner 会被使用:

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

Conclusion

字节数据流对大多数 Go 程序是至关重要的。它们是任何事的接口,从网络连接到磁盘上的文件再到用户从接盘上输入。io 包为哪些接口提供了基础。

我们已经看了读取字节,写入字节,复制字节和优化哪些操作。这些原生方法可能似乎很简单,但是他们为所有数据密集型应用提供了建筑块。请看一看 io 包,并且在你的应用程序中考虑它的接口。