在这篇文章中,我想介绍一下常见的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.Reader
interface来实现一个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中的io
package,我们可以像我们在这些模式中看到的那样操作数据。