GO标准库巡礼-IO

声明:该系列文章是基于对@benbjohnson的《Go Walkthrough》、go官方文档、《Go语言标准库》的学习汇总而成

io包为I/O提供了基本的接口,由于这些接口都以不同实现封装了低级操作,因此,除非另行通知,不应假定它们是线程安全的

读取bytes

Reader接口

在读取bytes上最常用到的是Reader接口,标准库的每一个模块几乎都实现了该接口

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

我们可以看到,Read函数接收byte切片p而不是每次read返回一个新的byte切片,其原因在于如果每次返回新的切片,那么GC将负担过重。

注意的是,当 Read ⽅法返回错误时,不代表没有读取到任何数据。

Reader接口的问题在于,首先,如果出现了EOF,Read函数会常规性地返回一个io.EOF错误,这可能让新手感到困惑;其次,buffer没有被许诺填满,当你传入一个长度为8的buffer的时候,你可能会得到0-8的数据,处理这种部分获取的办法是复杂且易出错的,不过庆幸的是,GO提供了一些辅助函数协助解决该问题

让reader读取更具保证

如果我们确定我们需要读取指定字节的buf,我们可以使用ReadFull

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

该函数确保在函数返回前buf被填满,如果一个字节都没有填满那么返回io.EOF错误,如果填满了部分字节,那么返回io.ErrUnexpectedEOF

一个简单的demo如下

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

另一个比较少用但是也有意义的函数是ReadAtLeast,该函数会读取额外的字节(如果可能),但是保证至少读取指定的长度。该函数较少使用,但是可以用于我们希望减少调用Read次数且愿意缓存多余数据的时候

func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)
整合多个reader

我们有些时候可能希望能够合并多个reader为一个。我们可以用MultiReader来实现这个目的。

func MultiReader(readers ...Reader) Reader

该函数会逐一按顺序读取readers切片。比方说MultiReader(a,b,c),会先读取a直到EOF再读取b。如果所有的输入都返回EOF,那么返回EOF。如果发生了非EOF错误,那么返回该错误

举个例子,可能我们会希望发送一个request,其中request header来自于内存,而request body来自于硬盘。很多人会尝试复制request header和文件到一个buffer,但是这样很慢而且浪费内存,我们可以用MultiReader

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

在使用reader的时候,一个比较常见的困扰是无法重新读取reader内容。比方说应用没有正确的解析request body而你却因为parser已经消费了该reader而无法debug

对此我们可以使用TeeReader,该函数会包裹原先的Reader r,每次对该新的Reader调用读取的时候都会向Writer w写入相应数据,Writter可以是buffer或者是stderr

func TeeReader(r Reader, w Writer) Reader

我们可以用TeeReader解决上述的问题

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

但是我们需要注意限制request body长度来避免OOM

限制数据流长度

数据流是无界的,这使得它们在特定场景下可能导致内存或者硬盘不足的问题,一个常见的例子是文件上传。我们当然可以自行限制大小,但是显然过于乏味,我们可以使用LimitReader函数

func LimitReader(r Reader, n int64) Reader

尴尬的是LimitReader不会告诉你是否底层reader长度超过n个字节,当读取n个字节之后,会返回io.EOF。一个比较trick的办法是设置读取n+1个字节,然后检查是否读取了超过n个字节

写入bytes

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

一般来说,写入字节比起读取字节更加简单,因为读取字节允许不足读取,而不足写入会返回错误

写复制

有些时候我们希望可以同时写入多个流,比方说log或者stderr。我们可以用MultiWriter函数实现这个目的

func MultiWriter(writers ...Writer) Writer

注意的是虽然有着类似于MultiReader的名字,MultiWriter和MultiReader差异较大,MultiReader用于将多个reader合并为一个,而MultiWriter则试图复制单次写到多个流

一个例子如下图

type MyService struct {
        LogOutput io.Writer
}
...
var buf bytes.Buffer
var s MyService
s.LogOutput = io.MultiWriter(&buf, os.Stderr)
优化字符串写入

