golang reader writer接口

文章最后更新时间为:2021年03月17日 11:04:38

golang 的笔记,内容多来源于互联网,放这里方便查找。

简介

Writer和Reader是两个抽象的接口,其定义如下

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

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

很多方法的接收参数都是io.Writer接口,当然还有io.Reader接口,这就是面向接口的编程,我们不用关注具体实现,只用关注这个接口可以做什么事情,如果我们换成输出到文件里,那么也很容易,只用把os.File类型作为参数即可。任何实现了该接口的类型,都可以作为参数。

reader

io.Reader接口的规则更多。

  • Read最多读取len(p)字节的数据,并保存到p。
  • 返回读取的字节数以及任何发生的错误信息
  • n要满足0 <= n <= len(p)
  • n<len(p)时,表示读取的数据不足以填满p,这时方法会立即返回,而不是等待更多的数据
  • 读取过程中遇到错误,会返回读取的字节数n以及相应的错误err
  • 在底层输入流结束时,方法会返回n>0的字节,但是err可能时EOF,也可以是nil
  • 在第6种(上面)情况下,再次调用read方法的时候,肯定会返回0,EOF
  • 调用Read方法时,如果n>0时,优先处理处理读入的数据,然后再处理错误err,EOF也要这样处理
  • Read方法不鼓励返回n=0并且err=nil的情况

io.Reader 表示一个读取器,它将数据从某个资源读取到传输缓冲区。在缓冲区中,数据可以被流式传输和使用。

通过string.NewReader(string) 创建一个字符串读取器,然后流式地按字节读取:

func main() {
    reader := strings.NewReader("Clear is better than clever")
    p := make([]byte, 4)

    for {
        n, err := reader.Read(p)
        if err != nil{
            if err == io.EOF {
                fmt.Println("EOF:", n)
                break
            }
            fmt.Println(err)
            os.Exit(1)
        }
        fmt.Println(n, string(p[:n]))
    }
}

结果:可以看到,最后一次返回的 n 值有可能小于缓冲区大小。

$ go run test.go
4 Clea
4 r is
4  bet
4 ter 
4 than
4  cle
3 ver
EOF: 0

自定义一个reader

type alphaReader struct {
    // 资源
    src string
    // 当前读取到的位置 
    cur int
}

// 创建一个实例
func newAlphaReader(src string) *alphaReader {
    return &alphaReader{src: src}
}

// 过滤函数
func alpha(r byte) byte {
    if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
        return r
    }
    return 0
}

// Read 方法
func (a *alphaReader) Read(p []byte) (int, error) {
    // 当前位置 >= 字符串长度 说明已经读取到结尾 返回 EOF
    if a.cur >= len(a.src) {
        return 0, io.EOF
    }

    // x 是剩余未读取的长度
    x := len(a.src) - a.cur
    n, bound := 0, 0
    if x >= len(p) {
        // 剩余长度超过缓冲区大小,说明本次可完全填满缓冲区
        bound = len(p)
    } else if x < len(p) {
        // 剩余长度小于缓冲区大小,使用剩余长度输出,缓冲区不补满
        bound = x
    }

    buf := make([]byte, bound)
    for n < bound {
        // 每次读取一个字节,执行过滤函数
        if char := alpha(a.src[a.cur]); char != 0 {
            buf[n] = char
        }
        n++
        a.cur++
    }
    // 将处理后得到的 buf 内容复制到 p 中
    copy(p, buf)
    return n, nil
}

func main() {
    reader := newAlphaReader("Hello! It's 9am, where is the sun?")
    p := make([]byte, 4)
    for {
        n, err := reader.Read(p)
        if err == io.EOF {
            break
        }
        fmt.Print(string(p[:n]))
    }
    fmt.Println()
}

结果

$ go run test.go
HelloItsamwhereisthesun

还可以组合多个Reader,目的是重用和屏蔽下层实现的复杂度。以下代码展示了 alphaReader 如何与 os.File 结合以过滤掉文件中的非字母字符:

package main

import (
    "fmt"
    "io"
    "os"
    "strings"
)

