0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何使用io.Reader和io.Writer接口在程序中实现流式IO

马哥Linux运维 来源:思否开发者社区 作者:ronniesong 2021-07-29 16:46 次阅读

Go 语言标准库 io 包内有一些常用接口和方法,本文配合图片和实际代码,详细介绍了 io 包。

前言

在 Go 中,输入和输出操作是使用原语实现的,这些原语将数据模拟成可读的或可写的字节流。

为此,Go 的 io 包提供了 io.Reader 和 io.Writer 接口,分别用于数据的输入和输出

Go 官方提供了一些 API,支持对内存结构,文件,网络连接等资源进行操作

本文重点介绍如何实现标准库中 io.Reader 和 io.Writer 两个接口,来完成流式传输数据。

io.Reader

io.Reader 表示一个读取器,它将数据从某个资源读取到传输缓冲区。在缓冲区中,数据可以被流式传输和使用。

对于要用作读取器的类型,它必须实现 io.Reader 接口的唯一一个方法 Read(p []byte)。

换句话说,只要实现了 Read(p []byte) ,那它就是一个读取器。

type Reader interface {

Read(p []byte) (n int, err error)

}

Read() 方法有两个返回值,一个是读取到的字节数,一个是发生错误时的错误。

同时,如果资源内容已全部读取完毕,应该返回 io.EOF 错误。

使用 Reader

利用 Reader 可以很容易地进行流式数据传输。Reader 方法内部是被循环调用的,每次迭代,它会从数据源读取一块数据放入缓冲区 p (即 Read 的参数 p)中,直到返回 io.EOF 错误时停止。

下面是一个简单的例子,通过 string.NewReader(string) 创建一个字符串读取器,然后流式地按字节读取:

func main() {

reader := strings.NewReader(“Clear is better than clever”)

p := make([]byte, 4)

for {

n, err := reader.Read(p)

if err != nil{

if err == io.EOF {

fmt.Println(“EOF:”, n)

break

}

fmt.Println(err)

os.Exit(1)

}

fmt.Println(n, string(p[:n]))

}

}

输出打印的内容:

4 Clea

4 r is

4 bet

4 ter

4 than

4 cle

3 ver

EOF: 0

可以看到,最后一次返回的 n 值有可能小于缓冲区大小。

自己实现一个 Reader

上一节是使用标准库中的 io.Reader 读取器实现的。

现在,让我们看看如何自己实现一个。它的功能是从流中过滤掉非字母字符。

type alphaReader struct {

// 资源

src string

// 当前读取到的位置

cur int

}

// 创建一个实例func newAlphaReader(src string) *alphaReader {

return &alphaReader{src: src}

}

// 过滤函数func alpha(r byte) byte {

if (r 》= ‘A’ && r 《= ‘Z’) || (r 》= ‘a’ && r 《= ‘z’) {

return r

}

return 0

}

// Read 方法func (a *alphaReader) Read(p []byte) (int, error) {

// 当前位置 》= 字符串长度 说明已经读取到结尾 返回 EOF

if a.cur 》= len(a.src) {

return 0, io.EOF

}

// x 是剩余未读取的长度

x := len(a.src) - a.cur

n, bound := 0, 0

if x 》= len(p) {

// 剩余长度超过缓冲区大小,说明本次可完全填满缓冲区

bound = len(p)

} else if x 《 len(p) {

// 剩余长度小于缓冲区大小,使用剩余长度输出,缓冲区不补满

bound = x

}

buf := make([]byte, bound)

for n 《 bound {

// 每次读取一个字节,执行过滤函数

if char := alpha(a.src[a.cur]); char != 0 {

buf[n] = char

}

n++

a.cur++

}

// 将处理后得到的 buf 内容复制到 p 中

copy(p, buf)

return n, nil

}

func main() {

reader := newAlphaReader(“Hello! It‘s 9am, where is the sun?”)

p := make([]byte, 4)

for {

n, err := reader.Read(p)

if err == io.EOF {

break

}

fmt.Print(string(p[:n]))

}

fmt.Println()

}

输出打印的内容:

HelloItsamwhereisthesun

组合多个 Reader,目的是重用和屏蔽下层实现的复杂度

标准库已经实现了许多 Reader。

使用一个 Reader 作为另一个 Reader 的实现是一种常见的用法。

这样做可以让一个 Reader 重用另一个 Reader 的逻辑,下面展示通过更新 alphaReader 以接受 io.Reader 作为其来源。

