解释Go中常见的I/O模式

在这篇文章中,我想介绍一下常见的I/O(输入和输出)模式。我想在这篇文章中用浅显的语言澄清这些概念,这样人们就可以在他们的应用程序中使用优雅的Go I/O API。
让我们看看开发人员在日常生活中需要的常见用例。

写到标准输出

每个Go编程教程教给你的最常见的例子:

package main

import "fmt"

func main() {
	fmt.Println("Hello World")
}

但是,没有人告诉你上述代码是这个例子的简化版:

package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Fprintln(os.Stdout, "Hello World")
}

这里发生了什么?我们有一个额外的包导入,叫做os,并且使用了fmt包中一个叫做Fprintln的方法。Fprintln方法接收一个io.Writer类型和一个要写入的字符串。os.Stdout满足io.Writer接口。
这个例子很好,可以扩展到除os.Stdout以外的任何写程序。

写到定制的writer

你学会了如何向os.stdout写入数据。让我们创建一个自定义writer并在那里存储一些信息。我们可以通过初始化一个空的缓冲区并向其写入内容来实现:

package main

import (
	"bytes"
	"fmt"
)

func main() {
	// Empty buffer (implements io.Writer)
	var b bytes.Buffer
	fmt.Fprintln(&b, "Hello World") // Don't forget &

	// Optional: Check the contents stored
	fmt.Println(b.String()) // Prints `Hello World`
}

这个片段实例化了一个空缓冲区,并将其作为Fprintln方法的第一个参数(io.Writer)。

同时写给多个writer

有时,人们需要将一个字符串写入多个writer中。我们可以使用io包中的MultiWriter方法轻松做到这一点。

package main

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

func main() {
	// Two empty buffers
	var foo, bar bytes.Buffer

	// Create a multi writer
	mw := io.MultiWriter(&foo, &bar)

	// Write message into multi writer
	fmt.Fprintln(mw, "Hello World")

	// Optional: verfiy data stored in buffers
	fmt.Println(foo.String())
	fmt.Println(bar.String())
}

在上面的片段中,我们创建了两个空的缓冲区,叫做foo和bar。我们将这些writer传递给一个叫做io.MultiWriter的方法,以获得一个组合写入器。消息Hello World将在内部同时被写入foo和bar中。

创建一个简单的reader

Go提供了io.Readerinterface来实现一个I/O reader。reader 不进行读取,但为他人提供数据。它是一个临时的信息仓库,有许多方法,如WriteTo, Seek等。
让我们看看如何从一个字符串创建一个简单的 reader:

package main

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

func main() {
	// Create a new reader (Readonly)
	r := strings.NewReader("Hello World")

	// Read all content from reader
	b, err := io.ReadAll(r)
	if err != nil {
		panic(err)
	}

	// Optional: verify data
	fmt.Println(string(b))
}

这段代码使用strings.NewReader方法创建了一个新的 reader。该 reader 拥有io.Reader接口的所有方法。我们使用io.ReadAll从 reader 中读取内容,它返回一个字节切片。最后,我们将其打印到控制台。
注意:os.Stdin是一个常用的 reader,用于收集标准输入。

一次性从多个 reader 上读取数据

与io.MultiWriter类似,我们也可以创建一个io.MultiReader来从多个 reader 那里读取数据。数据会按照传递给io.MultiReader的读者的顺序依次收集。这就像一次从不同的数据存储中收集信息,但要按照给定的顺序。

package main

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

func main() {
	// Create two readers
	foo := strings.NewReader("Hello Foo\n")
	bar := strings.NewReader("Hello Bar")

	// Create a multi reader
	mr := io.MultiReader(foo, bar)

	// Read data from multi reader
	b, err := io.ReadAll(mr)

	if err != nil {
		panic(err)
	}

	// Optional: Verify data
	fmt.Println(string(b))
}

代码很简单,创建两个名为foo和bar的 reader,并试图从它们中创建一个MultiReader。我们可以使用io.Readall来读取MultiReader的内容,就像一个普通的 reader。
现在我们已经了解了 reader 和 writer,让我们看看从 reader 复制数据到 writer 的例子。接下来我们看到复制数据的技术。
注意:不要对大的缓冲区使用io.ReadAll,因为它们会消耗尽内存。

将数据从 reader 复制到 writer

再次对定义的理解:
reader:我可以从谁那里复制数据
writer:我可以把数据写给谁?我可以向谁写数据
这些定义使我们很容易理解,我们需要从一个 reader(字符串阅读器)加载数据,并将其转储到一个 writer(如os.Stdout或一个缓冲区)。这个复制过程可以通过两种方式发生:

  • reader 将数据推送给 writer
  • writer 从 reader 中拉出数据

reader 将数据推送给 writer

这一部分解释了第一种拷贝的变化,即 reader 将数据推送到 writer 那里。它使用reader.WriteTo(writer)的API。