type alphaReader struct {
    // alphaReader 里组合了标准库的 io.Reader
    reader io.Reader
}

func newAlphaReader(reader io.Reader) *alphaReader {
    return &alphaReader{reader: reader}
}

func alpha(r byte) byte {
    if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
        return r
    }
    return 0
}

func (a *alphaReader) Read(p []byte) (int, error) {
    // 这行代码调用的就是 io.Reader
    n, err := a.reader.Read(p)
    if err != nil {
        return n, err
    }
    buf := make([]byte, n)
    for i := 0; i < n; i++ {
        if char := alpha(p[i]); char != 0 {
            buf[i] = char
        }
    }

    copy(p, buf)
    return n, nil
}


func main() {
    // file 也实现了 io.Reader
    file, err := os.Open("./alpha_reader3.go")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer file.Close()

    // 任何实现了 io.Reader 的类型都可以传入 newAlphaReader
    // 至于具体如何读取文件,那是标准库已经实现了的,我们不用再做一遍,达到了重用的目的
    reader := newAlphaReader(file)
    p := make([]byte, 4)
    for {
        n, err := reader.Read(p)
        if err == io.EOF {
            break
        }
        fmt.Print(string(p[:n]))
    }
    fmt.Println()
}

writer

io.Writer 表示一个编写器,它从缓冲区读取数据,并将数据写入目标资源。

要想实现一个io.Writer接口,就要遵循这些规则。

  • write方法向底层数据流写入len(p)字节的数据,这些数据来自于切片p
  • 返回被写入的字节数n,0 <= n <= len(p)
  • 如果n<len(p), 则必须返回一些非nil的err
  • 如果中途出现问题,也要返回非nil的err
  • Write方法绝对不能修改切片p以及里面的数据

下面是一个简单的例子,它使用 bytes.Buffer 类型作为 io.Writer 将数据写入内存缓冲区。

package main

import (
    "bytes"
    "fmt"
    "os"
)

func main() {
    proverbs := []string{
        "Channels orchestrate mutexes serialize",
        "Cgo is not Go",
        "Errors are values",
        "Don't panic",
    }
    var writer bytes.Buffer

    for _, p := range proverbs {
        n, err := writer.Write([]byte(p))
        if err != nil {
            fmt.Println(err)
            os.Exit(1)
        }
        if n != len(p) {
            fmt.Println("failed to write data")
            os.Exit(1)
        }
    }

    fmt.Println(writer.String())
}

下面我们来实现一个名为 chanWriter 的自定义 io.Writer ,它将其内容作为字节序列写入 channel 。

type chanWriter struct {
    // ch 实际上就是目标资源
    ch chan byte
}

func newChanWriter() *chanWriter {
    return &chanWriter{make(chan byte, 1024)}
}

func (w *chanWriter) Chan() <-chan byte {
    return w.ch
}

func (w *chanWriter) Write(p []byte) (int, error) {
    n := 0
    // 遍历输入数据,按字节写入目标资源
    for _, b := range p {
        w.ch <- b
        n++
    }
    return n, nil
}

func (w *chanWriter) Close() error {
    close(w.ch)
    return nil
}

func main() {
    writer := newChanWriter()
    go func() {
        defer writer.Close()
        writer.Write([]byte("Stream "))
        writer.Write([]byte("me!"))
    }()
    for c := range writer.Chan() {
        fmt.Printf("%c", c)
    }
    fmt.Println()
}

要使用这个 Writer,只需在函数 main() 中调用 writer.Write()(在单独的goroutine中)。
因为 chanWriter 还实现了接口 io.Closer ,所以调用方法 writer.Close() 来正确地关闭channel,以避免发生泄漏和死锁。

io.Reader和io.ReadCloser和io.ReadSeeker

  • ReadCloser 接口封装了Reader和Closer接口,所以它具有基本的Read和Close方法。
  • ReadSeeker 接口封装了基本的Read和Seek方法,Seek方法可以设定下一次读写的位移。
type Reader interface{
   Read( p []byte) (n int, err error)
}