type alphaReader struct {

// alphaReader 里组合了标准库的 io.Reader

reader io.Reader

}

func newAlphaReader(reader io.Reader) *alphaReader {

return &alphaReader{reader: reader}

}

func alpha(r byte) byte {

if (r 》= ’A‘ && r 《= ’Z‘) || (r 》= ’a‘ && r 《= ’z‘) {

return r

}

return 0

}

func (a *alphaReader) Read(p []byte) (int, error) {

// 这行代码调用的就是 io.Reader

n, err := a.reader.Read(p)

if err != nil {

return n, err

}

buf := make([]byte, n)

for i := 0; i 《 n; i++ {

if char := alpha(p[i]); char != 0 {

buf[i] = char

}

}

copy(p, buf)

return n, nil

}

func main() {

// 使用实现了标准库 io.Reader 接口的 strings.Reader 作为实现

reader := newAlphaReader(strings.NewReader(“Hello! It’s 9am, where is the sun?”))

p := make([]byte, 4)

for {

n, err := reader.Read(p)

if err == io.EOF {

break

}

fmt.Print(string(p[:n]))

}

fmt.Println()

}

这样做的另一个优点是 alphaReader 能够从任何 Reader 实现中读取。

例如,以下代码展示了 alphaReader 如何与 os.File 结合以过滤掉文件中的非字母字符:

func main() {

// file 也实现了 io.Reader

file, err := os.Open(“。/alpha_reader3.go”)

if err != nil {

fmt.Println(err)

os.Exit(1)

}

defer file.Close()

// 任何实现了 io.Reader 的类型都可以传入 newAlphaReader

// 至于具体如何读取文件,那是标准库已经实现了的,我们不用再做一遍,达到了重用的目的

reader := newAlphaReader(file)

p := make([]byte, 4)

for {

n, err := reader.Read(p)

if err == io.EOF {

break

}

fmt.Print(string(p[:n]))

}

fmt.Println()

}

io.Writer

io.Writer 表示一个编写器,它从缓冲区读取数据,并将数据写入目标资源。

对于要用作编写器的类型,必须实现 io.Writer 接口的唯一一个方法 Write(p []byte)

同样,只要实现了 Write(p []byte) ,那它就是一个编写器。

type Writer interface {

Write(p []byte) (n int, err error)

}

Write() 方法有两个返回值,一个是写入到目标资源的字节数,一个是发生错误时的错误。

使用 Writer

标准库提供了许多已经实现了 io.Writer 的类型。

下面是一个简单的例子,它使用 bytes.Buffer 类型作为 io.Writer 将数据写入内存缓冲区。

func main() {

proverbs := []string{

“Channels orchestrate mutexes serialize”,

“Cgo is not Go”,

“Errors are values”,

“Don‘t panic”,

}

var writer bytes.Buffer

for _, p := range proverbs {

n, err := writer.Write([]byte(p))

if err != nil {

fmt.Println(err)

os.Exit(1)

}

if n != len(p) {

fmt.Println(“failed to write data”)

os.Exit(1)

}

}

fmt.Println(writer.String())

}

输出打印的内容:

Channels orchestrate mutexes serializeCgo is not GoErrors are valuesDon’t panic

自己实现一个 Writer

下面我们来实现一个名为 chanWriter 的自定义 io.Writer ,它将其内容作为字节序列写入 channel 。

type chanWriter struct {

// ch 实际上就是目标资源

ch chan byte

}

func newChanWriter() *chanWriter {

return &chanWriter{make(chan byte, 1024)}

}

func (w *chanWriter) Chan() 《-chan byte {

return w.ch

}

func (w *chanWriter) Write(p []byte) (int, error) {

n := 0

// 遍历输入数据,按字节写入目标资源

for _, b := range p {

w.ch 《- b

n++

}

return n, nil

}

func (w *chanWriter) Close() error {

close(w.ch)

return nil

}

func main() {

writer := newChanWriter()

go func() {

defer writer.Close()

writer.Write([]byte(“Stream ”))

writer.Write([]byte(“me!”))

}()

for c := range writer.Chan() {

fmt.Printf(“%c”, c)

}

fmt.Println()

}

要使用这个 Writer,只需在函数 main() 中调用 writer.Write()(在单独的 goroutine 中)。

