请选择 进入手机版 | 继续访问电脑版
MSIPO技术圈 首页 IT技术 查看内容

Go语言IO模式

2023-07-13

Go语言IO模式

IO 操作是我们在编程中不可避免会遇到的,Go语言的 io 包中提供了相关的接口,定义了相应的规范,不同的数

据类型可以根据规范去实现相应的方法,提供更加丰富的功能。

本文主要介绍常见的 IO (输入和输出)模式,以及如何在应用程序中使用 Go I/O API。

IO 包官方文档:https://pkg.go.dev/io

1、IO包核心接口

1.1 Reader

io.Reader接口定义了 Read 方法,用于读取数据到字节数组中:

  • 入参:字节数组 p,会将数据读入到 p 中
  • 返回值:本次读取的字节数 n,以及遇到的错误 err
type Reader interface {
	Read(p []byte) (n int, err error)
}

1.2 Writer

io.Writer接口定义了 Write 方法,用于写数据到文件中

  • 入参:字节数组 p,会将 p 中的数据写入到文件中
  • 返回值:成功写入完成的字节数 n,以及遇到的错误 err
type Writer interface {
	Write(p []byte) (n int, err error)
}

1.3 Closer

io.Closer接口定义了 Close 方法,该方法用于关闭连接。

type Closer interface {
	Close() error
}
Se

第一次调用该方法后,再次调用该方法应该产生什么行为,该接口没有定义,依赖实现方法自定义。

1.4 Seeker

io.Seeker接口定义了 Seek 方法,该方法用于指定下次读取或者写入时的偏移量

  • 入参:计算新偏移量的起始值 whence, 基于whence的偏移量offset
  • 返回值:基于 whence 和 offset 计算后新的偏移量值,以及可能产生的错误
type Seeker interface {
	Seek(offset int64, whence int) (int64, error)
}

io包中定义了如下三种 whence:

const (
	SeekStart   = 0 // 基于文件开始位置
	SeekCurrent = 1 // 基于当前偏移量 
	SeekEnd     = 2 // 基于文件结束位置
)

2、IO包类型分类

2.1 基础类型

例如 Reader、Writer、Closer、ReaderAt、WriterAt、Seeker、ByteReader、ByteWriter、RuneReader、

StringWriter 等。

2.2 组合类型

在go语言中,可以利用接口的组合,来囊括其他接口中的方法,类似于定义了一个父接口,可以包含多个子接

口。如果一个 struct 实现了所有子接口的方法,也就相当于实现了父接口。小接口 + 接口组合的方式,很大程度

上增加了程序的灵活性,在我们自己业务开发过程中,可以借鉴这种做法。

使用的语法糖则是 Go 的匿名字段的用法。

针对上面四个最小粒度的接口,io包定义了如下几种组合接口:

// ReadWriter 是 Read 和 Write 方法的组合
type ReadWriter interface {
	Reader
	Writer
}

// ReadCloser 是 Read 和 Close 方法的组合
type ReadCloser interface {
	Reader
	Closer
}

// WriteCloser 是 Write 和 Close 方法的组合
type WriteCloser interface {
	Writer
	Closer
}

// ReadWriteCloser 是 Read、Write 和 Close 方法的组合
type ReadWriteCloser interface {
	Reader
	Writer
	Closer
}

// ReadSeeker 是 Read 和 Seek 方法的组合
type ReadSeeker interface {
	Reader
	Seeker
}

// WriteSeeker 是 Write 和 Seek 方法的组合
type WriteSeeker interface {
	Writer
	Seeker
}

// ReadWriteSeeker 是 Read、Write 和 Seek 方法的组合
type ReadWriteSeeker interface {
	Reader
	Writer
	Seeker
}

2.3 进阶类型

一般是在基础接口之上,增加了一些额外的实现。

例如 TeeReader、LimitReader、SectionReader、MultiReader、MultiWriter、PipeReader、PipeWriter 等。

3、IO包通用的函数

3.1 Cpoy

把一个 Reader 读出来,写到 Writer 里去,直到其中一方出错为止 (比如最常见的,读端出现 EOF )。

3.2 CopyN

这个和 Copy 一样,读 Reader,写 Writer,直到出错为止。但是 CopyN 比 Copy 多了一个结束条件:数据的拷

贝绝对不会超过 N 个;