package main

import (
	"bytes"
	"fmt"
	"strings"
)

func main() {
	// Create a reader
	r := strings.NewReader("Hello World")

	// Create a writer
	var b bytes.Buffer

	// Push data
	r.WriteTo(&b) // Don't forget &

	// Optional: verify data
	fmt.Println(b.String())
}

在代码中,我们使用WriteTo方法从一个名为r的 reader 那里把内容写进写 writer b。在下面的例子中,我们看到一个 writer 如何主动地从一个 reader 那里获取信息。

writer 从 reader 中拉出数据

方法writer.ReadFrom(reader)被一个 writer 用来从一个给定的 reader 中提取数据。让我们看一个例子:

package main

import (
	"bytes"
	"fmt"
	"strings"
)

func main() {
	// Create a reader
	r := strings.NewReader("Hello World")

	// Create a writer
	var b bytes.Buffer

	// Pull data
	b.ReadFrom(r)

	// Optional: verify data
	fmt.Println(b.String())
}

该代码看起来与前面的例子相似。无论你是作为 reader 还是 writer,你都可以选择变体1或2来复制数据。现在是第三种变体,它比较干净。

使用 io.Copy

io.Copy是一个实用的函数,它允许人们将数据从一个 reader 移到一个 writer。
让我们看看它是如何工作的:

package main

import (
	"bytes"
	"fmt"
	"io"
	"strings"
)

func main() {
	// Create a reader
	r := strings.NewReader("Hello World")

	// Create a writer
	var b bytes.Buffer

	// Copy data
	_, err := io.Copy(&b, r) // Don't forget &

	if err != nil {
		panic(err)
	}
	// Optional: verify data
	fmt.Println(b.String())
}

io.Copy的第一个参数是Writer(目标),第二个参数是Reader(源),用于复制数据。
每当有人将数据写入 writer 时,你希望有信息可以被相应的 reader 读取。这就出现了管道的概念。

用io.Pipe创建一个数据管道

io.Pipe返回一个 reader 和一个 writer,向 writer 中写入数据会自动允许程序从 reader 中消费数据。它就像一个Unix的管道。
你必须把写入逻辑放到一个单独的goroutine中,因为管道会阻塞 writer,直到从 reader 中读取数据,而且 reader 也会被阻塞,直到 writer 被关闭。

package main

import (
	"fmt"
	"io"
)

func main() {
	pr, pw := io.Pipe()

	// Writing data to writer should be in a go-routine
	// because pipe is synchronous.
	go func() {
		defer pw.Close() // Important! To notify writing is done
		fmt.Fprintln(pw, "Hello World")
	}()

	// Code is blocked until someone writes to writer and closes it
	b, err := io.ReadAll(pr)

	if err != nil {
		panic(err)
	}
	// Optional: verify data
	fmt.Println(string(b))
}

该代码创建了一个管道reader和管道writer。我们启动一个程序,将一些信息写入管道writer并关闭它。我们使用io.ReadAll方法从管道reader中读取数据。如果你不启动一个单独的程序(写或读,都是一个操作),程序将陷入死锁。
我们在下一节讨论管道的一个更实际的使用案例。

用io.Pipe、io.Copy和io.MultiWriter捕捉函数的stdout到一个变量中。

假设我们正在构建一个CLI应用程序。作为这个过程的一部分,我们创建一个函数产生的标准输出(到控制台),并将相同的信息赋值到一个变量中。我们怎样才能做到这一点呢?我们可以使用上面讨论的技术来创建一个解决方案。

package main

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

// Your function
func foo(w *io.PipeWriter) {
	defer w.Close()
	// Write a message to pipe writer
	fmt.Fprintln(w, "Hello World")
}

func main() {
	// Create a pipe
	pr, pw := io.Pipe()

	// Pass writer to function
	go foo(pw)

	// Variable to get standard output of function
	var b bytes.Buffer

	// Create a multi writer that is a combination of
	// os.Stdout and our variable byte buffer
	mw := io.MultiWriter(os.Stdout, &b)
	// Copies reader content to standard output
	_, err := io.Copy(mw, pr)

	if err != nil {
		panic(err)
	}

	// Optional: verify data
	fmt.Println(b.String())
}

上述程序是这样工作的:

  • 创建一个管道,给出一个 reader 和 writer。如果你向管道 writer 写了什么,Go会把它复制到管道的 reader 那里。
  • 创建一个 io.MultiWriter 与 os.Stdout 和自定义缓冲区 b
  • foo(作为一个协程)将把一个字符串写到管道 writer 中
  • io.Copy将把内容从管道 reader 复制到 MultiWriter 中。
  • os.Stdout将接收输出以及你的自定义缓冲区b
  • 内容现在可以在b中使用

使用Go中的iopackage,我们可以像我们在这些模式中看到的那样操作数据。

你可能感兴趣的:(golang,开发语言,后端)