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

简介: 解释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())
}


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


创建一个简单的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))
}


代码很简单,创建两个名为foobar的 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.MultiWriteros.Stdout 和自定义缓冲区 b
  • foo(作为一个协程)将把一个字符串写到管道 writer 中
  • io.Copy将把内容从管道 reader 复制到 MultiWriter 中。
  • os.Stdout将接收输出以及你的自定义缓冲区b
  • 内容现在可以在b中使用


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

相关文章
|
3月前
|
缓存 NoSQL Go
通过 SingleFlight 模式学习 Go 并发编程
通过 SingleFlight 模式学习 Go 并发编程
|
9天前
|
安全 Go 调度
探索Go语言的并发模式:协程与通道的协同作用
Go语言以其并发能力闻名于世,而协程(goroutine)和通道(channel)是实现并发的两大利器。本文将深入了解Go语言中协程的轻量级特性,探讨如何利用通道进行协程间的安全通信,并通过实际案例演示如何将这两者结合起来,构建高效且可靠的并发系统。
|
9天前
|
安全 Go 开发者
破译Go语言中的并发模式:从入门到精通
在这篇技术性文章中,我们将跳过常规的摘要模式,直接带你进入Go语言的并发世界。你将不会看到枯燥的介绍,而是一段代码的旅程,从Go的并发基础构建块(goroutine和channel)开始,到高级模式的实践应用,我们共同探索如何高效地使用Go来处理并发任务。准备好,让Go带你飞。
|
4月前
|
设计模式 Go
Go语言设计模式:使用Option模式简化类的初始化
在Go语言中,面对构造函数参数过多导致的复杂性问题,可以采用Option模式。Option模式通过函数选项提供灵活的配置,增强了构造函数的可读性和可扩展性。以`Foo`为例,通过定义如`WithName`、`WithAge`、`WithDB`等设置器函数,调用者可以选择性地传递所需参数,避免了记忆参数顺序和类型。这种模式提升了代码的维护性和灵活性,特别是在处理多配置场景时。
69 8
|
3月前
|
缓存 算法 Go
|
3月前
|
存储 Unix 测试技术
解释Go中常见的I/O模式
解释Go中常见的I/O模式
|
3月前
|
设计模式 Go
|
6月前
|
前端开发 Go
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
【5月更文挑战第3天】Go语言通过goroutines和channels实现异步编程,虽无内置Future/Promise,但可借助其特性模拟。本文探讨了如何使用channel实现Future模式,提供了异步获取URL内容长度的示例,并警示了Channel泄漏、错误处理和并发控制等常见问题。为避免这些问题,建议显式关闭channel、使用context.Context、并发控制机制及有效传播错误。理解并应用这些技巧能提升Go语言异步编程的效率和健壮性。
298 5
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
|
7天前
|
存储 前端开发 Go
Go语言中的数组
在 Go 语言中,数组是一种固定长度的、相同类型元素的序列。数组声明时长度已确定,不可改变,支持多种初始化方式,如使用 `var` 关键字、短变量声明、省略号 `...` 推断长度等。数组内存布局连续,可通过索引高效访问。遍历数组常用 `for` 循环和 `range` 关键字。