3.3 CopyBuffer

这个也是个拷贝实现,和 Copy,CopyN 本质无差异。这个能让用户指定使用多大的 Buffer 内存,这个可以让用

户能根据实际情况优化性能,比如大文件拷贝的话,可以考虑使用大一点的 buffer,提高效率 (1G 的文件拷贝,

它也是分了无数次的读写完成的,比如用 1M 的内存 buffer,不停的搬运,搬运 1024 次,才算完)。

4、io.Reader及其进阶的应用

4.1 读取文件

读取文件是最常见的使用 io.reader 的方式之一,以下的代码示例演示了如何利用 io.reader 读取本地的文件数

据。

package main

import (
	"fmt"
	"io"
	"log"
	"os"
)

func readFromLocalFile(filePath string) {
	file, err := os.Open(filePath)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()
	buffer := make([]byte, 1024)
	for {
		n, err := file.Read(buffer)
		if err != nil && err != io.EOF {
			log.Fatal(err)
		}
		if n == 0 {
			break
		}
		fmt.Print(string(buffer[:n]))
	}
}

func main(){
	readFromLocalFile("./files/a.txt")
}
# 程序输出
a1
a2
a3
a4
a5

4.2 读取网络数据

另外一个常见的使用 io.reader 的场景就是读取网络数据。比如,我们可以通过 HttpClient 从一个 http 链接上面

读取数据。以下的代码演示了如何利用 io.reader 进行网络读取。

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func readFromWeb(url string) {
	resp, err := http.Get(url)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	buffer := make([]byte, 1024)
	for {
		n, err := resp.Body.Read(buffer)
		if err != nil && err != io.EOF {
			log.Fatal(err)
		}
		if n == 0 {
			break
		}
		fmt.Print(string(buffer[:n]))
	}
}

func main(){
	readFromWeb("http://www.baidu.com")
}

4.3 读取进程输出

有时候我们需要使用 Go启动一个进程,并且读取该进程的输出。这时候,我们可以使用 io.reader 接口去读取进

程的输出流。以下的代码演示了如何使用 io.reader 读取进程的输出流。

package main

import (
	"fmt"
	"io"
	"log"
	"os/exec"
)

func readFromCommand(cmd *exec.Cmd) {
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		log.Fatal(err)
	}
	if err := cmd.Start(); err != nil {
		log.Fatal(err)
	}
	buffer := make([]byte, 1024)
	for {
		n, err := stdout.Read(buffer)
		if err != nil && err != io.EOF {
			log.Fatal(err)
		}
		if n == 0 {
			break
		}
		fmt.Print(string(buffer[:n]))
	}
}

func main() {
	readFromCommand(exec.Command("ls"))
}

4.4 io.MultiReader

有时候我们需要多个 io.reader 进行串联使用,比如在网络爬虫、多文件拼接等情况中,就需要从多个来源读取数

据并将其合并成一个完整的数据流。这时候,可以使用 io.MultiReader。以下的代码示例演示了如何使用

io.MultiReader 去单独读取两个文件然后进行合并。

package main

import (
	"fmt"
	"io"
	"log"
	"os"
)

func readMultipleFiles(file1, file2 *os.File) {
	combinedReader := io.MultiReader(file1, file2)

	// create a buffer to store the read content
	buffer := make([]byte, 1024)

	for {
		n, err := combinedReader.Read(buffer)
		if err != nil && err != io.EOF {
			log.Fatal(err)
		}

		if n == 0 {
			break
		}
		fmt.Println(string(buffer[:n]))
	}
}

func main() {
	file1, err := os.Open("./files/a.txt")
	if err != nil {
		log.Fatal(err)
	}
	defer file1.Close()
	file2, err := os.Open("./files/b.txt")
	if err != nil {
		log.Fatal(err)
	}
	defer file2.Close()
	readMultipleFiles(file1, file2)
}
# 程序输出
a1
a2
a3
a4
a5
b1
b2
b3
b4
b5

4.5 io.TeeReader

在某些情况下,我们需要将输入流的数据同时输出到多个不同的 io.writer 以满足不同的需求,比如在调试某些应

用时需要将输入流数据同时输出到不同的日志。这个时候就可以使用 io.TeeReader。以下的代码演示了如何使用

io.TeeReader 把输入流的数据同时输出到标准输出和标准错误输出。