因为 chanWriter 还实现了接口 io.Closer ,所以调用方法 writer.Close() 来正确地关闭 channel,以避免发生泄漏和死锁。

io 包里其他有用的类型和方法

如前所述,Go 标准库附带了许多有用的功能和类型,让我们可以轻松使用流式 io。

os.File

类型 os.File 表示本地系统上的文件。它实现了 io.Reader 和 io.Writer ,因此可以在任何 io 上下文中使用。

例如,下面的例子展示如何将连续的字符串切片直接写入文件:

func main() {

proverbs := []string{

“Channels orchestrate mutexes serialize

”,

“Cgo is not Go

”,

“Errors are values

”,

“Don‘t panic

”,

}

file, err := os.Create(“。/proverbs.txt”)

if err != nil {

fmt.Println(err)

os.Exit(1)

}

defer file.Close()

for _, p := range proverbs {

// file 类型实现了 io.Writer

n, err := file.Write([]byte(p))

if err != nil {

fmt.Println(err)

os.Exit(1)

}

if n != len(p) {

fmt.Println(“failed to write data”)

os.Exit(1)

}

}

fmt.Println(“file write done”)

}

同时,io.File 也可以用作读取器来从本地文件系统读取文件的内容。

例如,下面的例子展示了如何读取文件并打印其内容:

func main() {

file, err := os.Open(“。/proverbs.txt”)

if err != nil {

fmt.Println(err)

os.Exit(1)

}

defer file.Close()

p := make([]byte, 4)

for {

n, err := file.Read(p)

if err == io.EOF {

break

}

fmt.Print(string(p[:n]))

}

}

标准输入、输出和错误

os 包有三个可用变量 os.Stdout ,os.Stdin 和 os.Stderr ,它们的类型为 *os.File,分别代表 系统标准输入,系统标准输出 和 系统标准错误 的文件句柄。

例如,下面的代码直接打印到标准输出:

func main() {

proverbs := []string{

“Channels orchestrate mutexes serialize

”,

“Cgo is not Go

”,

“Errors are values

”,

“Don’t panic

”,

}

for _, p := range proverbs {

// 因为 os.Stdout 也实现了 io.Writer

n, err := os.Stdout.Write([]byte(p))

if err != nil {

fmt.Println(err)

os.Exit(1)

}

if n != len(p) {

fmt.Println(“failed to write data”)

os.Exit(1)

}

}

}

io.Copy()

io.Copy() 可以轻松地将数据从一个 Reader 拷贝到另一个 Writer。

它抽象出 for 循环模式(我们上面已经实现了)并正确处理 io.EOF 和 字节计数。

下面是我们之前实现的简化版本:

func main() {

proverbs := new(bytes.Buffer)

proverbs.WriteString(“Channels orchestrate mutexes serialize

”)

proverbs.WriteString(“Cgo is not Go

”)

proverbs.WriteString(“Errors are values

”)

proverbs.WriteString(“Don‘t panic

”)

file, err := os.Create(“。/proverbs.txt”)

if err != nil {

fmt.Println(err)

os.Exit(1)

}

defer file.Close()

// io.Copy 完成了从 proverbs 读取数据并写入 file 的流程

if _, err := io.Copy(file, proverbs); err != nil {

fmt.Println(err)

os.Exit(1)

}

fmt.Println(“file created”)

}

那么,我们也可以使用 io.Copy() 函数重写从文件读取并打印到标准输出的先前程序,如下所示:

func main() {

file, err := os.Open(“。/proverbs.txt”)

if err != nil {

fmt.Println(err)

os.Exit(1)

}

defer file.Close()

if _, err := io.Copy(os.Stdout, file); err != nil {

fmt.Println(err)

os.Exit(1)

}

}

io.WriteString()

此函数让我们方便地将字符串类型写入一个 Writer:

func main() {

file, err := os.Create(“。/magic_msg.txt”)

if err != nil {

fmt.Println(err)

os.Exit(1)

}

defer file.Close()

if _, err := io.WriteString(file, “Go is fun!”); err != nil {

fmt.Println(err)

os.Exit(1)

}

}

使用管道的 Writer 和 Reader

类型 io.PipeWriter 和 io.PipeReader 在内存管道中模拟 io 操作。

数据被写入管道的一端,并使用单独的 goroutine 在管道的另一端读取。

下面使用 io.Pipe() 创建管道的 reader 和 writer,然后将数据从 proverbs 缓冲区复制到io.Stdout :

