zoukankan      html  css  js  c++  java
  • [转]Go 中 io 包的使用方法

    原文:https://segmentfault.com/a/1190000015591319

    ---------------------------

    前言

    在 Go 中,输入和输出操作是使用原语实现的,这些原语将数据模拟成可读的或可写的字节流。
    为此,Go 的 io 包提供了 io.Reader 和 io.Writer 接口,分别用于数据的输入和输出,如图:

    图片描述

    Go 官方提供了一些 API,支持对内存结构,文件,网络连接等资源进行操作
    本文重点介绍如何实现标准库中 io.Reader 和 io.Writer 两个接口,来完成流式传输数据。

    io.Reader

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

    对于要用作读取器的类型,它必须实现 io.Reader 接口的唯一一个方法 Read(p []byte)
    换句话说,只要实现了 Read(p []byte) ,那它就是一个读取器。

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

    Read() 方法有两个返回值,一个是读取到的字节数,一个是发生错误时的错误。
    同时,如果资源内容已全部读取完毕,应该返回 io.EOF 错误。

    使用 Reader

    利用 Reader 可以很容易地进行流式数据传输。Reader 方法内部是被循环调用的,每次迭代,它会从数据源读取一块数据放入缓冲区 p (即 Read 的参数 p)中,直到返回 io.EOF 错误时停止。

    下面是一个简单的例子,通过 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]))
        }
    }
    输出打印的内容:
    4 Clea
    4 r is
    4  bet
    4 ter 
    4 than
    4  cle
    3 ver
    EOF: 0 

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

    自己实现一个 Reader

    上一节是使用标准库中的 io.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()
    }
    输出打印的内容:
    HelloItsamwhereisthesun

    组合多个 Reader,目的是重用和屏蔽下层实现的复杂度

    标准库已经实现了许多 Reader。
    使用一个 Reader 作为另一个 Reader 的实现是一种常见的用法。
    这样做可以让一个 Reader 重用另一个 Reader 的逻辑,下面展示通过更新 alphaReader 以接受 io.Reader 作为其来源。

    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() {
        //  使用实现了标准库 io.Reader 接口的 strings.Reader 作为实现
        reader := newAlphaReader(strings.NewReader("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()
    }

    这样做的另一个优点是 alphaReader 能够从任何 Reader 实现中读取。
    例如,以下代码展示了 alphaReader 如何与 os.File 结合以过滤掉文件中的非字母字符:

    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()
    }

    io.Writer

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

    图片描述

    对于要用作编写器的类型,必须实现 io.Writer 接口的唯一一个方法 Write(p []byte)
    同样,只要实现了 Write(p []byte) ,那它就是一个编写器。

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

    Write() 方法有两个返回值,一个是写入到目标资源的字节数,一个是发生错误时的错误。

    使用 Writer

    标准库提供了许多已经实现了 io.Writer 的类型。
    下面是一个简单的例子,它使用 bytes.Buffer 类型作为 io.Writer 将数据写入内存缓冲区。

    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())
    }
    输出打印的内容:
    Channels orchestrate mutexes serializeCgo is not GoErrors are valuesDon't panic

    自己实现一个 Writer

    下面我们来实现一个名为 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 包里其他有用的类型和方法

    如前所述,Go标准库附带了许多有用的功能和类型,让我们可以轻松使用流式io。

    os.File

    类型 os.File 表示本地系统上的文件。它实现了 io.Reader 和 io.Writer ,因此可以在任何 io 上下文中使用。
    例如,下面的例子展示如何将连续的字符串切片直接写入文件:

    func main() {
        proverbs := []string{
            "Channels orchestrate mutexes serialize
    ",
            "Cgo is not Go
    ",
            "Errors are values
    ",
            "Don't panic
    ",
        }
        file, err := os.Create("./proverbs.txt")
        if err != nil {
            fmt.Println(err)
            os.Exit(1)
        }
        defer file.Close()
    
        for _, p := range proverbs {
            // file 类型实现了 io.Writer
            n, err := file.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("file write done")
    }

    同时,io.File 也可以用作读取器来从本地文件系统读取文件的内容。
    例如,下面的例子展示了如何读取文件并打印其内容:

    func main() {
        file, err := os.Open("./proverbs.txt")
        if err != nil {
            fmt.Println(err)
            os.Exit(1)
        }
        defer file.Close()
    
        p := make([]byte, 4)
        for {
            n, err := file.Read(p)
            if err == io.EOF {
                break
            }
            fmt.Print(string(p[:n]))
        }
    }

    标准输入、输出和错误

    os 包有三个可用变量 os.Stdout ,os.Stdin 和 os.Stderr ,它们的类型为 *os.File,分别代表 系统标准输入系统标准输出 和 系统标准错误 的文件句柄。
    例如,下面的代码直接打印到标准输出:

    func main() {
        proverbs := []string{
            "Channels orchestrate mutexes serialize
    ",
            "Cgo is not Go
    ",
            "Errors are values
    ",
            "Don't panic
    ",
        }
    
        for _, p := range proverbs {
            // 因为 os.Stdout 也实现了 io.Writer
            n, err := os.Stdout.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)
            }
        }
    }

    io.Copy()

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

    func main() {
        proverbs := new(bytes.Buffer)
        proverbs.WriteString("Channels orchestrate mutexes serialize
    ")
        proverbs.WriteString("Cgo is not Go
    ")
        proverbs.WriteString("Errors are values
    ")
        proverbs.WriteString("Don't panic
    ")
    
        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)
        }
    }

    io.WriteString()

    此函数让我们方便地将字符串类型写入一个 Writer:

    func main() {
        file, err := os.Create("./magic_msg.txt")
        if err != nil {
            fmt.Println(err)
            os.Exit(1)
        }
        defer file.Close()
        if _, err := io.WriteString(file, "Go is fun!"); err != nil {
            fmt.Println(err)
            os.Exit(1)
        }
    }

    使用管道的 Writer 和 Reader

    类型 io.PipeWriter 和 io.PipeReader 在内存管道中模拟 io 操作。
    数据被写入管道的一端,并使用单独的 goroutine 在管道的另一端读取。
    下面使用 io.Pipe() 创建管道的 reader 和 writer,然后将数据从 proverbs 缓冲区复制到io.Stdout :

    func main() {
        proverbs := new(bytes.Buffer)
        proverbs.WriteString("Channels orchestrate mutexes serialize
    ")
        proverbs.WriteString("Cgo is not Go
    ")
        proverbs.WriteString("Errors are values
    ")
        proverbs.WriteString("Don't panic
    ")
    
        piper, pipew := io.Pipe()
    
        // 将 proverbs 写入 pipew 这一端
        go func() {
            defer pipew.Close()
            io.Copy(pipew, proverbs)
        }()
    
        // 从另一端 piper 中读取数据并拷贝到标准输出
        io.Copy(os.Stdout, piper)
        piper.Close()
    }

    缓冲区 io

    标准库中 bufio 包支持 缓冲区 io 操作,可以轻松处理文本内容。
    例如,以下程序逐行读取文件的内容,并以值 ' ' 分隔:

    func main() {
        file, err := os.Open("./planets.txt")
        if err != nil {
            fmt.Println(err)
            os.Exit(1)
        }
        defer file.Close()
        reader := bufio.NewReader(file)
    
        for {
            line, err := reader.ReadString('
    ')
            if err != nil {
                if err == io.EOF {
                    break
                } else {
                    fmt.Println(err)
                    os.Exit(1)
                }
            }
            fmt.Print(line)
        }
    
    }

    ioutil

    io 包下面的一个子包 utilio 封装了一些非常方便的功能
    例如,下面使用函数 ReadFile 将文件内容加载到 []byte 中。

    package main
    
    import (
      "io/ioutil"
       ...
    )
    
    func main() {
        bytes, err := ioutil.ReadFile("./planets.txt")
        if err != nil {
            fmt.Println(err)
            os.Exit(1)
        }
        fmt.Printf("%s", bytes)
    }

    总结

    本文介绍了如何使用 io.Reader 和 io.Writer 接口在程序中实现流式IO。
    阅读本文后,您应该能够了解如何使用 io 包来实现 流式传输IO数据的程序。
    其中有一些例子,展示了如何创建自己的类型,并实现io.Reader 和 io.Writer 。

    这是一个简单介绍性质的文章,没有扩展开来讲。
    例如,我们没有深入文件IO,缓冲IO,网络IO或格式化IO(保存用于将来的写入)。
    我希望这篇文章可以让你了解 Go语言中 流式IO 的常见用法是什么。

    谢谢!

  • 相关阅读:
    316 Remove Duplicate Letters 去除重复字母
    315 Count of Smaller Numbers After Self 计算右侧小于当前元素的个数
    313 Super Ugly Number 超级丑数
    312 Burst Balloons 戳气球
    309 Best Time to Buy and Sell Stock with Cooldown 买股票的最佳时间含冷冻期
    Java 类成员的初始化顺序
    JavaScript 全局
    HTML字符实体
    Java中的toString()方法
    JavaScript 弹窗
  • 原文地址:https://www.cnblogs.com/oxspirt/p/15338001.html
Copyright © 2011-2022 走看看