package main

import (
	"fmt"
	"io"
	"log"
	"os"
)

func printToStdAndErr(reader io.Reader) {
	teeReader := io.TeeReader(reader, os.Stderr)
	buffer := make([]byte, 1024)
	for {
		n, err := teeReader.Read(buffer)
		if err != nil && err != io.EOF {
			log.Fatal(err)
		}
		if n == 0 {
			break
		}
		fmt.Println(string(buffer[:n]))
	}
}

func main() {
	file1, err := os.Open("./files/a.txt")
	if err != nil {
		log.Fatal(err)
	}
	defer file1.Close()
	printToStdAndErr(file1)
}
# 输出
a1
a2
a3
a4
a5
a1
a2
a3
a4
a5

4.6 io.LimitedReader

有时候我们需要从一个 io.reader 中读取一定的数据量,这种需求可以通过 io.LimitedReader 完成。

io.LimitedReader 可以从某个数据流中按照指定的长度读取数据,读取长度上限达到之后,就停止读取数据。以

下的代码演示了如何在 Go 中使用 io.LimitedReader。

package main

import (
	"fmt"
	"io"
	"log"
	"os"
)

func readByteSlice(reader io.Reader) {
	limitedReader := &io.LimitedReader{R: reader, N: 10}
	buffer := make([]byte, 1024)
	for {
		n, err := limitedReader.Read(buffer)
		if err != nil && err != io.EOF {
			log.Fatal(err)
		}
		if n == 0 {
			break
		}
		fmt.Print(string(buffer[:n]))
	}
}

func main() {
	file1, err := os.Open("./files/a.txt")
	if err != nil {
		log.Fatal(err)
	}
	defer file1.Close()
	readByteSlice(file1)
}
# 程序输出
a1
a2
a3

5、IO包的一些使用

5.1 写到标准输出

package main

import "fmt"