func main() {

proverbs := new(bytes.Buffer)

proverbs.WriteString(“Channels orchestrate mutexes serialize

”)

proverbs.WriteString(“Cgo is not Go

”)

proverbs.WriteString(“Errors are values

”)

proverbs.WriteString(“Don’t panic

”)

piper, pipew := io.Pipe()

// 将 proverbs 写入 pipew 这一端

go func() {

defer pipew.Close()

io.Copy(pipew, proverbs)

}()

// 从另一端 piper 中读取数据并拷贝到标准输出

io.Copy(os.Stdout, piper)

piper.Close()

}

缓冲区 io

标准库中 bufio 包支持 缓冲区 io 操作,可以轻松处理文本内容。

例如,以下程序逐行读取文件的内容,并以值 ‘ ’ 分隔:

func main() {

file, err := os.Open(“。/planets.txt”)

if err != nil {

fmt.Println(err)

os.Exit(1)

}

defer file.Close()

reader := bufio.NewReader(file)

for {

line, err := reader.ReadString(‘

’)

if err != nil {

if err == io.EOF {

break

} else {

fmt.Println(err)

os.Exit(1)

}

}

fmt.Print(line)

}

}

ioutil

io 包下面的一个子包 utilio 封装了一些非常方便的功能

例如,下面使用函数 ReadFile 将文件内容加载到 []byte 中。

package main