type ReadCloser interface{
   Reader
   Closer
}



type ReadSeeker interface{
     Reader
     Seeker
}

为什么需要ReadCloser这种变体?比如http.Response.Body

如果没有关闭resp.Body,golang将继续保持连接以重用它,所以将会造成资源泄漏。

在golang使用client.Do方法之后,go将运行goroutinereadLoop方法作为步骤之一

如何将 Reader 转化成 ReadCloser

go语言包 ioutil 提供了NopCloser方法可以将 io.Reader 封装为 io.ReadCloser

func NopCloser (r io.Reader) io.ReadCloser

源码:https://golang.org/src/io/ioutil/ioutil.go?s=3440:3481#L105

如何将Reader 或者 ReadCloser 转化为 ReadSeeker

这里有详细的原因和方法

http://stackoverflow.com/questions/37718191/how-do-i-go-from-io-readcloser-to-io-readseeker

原因: ReadSeeker 封装了Seek()方法,这个方法要求资源的任何位置都能被定位,例如存储在磁盘里文件,你可以随时读取文件的任意位置。而response.Body 是通过TCP连接从网络中读取数据,这些数据没有被存储,并且数据发送者不会再次发送数据给你,因此 response.Body 没有实现 io.Seeker 方法。

方法:基于以上分析,对于一些像 response.Body类型的 io.ReadCloser,将它转化为 ReadSeeker的方法就是先将 io.ReadCloser 全部读取到内存中,利用 ioutil.ReadAll() 方法,然后利用 bytes.NewReader() 方法就可以从[]byte中获得 io.ReadSeeker

它的缺点就是所有的内容都需要存储在内存中,这样会造成内存损耗。

io.reader复制

io.reader 被读取一次后就没了,但有些情况下我们需要复制。

最麻烦的方式,将数据读出来,再用读出来的数据,创建两个Reader。

你可以先把 io.Reader 类型的对象读到 []bytes 类型的对象里面,通过 []bytes 类型对象创建多个 io.Reader 比如:

b, err :=  ioutil.ReadAll(io.Reader)
reader1 := bytes.NewReader(b)
reader2 := bytes.NewReader(b)

io.Copy

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

io.Copy() 可以轻松地将数据从一个 Reader 拷贝到另一个 Writer。
它抽象出 for 循环模式(我们上面已经实现了)并正确处理 io.EOF 和 字节计数。

package main

import (
    "bytes"
    "fmt"
    "io"
    "os"
)

func main() {
    proverbs := new(bytes.Buffer)
    proverbs.WriteString("Channels orchestrate mutexes serialize\n")
    proverbs.WriteString("Cgo is not Go\n")
    proverbs.WriteString("Errors are values\n")
    proverbs.WriteString("Don't panic\n")

    file, err := os.Create("./proverbs.txt")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer file.Close()

    // io.Copy 完成了从 proverbs 读取数据并写入 file 的流程
    if _, err := io.Copy(file, proverbs); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println("file created")
}

同时也可以使用 io.Copy() 函数重写从文件读取并打印到标准输出的先前程序,如下所示:

func main() {
    file, err := os.Open("./proverbs.txt")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer file.Close()

    if _, err := io.Copy(os.Stdout, file); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

bufio

bufio 包实现了缓存IO。它包装了 io.Reader 和 io.Writer 对象,创建了另外的Reader和Writer对象,它们也实现了 io.Reader 和 io.Writer 接口,不过它们是有缓存的。该包同时为文本I/O提供了一些便利操作。

type Reader struct {
    buf          []byte        // 缓存
    rd           io.Reader    // 底层的io.Reader
    // r:从buf中读走的字节(偏移);w:buf中填充内容的偏移;
    // w - r 是buf中可被读的长度(缓存数据的大小),也是Buffered()方法的返回值
    r, w         int
    err          error        // 读过程中遇到的错误
    lastByte     int        // 最后一次读到的字节(ReadByte/UnreadByte)
    lastRuneSize int        // 最后一次读到的Rune的大小 (ReadRune/UnreadRune)
}

type Writer struct {
    err error        // 写过程中遇到的错误
    buf []byte        // 缓存
    n   int            // 当前缓存中的字节数
    wr  io.Writer    // 底层的 io.Writer 对象
}

bufio.Reader

  • ReadSlice (返回的是指针,不是一个拷贝)
  • ReadBytes
  • ReadString
  • ReadLine

一般情况下用ReadBytes、ReadString就好

package main

import (
    "bufio"
    "fmt"
    "strings"
)

func main() {
    reader := bufio.NewReader(strings.NewReader("http://studygolang.com. \nIt is the home of gophers"))
    line, _ := reader.ReadBytes('\n')
    fmt.Printf("the line:%s\n", line)
    // 这里可以换上任意的 bufio 的 Read/Write 操作
    n, _ := reader.ReadBytes('\n')
    fmt.Printf("the line:%s\n", line)
    fmt.Println(string(n))
}

bufio.Scanner

Scanner是为了按照某个界定符号分割Reader。

按行读取文件,简单好用

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    file, err := os.Create("scanner.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    file.WriteString("http://studygolang.com.\nIt is the home of gophers.\nIf you are studying golang, welcome you!")
    // 将文件 offset 设置到文件开头
    file.Seek(0, os.SEEK_SET)
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
}

改写Split,实现统计一个Reader有多少个单词。

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)
// ScanWords is a split function for a Scanner that returns each
// space-separated word of text, with surrounding spaces deleted. It will
// never return an empty string. The definition of space is set by
// unicode.IsSpace.
func main() {
    const input = "This is The Golang Standard Library.\nWelcome you!"
    scanner := bufio.NewScanner(strings.NewReader(input))
    scanner.Split(bufio.ScanWords)
    count := 0
    for scanner.Scan() {
        count++
    }
    if err := scanner.Err(); err != nil {
        fmt.Fprintln(os.Stderr, "reading input:", err)
    }
    fmt.Println(count)
}

Writer

实现了以下方法

// NewWriterSize 将 wr 封装成一个带缓存的 bufio.Writer 对象,
// 缓存大小由 size 指定(如果小于 4096 则会被设置为 4096)。
// 如果 wr 的基类型就是有足够缓存的 bufio.Writer 类型,则直接将
// wr 转换为基类型返回。
func NewWriterSize(wr io.Writer, size int) *Writer
​
// NewWriter 相当于 NewWriterSize(wr, 4096)
func NewWriter(wr io.Writer) *Writer
​
// WriteString 功能同 Write,只不过写入的是字符串
func (b *Writer) WriteString(s string) (int, error)
​
// WriteRune 向 b 写入 r 的 UTF-8 编码,返回 r 的编码长度。
func (b *Writer) WriteRune(r rune) (size int, err error)
​
// Flush 将缓存中的数据提交到底层的 io.Writer 中
func (b *Writer) Flush() error
​
// Available 返回缓存中未使用的空间的长度
func (b *Writer) Available() int
​
// Buffered 返回缓存中未提交的数据的长度
func (b *Writer) Buffered() int
​
// Reset 将 b 的底层 Writer 重新指定为 w,同时丢弃缓存中的所有数据,复位
// 所有标记和错误信息。相当于创建了一个新的 bufio.Writer。
func (b *Writer) Reset(w io.Writer)

一个Reader多次读取

io.TeeReader

读取的时候,同时输出到os.Stdout中

package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "os"
    "strings"
)

func main() {
    var r io.Reader = strings.NewReader("some io.Reader stream to be read\n")

    r = io.TeeReader(r, os.Stdout)

    // Everything read from r will be copied to stdout.
    b, _ := ioutil.ReadAll(r)
    fmt.Println(string(b))
}

一个Writer多次写入

io.MultiWriter

func test() {
    file, err := os.Create("tmp.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    writer := io.MultiWriter(file, os.Stdout)
    writer.Write([]byte("Hello"))
}

并发对一个steam进行读写

可以参考: https://github.com/djherbis/stream

参考

1 + 7 =
快来做第一个评论的人吧~