func main() {
	// Hello World
	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 接口。

5.2 写到定制的writer

学会了如何向 os.stdout 写入数据,让我们创建一个自定义 writer 并在那里存储一些信息。我们可以通过初始化

一个空的缓冲区并向其写入内容来实现:

package main

import (
	"bytes"
	"fmt"
)

func main() {
	// 空buffer,实现了io.Writer
	var b bytes.Buffer
	// 这里需要传入地址&
	fmt.Fprintln(&b, "Hello World")
	// Hello World
	fmt.Println(b.String())
}

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

5.3 同时写给多个writer

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

package main

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

func main() {
	// 两个空buffers
	var buf1, buf2 bytes.Buffer
	// 创建MultiWriter
	mw := io.MultiWriter(&buf1, &buf2)
	// 写入数据到MultiWriter
	fmt.Fprintln(mw, "Hello World")
	// Hello World
	fmt.Println(buf1.String())
	// Hello World
	fmt.Println(buf2.String())
}

在上面的片段中,我们创建了两个空的缓冲区,叫做 buf1 和 buf2。我们将这些 writer 传递给一个叫做

io.MultiWriter 的方法,以获得一个组合写入器。消息 Hello World 将在内部同时被写入 buf1 和 buf2 中。

5.4 创建一个简单的reader

Go提供了 io.Reader interface 来实现一个 IO reader。reader 不进行读取,但为他人提供数据。它是一个临时的

信息仓库,有许多方法,如 WriteTo, Seek 等。

让我们看看如何从一个字符串创建一个简单的 reader:

package main

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

func main() {
	// 创建一个NewReader
	r := strings.NewReader("Hello World")
	// 从Reader读取所有数据
	// func ReadAll(r Reader) ([]byte, error) {}
	b, err := io.ReadAll(r)
	if err != nil {
		panic(err)
	}
	// Hello World
	fmt.Println(string(b))
}

这段代码使用 strings.NewReader 方法创建了一个新的 reader。该 reader 拥有 io.Reader 接口的所有方法。我

们使用 io.ReadAll 从 reader 中读取内容,它返回一个字节切片。最后,我们将其打印到控制台。

注意:os.Stdin 是一个常用的 reader,用于收集标准输入。

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

与 io.MultiWriter 类似,我们也可以创建一个 io.MultiReader 来从多个 reader 那里读取数据。数据会按照传递给

io.MultiReader 的读者的顺序依次收集。这就像一次从不同的数据存储中收集信息,但要按照给定的顺序。

package main

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

func main() {
	// 创建两个Reader
	buf1 := strings.NewReader("Hello buf1\n")
	buf2 := strings.NewReader("Hello buf2")
	// 创建一个MultiReader
	mr := io.MultiReader(buf1, buf2)
	// 从MultiReader读取数据
	b, err := io.ReadAll(mr)
	if err != nil {
		panic(err)
	}
	// Hello buf1
	// Hello buf2
	fmt.Println(string(b))
}

代码很简单,创建两个名为 buf1 和 buf2 的 reader,并试图从它们中创建一个 MultiReader 。我们可以使用

io.Readall 来读取 MultiReader 的内容,就像一个普通的 reader。

现在我们已经了解了 reader 和 writer,让我们看看从 reader 复制数据到 writer 的例子。接下来我们看到复制数

据的技术。

注意:不要对大的缓冲区使用 io.ReadAll,因为它们会消耗尽内存。

5.6 将数据从 reader 复制到 writer

再次对定义的理解:

reader:我可以从谁那里复制数据

writer:我可以把数据写给谁?我可以向谁写数据

这些定义使我们很容易理解,我们需要从一个 reader 加载数据,并将其转储到一个 writer (如os.Stdout或一个缓

冲区)。这个复制过程可以通过两种方式发生:

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

5.6.1 reader 将数据推送给 writer

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

API。

package main

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

func main() {
	r := strings.NewReader("Hello World")
	var b bytes.Buffer
	r.WriteTo(&b)
	// Hello World
	fmt.Println(b.String())
}

在代码中,我们使用 WriteTo 方法从一个名为 r 的 reader 那里把内容写进写 writer b。在下面的例子中,我们看

到一个 writer 如何主动地从一个 reader 那里获取信息。

5.6.2 writer 从 reader 中拉出数据

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

package main

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

func main() {
	r := strings.NewReader("Hello World")
	var b bytes.Buffer
	b.ReadFrom(r)
	// Hello World
	fmt.Println(b.String())
}

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

第三种变体,它比较干净。

5.6.3 使用 io.Copy

io.Copy 是一个实用的函数,它允许人们将数据从一个 reader 移到一个 writer。

让我们看看它是如何工作的:

package main

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

func main() {
	r := strings.NewReader("Hello World")
	var b bytes.Buffer
	_, err := io.Copy(&b, r)
	if err != nil {
		panic(err)
	}
	// Hello World
	fmt.Println(b.String())
}

io.Copy 的第一个参数是 Writer (目标),第二个参数是 Reader (源),用于复制数据。

每当有人将数据写入 writer 时,你希望有信息可以被相应的 reader 读取,这就出现了管道的概念。

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

io.Pipe 返回一个 reader 和一个 writer,向 writer 中写入数据会自动允许程序从 reader 中消费数据,它就像一个

Unix的管道。

你必须把写入逻辑放到一个单独的 goroutine 中,因为管道会阻塞 writer,直到从 reader 中读取数据,而且

reader 也会被阻塞,直到 writer 被关闭。

package main

import (
	"fmt"
	"io"
)

func main() {
	// func Pipe() (*PipeReader, *PipeWriter) {}
	pr, pw := io.Pipe()
	go func() {
		defer pw.Close()
		fmt.Fprintln(pw, "Hello World")
	}()
	b, err := io.ReadAll(pr)
	if err != nil {
		panic(err)
	}
	// Hello World
	fmt.Println(string(b))
}

该代码创建了一个管道 reader 和管道 writer。我们启动一个程序,将一些信息写入管道 writer 并关闭它。我们使

用 io.ReadAll 方法从管道 reader 中读取数据。如果你不启动一个单独的程序(写或读,都是一个操作),程序将陷

入死锁。

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

假设我们正在构建一个CLI应用程序。作为这个过程的一部分,我们创建一个函数产生的标准输出(到控制台),并

将相同的信息赋值到一个变量中。我们怎样才能做到这一点呢?我们可以使用上面讨论的技术来创建一个解决方

案。

package main

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

func foo(w *io.PipeWriter) {
	defer w.Close()
	fmt.Fprintln(w, "Hello World")
}

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

相关阅读

手机版|MSIPO技术圈 皖ICP备19022944号-2

Copyright © 2024, msipo.com

返回顶部