在标准库中的很多writer都实现了WriteString方法用于提升写入string的性能(通过在转换string到[]byte过程中不分配额外空间实现),我们可以使用io.WriteString函数来充分利用这一点。

该函数会首先检查writer是否实现了WriteString并且如果可能的话就使用它,否则,就会回滚到原始版本-复制字符串到一个[]byte,然后调用Write函数

复制bytes

连接reader和writer

现在我们有reader用于读取数据,有writer用于写入数据,一个自然的想法就是连接reader和writer——从reader复制数据并写入到writer。Copy函数为我们实现了该功能

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

该函数使用一个32KB的缓存来从src读取并写入到dst中,过程中如果发生了任何非EOF的错误,复制会立刻停止并且返回该错误

该函数的一个缺点是,无法控制复制的数量,这对于一些受限制的writer(如文件大小)不太有利,我们可以用CopyN来指定写入的准确数目

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

另一个缺点在于,每次调用Copy都会创建分配一个32KB的缓冲,如果我们要进行大量的Copy操作,我们可以转而使用CopyBuffer。不过一般情况下,Copy不会成为性能瓶颈

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

Copy本身使用了buffer来连接reader和writer,如果想要进一步提升性能,我们可以实现WriteTo和ReadFrom函数,当Copy函数检测发现reader和writer实现了这俩接口,就会避免使用buffer

type WriterTo interface {
        WriteTo(w Writer) (n int64, err error)
}
type ReaderFrom interface {
        ReadFrom(r Reader) (n int64, err error)
}
适配reader和writer

有些时候你可能发现函数要求一个reader,而你只有一个writer。或许你需要动态地向HTTP REQUEST写入数据,但是http.NewRequest()只接受一个reader

你可以利用Pipe函数转换一个Writer

func Pipe() (*PipeReader, *PipeWriter)

Pipe函数提供一个新的reader和writer,所有对writer的写入都会写入到reader中

这个函数较少使用,但是exec.Cmd使用该函数来实现管道

关闭流

close本身很简单,但是一个可用的建议是,让自己的close方法返回一个error,从而实现了Closer接口(在某些特定的时间可能有用)

type Closer interface {
        Close() error
}

值得注意的是,初学者常常写成下图所示的方式,一旦文件没有找到,err!=nil成立退出,Close就会触发panic(因为file是nil),正确做法是在检查错误之后注册defer

file, err := os.Open("studygolang.txt")
defer file.Close()
if err != nil {
...
} 

在流中移动

通常来说,我们面对的是一个持续的从头到尾的流但是偶尔也有例外。比方说一个文件,可以执行的像一个流,但是也允许你跳转到特定的位置。

Seeker接口可以用来实现跳转

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

有三种方式来实现跳转,一个是从头开始,一个是从尾开始,一个是从当前位置开始,我们用whence来指定一种模式(io.SEEK_SET,io.SEEK_CUR,io.SEEK_END)。用offset来指定偏移量

如果我们只使用文件的固定一块,那么seeking就是管用的

针对数据类型的优化

如果我们只是想要一个byte,那么使用上述的方式就显得无聊了,go提供了一些接口来让这更加容易

获取单个byte

go提供了ByteWriter和ByteReader接口

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

你可能发现了,ReadByte和WriteByte都没有提供长度,因为对于ByteReader和ByteWriter而言,每次要么获取了一个byte,要么就是返回错误

除此之外,go还提供了ByteScanner接口,该接口允许你读取了一个byte之后,将其推回以允许重复读取(相当于peek),注意的是不能连续两次调用UnreadByte(相当于要求上一次操作是Read)

type ByteScanner interface {
        ByteReader
        UnreadByte() error
}
获取单个rune

如果我们在处理unicode,我们可以使用RuneReader和RuneScanner接口

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

发布了31 篇原创文章 · 获赞 32 · 访问量 736

猜你喜欢

转载自blog.csdn.net/a348752377/article/details/104448526