Golang walkthrough: io package

Golang walkthrough: io package

Go是用于处理字节的编程语言。 无论您有字节列表,字节流还是单个字节,Go都可以轻松处理。 从这些简单的原语,我们构建了我们的抽象和服务。 io包是标准库中最基础的包之一。 它提供了一组用于处理字节流的接口和助手。

这篇文章是一系列演练的一部分,可以帮助您更好地了解标准库。 虽然官方的文档提供了大量的信息,但是在现实世界的环境中还是很难理解库的意思。 本系列旨在提供如何在每天应用程序中使用标准库包的上下文。 如果您有任何问题或意见,可以在Twitter上的@benbjohnson与我联系。(当然也可以联系我,listomebao@gmail.com)

Reading bytes

字节有两个最基本的操作,。让我们先看如何读字节。

Reader interface

从数据流读取字节最基本的结构是Reader接口:

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

该接口贯穿在整个标准库中的实现,从网络连接到文件都是内存片的包装。

读取器通过将缓冲区p传递给Read()方法,以便我们可以重用相同的字节。 如果Read()返回一个字节片而不是接受一个参数,那么读者将不得不在每个Read()调用上分配一个新的字节片。 这将对垃圾收集器造成严重破坏。

Reader接口的一个问题是它附带了一些细微的规则。 首先,当流完成时,它返回一个io.EOF错误作为使用的正常部分。 这可能会让初学者感到困惑。 其次,您的缓冲区不能保证填写。 如果您传递8字节的片段,则可以在0到8个字节之间的任何地方接收。 处理部分读取可能是凌乱和容易出错的。 幸运的是有这些问题的帮助函数。

Improving reader guarantees

假设你有一个协议你正在解析,你知道你需要从阅读器读取一个8字节的uint64值。 在这种情况下,最好使用io.ReadFull(),因为你有一个固定的大小读取:

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

此功能确保您的缓冲区在返回前完全填充数据。 如果您的缓冲区部分读取,那么您将收到一个io.ErrUnexpectedEOF。 如果没有读取字节,则返回io.EOF。 这个简单的保证可以极大地简化你的代码。 要读取8个字节,您只需要这样做:

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

还有许多更高级别的解析器,如处理解析特定类型的binary.Read()。 我们将在以后的演练中介绍不同的软件包。
另一个很有用的函数是ReadAtLeast():

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

此函数将读取附加数据到缓冲区中,如果可用,始终返回最小字节数。 我没有发现这个功能的需要,但如果您需要最小化Read()调用,并且您愿意缓冲附加数据,我可以看到它是有用的。

Concatenating streams

很多时候,您将遇到需要将多个读操作组合在一起的实例。 您可以使用MultiReader将它们组合成单个读取器:

func MultiReader(readers ...Reader) Reader

例如,您可能正在发送一个将内存中的http头与磁盘上的数据相结合的HTTP请求体。 许多人会尝试将标题和文件复制到内存缓冲区中,但速度很慢且使用大量的内存。 这是一个更简单的方法:

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

MultiReaderhttp.Post()将两个读接口视为一个单独的连接读接口。

Duplicating streams

使用读接口可能遇到的一个问题是,读取数据后,无法重读数据。 例如,您的应用程序可能无法解析HTTP请求正文,并且您无法调试问题,因为解析器已经消耗了数据。 TeeReader是一个很好的选择,用于捕获读者的数据,而不会干扰读接口的消费者。

func TeeReader(r Reader, w Writer) Reader

这个函数构造一个新的读接口,包装你的读接口r。 来自新读接口的任何读取也将写入w。 该Writer可以是从内存缓冲区到日志文件,或者STDERR的任何内容。 例如,您可以捕获如下所示的不良请求:

var buf bytes.Buffer
body := io.TeeReader(req.Body, &buf)
// ... process body ...
if err != nil {
        // inspect buf
        return err
}

但是,您必须限制要捕获的请求正文,以免内存不足。

Restricting stream length

因为流是无界的,所以在某些情况下可能会导致内存或磁盘问题。 最常见的示例是文件上传端点。 端点通常具有大小限制,以防止磁盘被占满,但手动执行此操作可能是乏味的。 LimitReader通过生成限制读取的总字节数的读接口来提供此功能:

func LimitReader(r Reader, n int64) Reader

LimitReader的一个问题是它不会告诉你你的底层阅读器是否超过n。 一旦从r读取n个字节,它将简单地返回io.EOF。 您可以使用的一个技巧是将限制设置为n + 1,然后检查是否在最后读取超过n个字节。

Writing bytes

现在我们已经涵盖了从流中读取字节,我们来看看如何将它们写入流。

Writer interface

Writer接口是Reader的相反操作。 我们提供一个字节缓冲区来推送到一个流。

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

一般来说,写入字节比阅读更简单。 读者使数据处理复杂化,因为它们允许部分读取,但部分写入将始终返回错误。

Duplicating writes

有时你会发送写入多个流。 也许是一个日志文件或STDERR。 这与TeeReader类似,只是我们想重复写入,而不是重复读取。

在这种情况下,MultiWriter派上用场:

func MultiWriter(writers ...Writer) Writer

这个名字有点使人疑惑,因为它和MultiReader是不一样的逻辑。 MultiReader将几个读接口连接成一个,而MultiWriter返回一个写接口,它将每个写入复制到多个写接口。 我在单元测试中广泛使用MultiWriter,我需要断言服务正在正确记录:

