Go 中 io.Pipe 的用法

「这是我参与21月更文挑战的第21天,活动详情查看:2021最后一次更文挑战

在这篇文章中,我想展示 Go 标准库 io 的一个函数 Pipe

func Pipe() (*PipeReader, *PipeWriter)
复制代码

Pipe creates a synchronous in-memory pipe. It can be used to connect code expecting an io.Reader with code expecting an io.Writer.

Reads and Writes on the pipe are matched one to one except when multiple Reads are needed to consume a single Write. That is, each Write to the PipeWriter blocks until it has satisfied one or more Reads from the PipeReader that fully consume the written data. The data is copied directly from the Write to the corresponding Read (or Reads); there is no internal buffering.

It is safe to call Read and Write in parallel with each other or with Close. Parallel calls to Read and parallel calls to Write are also safe: the individual calls will be gated sequentially.

根据文档,io.Pipe 创建了一个同步内存管道,可用于将需要 io.Reader 的代码与需要 io.Writer 的代码连接起来。

调用时,io.Pipe() 返回一个 PipeReader 和一个 PipeWriter。它们是连接的(管道),因此写入 PipeWriter 的所有内容都可以从 PipeReader 读取。

下面将用三个栗子来展示 io.Pipe 的用法以及它与 I/O 结合带来的特性。

Example1: JSON to HTTP Request

当我们将一些数据编码为 JSON,并希望通过 http.Post 将其发送到 Web 端时,通常,我们会先将内存数据通过 json.Encoder 进行编码,然后再讲结果提供给 http.Post 作为输入。但JSON encoder 需要一个 io.Writer,而 http Post 方法需要一个 io.Reader 作为输入,所以我们不能只是将它们连接在一起。我们需要在他们中间通过 []bytes 进行转换。我们也可以使用 io.Pipe 来做这件事。

pr, pw := io.Pipe()

go func() {
    // close the writer, so the reader knows there's no more data
    defer pw.Close()

    // write json data to the PipeReader through the PipeWriter
    if err := json.NewEncoder(pw).Encode(&PayLoad{Content: "Hello Pipe!"}); err != nil {
        log.Fatal(err)
    }
}()

// JSON from the PipeWriter lands in the PipeReader
// ...and we send it off...
if _, err := http.Post("http://example.com", "application/json", pr); err != nil {
    log.Fatal(err)
}
复制代码

首先,我们将结构体 PayLoad 编码为 JSON,并将数据写入通过调用 io.Pipe 创建的 PipeWriter。之后,我们创建一个 http POST 请求,该请求从 PipeReader 获取其数据。 PipeReader 被写入 PipeWriter 的数据填满。

这里需要注意的是,我们必须异步编码以防止死锁,因为如果我们没有读取器,我们将在没有读取器的情况下进行写入。

这个实际例子很好地展示了 io.Pipe 的多功能性。它确实激励 gophers 使用 io.Reader 和 io.Writer 构建组件,而不必担心它们一起使用。

Split up Data with TeeReader

我发现了另一种将 io.PipeTeeReader 相结合的使用方法,他可以产生类似数据流镜像的效果。国外网友的一个用法是:将视频文件转码为另一种格式并上传时,同时还上传了原始文件,通过 Pipe 与 TeeReader 结合,在最小的开销并且完全并行的情况下完成。

简化后的代码如下:

pr, pw := io.Pipe()

// we need to wait for everything to be done
wg := sync.WaitGroup{}
wg.Add(2)

// we get some file as input
f, err := os.Open("./fruit.txt")
if err != nil {
    log.Fatal(err)
}

// TeeReader gets the data from the file and also writes it to the PipeWriter
tr := io.TeeReader(f, pw) 

go func() {
    defer wg.Done()
    defer pw.Close()

    // get data from the TeeReader, which feeds the PipeReader through the PipeWriter
    _, err := http.Post("https://example.com", "text/html", tr)
    if err != nil {
        log.Fatal(err)
    }
}()

go func() {
    defer wg.Done()
    // read from the PipeReader to stdout
    if _, err := io.Copy(os.Stdout, pr); err != nil {
        log.Fatal(err)
    }
}()

wg.Wait()
复制代码

我们有某种输入 io.Reader,在这种情况下是一个文件,并创建一个 TeeReader,它返回一个 Reader,该 Reader 将写入你提供的 Writer,它从你提供的 Reader 读取的所有内容。

现在我们启动两个 goroutine,一个只是将数据打印到 stdout,另一个将数据发送到 HTTP 端点。TeeReader 使用 io.Pipe 拆分给定的输入。当使用 TeeReader 时,PipeReader 也会接收到这些相同的字节。

Example 3: Piping the output of Shell commands

第三种方式是将 io.Pipe 与 os.Exec 结合在一起。基本上,它执行大多数 CI 服务(如 Jenkins 或 Travis CI)中的任务运行器所做的工作,即执行一些 shell 命令并在某些网站上显示其输出。

简化后的代码如下:

pr, pw := io.Pipe()
defer pw.Close()

// tell the command to write to our pipe
cmd := exec.Command("cat", "fruit.txt")
cmd.Stdout = pw

go func() {
    defer pr.Close()
    // copy the data written to the PipeReader via the cmd to stdout
    if _, err := io.Copy(os.Stdout, pr); err != nil {
        log.Fatal(err)
    }
}()

// run the command, which writes all output to the PipeWriter
// which then ends up in the PipeReader
if err := cmd.Run(); err != nil {
    log.Fatal(err)
}
复制代码

首先,我们定义命令 —— cat 一个名为 fruit.txt 的文件,它会在标准输出上文件的内容。然后,我们将命令的标准输出设置为我们的 PipeWriter

所以我们将命令的输出重定向到我们的管道,就像以前一样,这将使我们可以在另一个点通过我们的 PipeReader 读取它。

猜你喜欢

转载自juejin.im/post/7032933119992791053