在Go中使用io.Pipe的例子

354 阅读5分钟

关于io.Readerio.Writer 界面的艺术作品,人们已经写了很多,也说了很多。简单而强大--就像Go本身一样。

在这篇文章中,我想展示Go标准库中另一个我认为既简单又强大的部分 -[io.Pipe]。

pr, pw := io.Pipe()

根据文档,io.Pipe 创建了一个同步的内存管道,它可以用来连接期待io.Reader 的代码和期待io.Writer 的代码。

在调用时,io.Pipe() 会返回一个PipeReader 和一个PipeWriter 。它们被连接起来(因此称为管道),因此写到PipeWriter 的所有内容都可以从PipeReader 读取。

下面的三个例子显示了io.Pipe 的使用情况,它的多功能性以及它使我们能够做的思考和组成I/O的方式。

让我们开始吧!

例1:JSON到HTTP请求

这是人们在谈到io.Pipe 时经常看到的例子。我们将一些数据编码为JSON ,并希望通过http.Post ,将其发送到一个网络终端。不幸的是(或者说幸运的是),JSON编码器需要一个io.Writer ,而http请求方法需要一个io.Reader 作为输入,所以我们不能直接把它们连接起来。

当然,我们可以创建中间的[]byte 表示,但这既不节省内存,也不是特别优雅。这就是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 的多功能性。它真正激励了地鼠们使用io.Readerio.Writer 构建组件,而不必担心它们被一起使用。

例2:用TeeReader拆分数据

在[@rodaine]关于异步拆分一个io.Reader 的伟大[博文]中,我发现了另一种非常酷的使用io.PipeTeeReader (读作:T-Reader)的方式。

解决方案#4中,他描述了使用一个视频文件并同时将其转码为另一种格式并上传的用例,同时也上传了原始文件。所有这一切都以最小的开销和完全并行的方式进行。

基于这个解决方案,我试图通过下面的例子来捕捉它的要点。

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那里。

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

很酷吧,哈?

例子3:对Shell命令的输出进行管道处理

我最近偶然发现了这个gist,它以一种很好的方式将io.Pipeos.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 的文件,这将只是在stdout 吐出文件的内容。然后,这一点很重要,我们将命令的stdout 设置为我们的PipeWriter

因此,我们将Command 的输出重定向到我们的管道,像以前一样,这将使我们有可能在另一个点通过我们的PipeReader 读取它。在这个相当复杂的案例中,这个点只是一个goroutine,我们将cat的结果转储到stdout(无论如何它都会这样做),但我认为很容易想象在这里做一些有趣的事情,比如将命令的结果导出到某个地方,或者像这个gist中看到的那样将其冲到一个网页上,在那里我们需要一个io.Writer 作为输入。

总结

我希望这些例子能让你相信,通过使用io.Pipe 和漂亮的抽象(期待io.Readerio.Writer )所带来的许多机会。io.Pipe 不仅能实现基于最佳实践的组件的无缝组合,而且在使用TeeReader 时也相当灵活,它指出了以可读和可扩展的方式在定制的I/O处理管道中使用io.Pipe 的巨大可能性。

当然,这篇文章只涉及到这个话题的表面,因为它没有处理这个方法固有的问题,也没有处理错误,但我计划在未来通过一两篇关于这些和一些更高级的话题的文章来弥补这个问题。

祝你玩得开心!:)