type MyService struct {
        LogOutput io.Writer
}
...
var buf bytes.Buffer
var s MyService
s.LogOutput = io.MultiWriter(&buf, os.Stderr)

使用MultiWriter允许我验证buf的内容,同时也看到我的终端中的完整日志输出进行调试。

Optimizing string writes

标准库中有很多写入器具有WriteString()方法,可以通过在将字符串转换为字节片段时不需要分配来提高写入性能。 您可以使用io.WriteString()函数来利用此优化。 这个函数功能简单。 它首先检查作者是否实现了WriteString()方法并使用它(如果可用)。 否则,它将返回将字符串复制到字节片并使用Write()方法。

Copying bytes

现在我们可以读取字节,我们可以写入字节,只有这样,我们才想把这两边插在一起,并在读接口和写接口之间复制。

Connecting readers & writers

将读接口的数据复制到写接口的最基本方法是Copy()函数:

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

此函数使用32KB缓冲区从src读取,然后写入dst。 如果在读取或写入中出现io.EOF之外的任何错误,则复制将被停止,并返回错误。 Copy()的一个问题是你不能保证最大的字节数。 例如,您可能希望将日志文件复制到当前文件大小。 如果日志在复制期间继续增长,那么最终会出现比预期更多的字节。 在这种情况下,您可以使用CopyN()函数来指定要写入的确切字节数:

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

Copy()的另一个问题是它需要为每个调用的32KB缓冲区分配一个。 如果您正在执行大量拷贝,那么可以使用CopyBuffer()来重用自己的缓冲区:

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

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

Optimizing copy

为避免完全使用中间缓冲区,类型可以实现直接读写的接口。 当实现时,Copy()函数将避免中间缓冲区并直接使用这些实现。 WriterTo接口适用于要直接写入数据的类型:

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

我在BoltDB的Tx.WriteTo()中使用了这一点,允许用户从事务中快照数据库。 在读取数据这方面,ReaderFrom允许读接口直接读取数据:

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

Adapting reader & writers

有时你会发现你有一个接受Reader的功能,但是你所有的都是Writer。 也许您需要将数据动态写入HTTP请求,但http.NewRequest()只接受一个Reader。 您可以使用io.Pipe()来反转写接口:

func Pipe() (*PipeReader, *PipeWriter)

这为您提供了一个新的读接口和写接口。 对新的PipeWriter的任何写入将转到PipeReader。 我很少直接使用这个功能,但是exec.Cmd使用它来实现StdinStdoutStderr管道,这在使用命令执行时非常有用。

Closing streams

所有好的事情都必须结束,这在使用字节流时也不例外。 关闭接口提供关闭流的通用方式:

type Closer interface {
        Close() error
}

对于Closer没有什么可说的,因为它很简单,但是我发现Close()函数总是返回一个错误,我的类型可以在需要时实现Closer。 Closer并不总是直接使用,但有时会与其他接口组合使用ReadCloserWriteCloserReadWriteCloser

Moving around within streams

流通常是从头到尾的连续字节流,但是还有一些例外。 例如,一个文件可以作为一个流来操作,但你也可以跳转到文件中的特定位置。 提供了Seeker接口,用于在流中跳转:

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

有三种方式跳跃:从当前位置移动,从一开始移动,从最后移动。 您可以使用whence参数指定移动模式。 offset参数指定要移动的字节数。 如果您在文件中使用固定长度的块或者您的文件包含偏移索引,偏移可能会很有用。 有时,这些数据存储在标题中,所以从头开始是有意义的,但有时这个数据是在预告片中指定的,所以你需要从最后移动。

Optimizing for Data Types

如果你需要的是一个字节或rune,那么读取和写入块可能很繁琐。 Go提供了一些接口,使这更容易。

Working with individual bytes

ByteReaderByteWriter接口提供了一个用于读取和写入单个字节的简单接口:

type ByteReader interface {
        ReadByte() (c byte, err error)
}
type ByteWriter interface {
        WriteByte(c byte) error
}

您会注意到没有长度的参数,因为长度将始终为0或1.如果一个字节未被读取或写入,则返回错误。 ByteScanner接口还提供用于处理缓冲字节读取器:

type ByteScanner interface {
        ByteReader
        UnreadByte() error
}

这允许您将先前读取的字节推回读取器,以便下次读取。 这在编写LL(1)解析器时特别有用,因为它允许您窥视下一个可用字节。

Working with individual runes

如果您正在解析Unicode数据,那么您需要使用rune而不是单个字节。 在这种情况下,会使用RuneReaderRuneScanner

type RuneReader interface {
        ReadRune() (r rune, size int, err error)
}
type RuneScanner interface {
        RuneReader
        UnreadRune() error
}

Conclusion

字节流对大多数Go程序至关重要。 它们是从网络连接到磁盘上的文件到用户从键盘输入的所有内容的界面。 io包为所有这些交互提供了基础。 我们研究了读取字节,写入字节,复制字节,最后研究了优化这些操作。 这些原语可能看起来很简单,但它们为所有数据密集型应用程序提供了基础。 请看看io包,并在应用程序中考虑它的接口。

参考链接

https://medium.com/go-walkthrough/go-walkthrough-io-package-8ac5e95a9fbd

Go 
comments powered by Disqus