import (

“io/ioutil”

。。。

func main() {

bytes, err := ioutil.ReadFile(“。/planets.txt”)

if err != nil {

fmt.Println(err)

os.Exit(1)

}

fmt.Printf(“%s”, bytes)

}

总结

本文介绍了如何使用 io.Reader 和 io.Writer 接口在程序中实现流式 IO。阅读本文后,您应该能够了解如何使用 io 包来实现 流式传输 IO 数据的程序。

其中有一些例子,展示了如何创建自己的类型,并实现io.Reader 和 io.Writer 。

这是一个简单介绍性质的文章,没有扩展开来讲。

例如,我们没有深入文件 IO,缓冲 IO,网络 IO 或格式化 IO(保存用于将来的写入)。

我希望这篇文章可以让你了解 Go 语言中 流式 IO 的常见用法是什么。

谢谢!

转自:ronniesong

segmentfault.com/a/1190000015591319

编辑:jq

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • Writer
    +关注

    关注

    0

    文章

    8

    浏览量

    7280

原文标题:Go 中 io 包的使用方法

文章出处:【微信号:magedu-Linux,微信公众号:马哥Linux运维】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    浅谈符号IO域和图形IO

    符号IO域组态 方法:把“符号IO域”这个灰色块状拖拽到指定画面位置,鼠标右键弹出对话框“属性”后点击“常规”找到过程“变量”后,连接变量。添加文本并创建文本名称。
    的头像 发表于 11-29 09:33 484次阅读
    浅谈符号<b class='flag-5'>IO</b>域和图形<b class='flag-5'>IO</b>域

    异步IO框架iouring介绍

    前言 Linux内核5.1支持了新的异步IO框架iouring,由Block IO大神也即Fio作者Jens Axboe开发,意在提供一套公用的网络和磁盘异步IO,不过io_uring
    的头像 发表于 11-09 09:30 496次阅读
    异步<b class='flag-5'>IO</b>框架iouring介绍

    linux异步io框架iouring应用

    Linux内核5.1支持了新的异步IO框架iouring,由Block IO大神也即Fio作者Jens Axboe开发,意在提供一套公用的网络和磁盘异步IO,不过io_uring目前在
    的头像 发表于 11-08 15:39 230次阅读
    linux异步<b class='flag-5'>io</b>框架iouring应用

    信号驱动IO与异步IO的区别

    一. 谈信号驱动IO (对比异步IO来看) 信号驱动IO 对比 异步 IO进行理解 信号驱动IO: 内核将数据准备好的时候, 使用SIGIO
    的头像 发表于 11-08 15:32 328次阅读
    信号驱动<b class='flag-5'>IO</b>与异步<b class='flag-5'>IO</b>的区别

    java中的IO流与Guava工具

    流将数据(从文件、网络、数据等)写入到程序,这里的IO指的是基于流作为载体进行数据传输。如果把数据比作合理的水,河就是IO流,也是数据的载体。 Java为我们提供了非常多的操作IO
    的头像 发表于 09-25 16:24 463次阅读

    IO与NIO有何区别

    NIO 提到IO,这是Java提供的一套类库,用于支持应用程序与内存、文件、网络间进行数据交互,实现数据写入与输出。JDK自从1.4版本后,提供了另一套类库NIO,我们平时习惯称呼为NEW I
    的头像 发表于 09-25 11:00 390次阅读
    <b class='flag-5'>IO</b>与NIO有何区别

    IO如何实现

    IO模型 我们的程序基本上都是对数据的IO操作以及基于CPU的运算。 基于Java的开发大部分是网络相关的编程,不管是基于如Tomcat般的Web容器,或是基于Netty开发的应用间的RPC服务
    的头像 发表于 09-25 10:57 468次阅读
    <b class='flag-5'>IO</b>如何<b class='flag-5'>实现</b>

    什么是ProfiNET/IO协议接口

    什么是ProfiNET/IO协议接口? ProfiNET/IO协议接口是一种基于以太网技术的实时通信协议,它可以将数据传输速度提高到100Mbit/s以上,并且支持多种拓扑结构和通信方
    的头像 发表于 08-23 10:33 1968次阅读
    什么是ProfiNET/<b class='flag-5'>IO</b>协议<b class='flag-5'>接口</b>?

    采用ADC扫描实现一个IO上挂多个按键

    有时候做设计时,我们会遇到外部按键比较多,IO口不够用的情况。这时大部分人会考虑通过其它芯片扩展IO,或者直接换一个IO口足够的MCU。其实,还有个方法可以实现一个
    的头像 发表于 08-11 09:16 1343次阅读
    采用ADC扫描<b class='flag-5'>实现</b>一个<b class='flag-5'>IO</b>上挂多个按键

    硬件设计如何实现一个IO上挂多个按键?

    有时候做设计时,我们会遇到外部按键比较多,IO口不够用的情况。这时大部分人会考虑通过其它芯片扩展IO,或者直接换一个IO口足够的MCU。其实,还有个方法可以实现一个
    发表于 08-11 09:16 433次阅读
    硬件设计如何<b class='flag-5'>实现</b>一个<b class='flag-5'>IO</b>上挂多个按键?

    IO-Link Master/IO Hub/IO-Link传感器和执行器解决方案

    电子发烧友网站提供《IO-Link Master/IO Hub/IO-Link传感器和执行器解决方案.pdf》资料免费下载
    发表于 08-01 11:26 7次下载
    <b class='flag-5'>IO</b>-Link Master/<b class='flag-5'>IO</b> Hub/<b class='flag-5'>IO</b>-Link传感器和执行器解决方案

    符号IO域和图形IO域的介绍

    符号IO域指已经编辑好的符号来显示输出输入变量。
    的头像 发表于 07-31 09:52 1212次阅读
    符号<b class='flag-5'>IO</b>域和图形<b class='flag-5'>IO</b>域的介绍

    PLC IO接口的详细介绍

    让我们以汇辰H7系列PLC为例,认识以下PLC的IO接口的位置,西门子PLC的接口位置也完全相同。
    发表于 06-26 17:18 5332次阅读
    PLC <b class='flag-5'>IO</b><b class='flag-5'>接口</b>的详细介绍

    基于TXS0108实现FPGA IO Bank接不同外设IO接口电压转换

    引言:上一篇文章我们介绍了通过添加电阻器、场效应晶体管(FET)开关、电平转换器甚至其他Xilinx FPGA等选项实现HP Bank IO与2.5V/3.3V外设对接的方法。本文介绍利用TI公司TXS0108实现FPGA
    的头像 发表于 05-16 09:02 2131次阅读
    基于TXS0108<b class='flag-5'>实现</b>FPGA <b class='flag-5'>IO</b> Bank接不同外设<b class='flag-5'>IO</b><b class='flag-5'>接口</b>电压转换

    Xilinx 7系列FPGA高性能接口与2.5V/3.3V外设IO接口设计

    Xilinx 7系列FPGA IO Bank分为HP Bank和HR Bank,HP IO接口电压范围为1.2V~1.8V,可以实现高性能,HR I
    发表于 05-15 09:27 2151次阅读
    Xilinx 7系列FPGA高性能<b class='flag-5'>接口</b>与2.5V/3.3V外设<b class='flag-5'>IO</b><b class='flag-5'>接口</b>设计