Go编程技巧--io.Reader/Writer

Go原生的pkg中有一些核心的interface,其中io.Reader/Writer是比较常用的接口。很多原生的结构都围绕这个系列的接口展开,在实际的开发过程中,你会发现通过这个接口可以在多种不同的io类型之间进行过渡和转化。本文结合实际场景来总结一番。

总览

在这里插入图片描述
围绕io.Reader/Writer,有几个常用的实现:

  • net.Conn, os.Stdin, os.File: 网络、标准输入输出、文件的流读取
  • strings.Reader: 把字符串抽象成Reader
  • bytes.Reader: 把[]byte抽象成Reader
  • bytes.Buffer: 把[]byte抽象成Reader和Writer
  • bufio.Reader/Writer: 抽象成带缓冲的流读取(比如按行读写)

这些实现对于初学者来说其实比较难去记忆,在遇到实际问题的时候更是一脸蒙圈,不知如何是好。下面用实际的场景来举例

什么是 io.Reader

type Reader interface {
  Read(p []byte) (n int, err error)
}
  • Reader 接口仅有一个 Read 方法,所以只要实现了Read(p []byte) (n int, err
    error)方法的类型,我们就认为其类型就是 Reader。
  • Read 方法会把读取的数据复制到 p 中 ,一次只能读取最高不超过 len(p) 的数据,并不保证一定会读满整个 p,读取的数据量返回到
    n,并在读完的时候返回 io.EOF 到 err。
  • Read 方法可以从不同来源读取数据,这个来源可能是文件、网络传输的数据或者普通的字符串,io.Reader
    接口不关心数据从哪里来,只要实现了 Read 方法,就能从来源处读数据。

标准库中的 io.Reader

在 go 标准库中,是有一些实现 io.Reader 接口的类型,这些也是 io.Reader 读数据的不同来源,下面列举下:

读文件时,通过 os.Open 打开一个文件,os.Open 会返回一个 Reader

var r io.Reader
var err error
r, err = os.Open("file.txt")

一个普通的字符串可通过 strings.NewReader 初始化一个 Reader

var r io.Reader
r = strings.NewReader("Read will return these bytes")

在网络传输中,http.Request 的 body 类型也是一个 Reader

var r io.Reader
req := http.Request{}
r = req.Body

bytes.Buffer 也是一个 Reader

var r io.Reader
var buf bytes.Buffer
r = &buf

如何使用 io.Reader

通过编写如下程序,使用 File.Read 的方法进行读取。

package main

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

func main() {
	file, err := os.Open("./book.txt")
	if err != nil {
		panic(err)
	}
	defer file.Close()
	bytesRead := make([]byte, 4)
	for {
		n, err := file.Read(bytesRead)
		// 读到文件的结尾,err 是 io.EOF
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Fatalf("error reading from book.txt: %v", err)
			break
		}
		if n > 0 {
			log.Printf("We read \"%s\" into bytesRead (%d bytes)",
				string(bytesRead[:n]), n)
		}
	}
}

结果

2022/07/10 12:04:22 We read "this" into bytesRead (4 bytes)
2022/07/10 12:04:22 We read " is " into bytesRead (4 bytes)
2022/07/10 12:04:22 We read "a bo" into bytesRead (4 bytes)
2022/07/10 12:04:22 We read "ok.
" into bytesRead (4 bytes)

上面已说过 io.reader 接口的本质是从不同的源读取数据,而这里的来源是文件,程序中的 file 变量是实现了 Read 方法的 Reader 类型,所以可以调用 Read 方法从文件中读数据,然后 copy 到 bytesRead 中。
可能不少小伙伴对最后 string(bytesRead[:n]) 的 re-slice 有些疑惑,这里如果不 re-slice下,打印的结果是不对的;因为 bytesRead 会保存上一次调用的读取的结果,感兴趣的小伙伴可以实践下~

版本一在日常的开发中我们基本不用,因为太繁琐,我们可以使用的 io.ReadAll 方法简化代码,感兴趣的小伙伴可以看下 io.ReadAll 源码,本质是把版本一的for循环封装了下。

package main

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

func main() {
   file, err := os.Open("./book.txt")
   if err != nil {
      panic(err)
   }
   defer file.Close()
   bytesRead, err := io.ReadAll(file)
   if err != nil {
      log.Fatalf("error reading from book.txt: %v", err)
   }
   log.Printf("We read "%s" into bytesRead (%d bytes)",
      string(bytesRead), len(bytesRead))
}

结果

2022/07/10 12:42:41 We read "this is a book.
" into bytesRead (16 bytes)

使用 os.ReadFile 在上一次基础上再次封装下,把 os.Open 也封装在内,这个方法是最常用的,因为足够简单。

package main

import (
   "log"
   "os"
)

func main() {
   bytesRead, err := os.ReadFile("./book.txt")
   if err != nil {
      return
   }
   log.Printf("We read "%s" into bytesRead (%d bytes)",
      string(bytesRead), len(bytesRead))
}

从字符串中初始化 io.Reader

从字符串(源)中读数据,然后 copy 到 buf 中。

package main

import (
    "io"
    "log"
    "strings"
)

func main() {
    s := strings.NewReader("this is a book.")
    buf := make([]byte, 5)
    for {
        n, err := s.Read(buf)
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatalf("error reading from book.txt: %v", err)
            break
        }

        if n > 0 {
            log.Printf("We read \"%s\" into bytesRead (%d bytes)",
                string(buf[:n]), len(buf))
        }
    }
}


结果

2022/07/10 13:03:32 We read "this " into bytesRead (5 bytes)
2022/07/10 13:03:32 We read "is a " into bytesRead (5 bytes)
2022/07/10 13:03:32 We read "book." into bytesRead (5 bytes)

其实本质和读文件类似,也可以使用 os.ReadAll(s) 简化下程序,这里不再赘述。

另一个使用比较广泛是反序列化一些json数据,比如我们有个json数据,想反序列化到结构体中,通过如下代码可以操作成功。

package main

import (
   "encoding/json"
   "log"
   "strings"
)

type Student struct {
   Name string `json:"name"`
   Sex  string `json:"sex"`
}

func main() {
   jsonData := `
            {
                "name":"zhangsan",
                "sex":"boy"
            }`

   rdr := strings.NewReader(jsonData)

   var s Student
   if err := json.NewDecoder(rdr).Decode(&s); err != nil {
      log.Fatalf("error deserializing JSON: %v", err)
      return
   }

   log.Printf("Hi! I'm a %s! \n", s.Name)
}

json.NewDecoder 接收 io.reader 类型的数据,rdr 是 io.reader 类型,可以从 rdr 读数据。
读到数据后,通过 json.Decoder.Decode 方法解码 json 数据,也就是反序列化一个结构体

网络传输中获取一个 reader

package main

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

func main() {
    conn, err := net.Dial("tcp", "baidu.com:80")
    if err != nil {
        panic(err)
    }
    defer conn.Close()

    fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")

    buf := make([]byte, 200)
    for {
        n, err := conn.Read(buf)
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatalf("error reading from conn: %v", err)
            break
        }
        if n > 0 {
            log.Printf("We read "%s" into bytesRead (%d bytes)",
                string(buf[:n]), n)
        }
    }
}

运行以上程序,输出为:

2022/07/10 13:24:57 We read "HTTP/1.1 200 OK
Date: Sun, 10 Jul 2022 05:24:57 GMT
Server: Apache
Last-Modified: Tue, 12 Jan 2010 13:48:00 GMT
ETag: "51-47cf7e6ee8400"
Accept-Ranges: bytes
Content-Length: 81
Cache-Control: m" into bytesRead (200 bytes)
2022/07/10 13:24:57 We read "ax-age=86400
Expires: Mon, 11 Jul 2022 05:24:57 GMT
Connection: Close
Content-Type: text/html

<html>
<meta http-equiv="refresh" content="0;url=http://www.baidu.com/">
</html>
" into bytesRead (181 bytes)


本质和读文件类似,也可以使用 io.ReadAll(conn) 简化下程序,简化后清爽了许多。

package main

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

func main() {
   conn, err := net.Dial("tcp", "baidu.com:80")
   if err != nil {
      panic(err)
   }
   defer conn.Close()

   fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")

   data, _ := io.ReadAll(conn)
   log.Printf("Hi! I'm a %s! \n", string(data))
}

如上所见,io.Reader 是一个很方便用的接口,这也是为什么在 go 代码中,随处可见 io.Readers,所以这是一个你真的需要掌握的知识点~

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 这是一个关于 JDK 的 Java 选项的提示信息,其中包含了三个参数:--add-opens=java.base/java.lang=all-unnamed、--add-opens=java.base/java.io=all-unnamed 和 --add-opens=java.rmi/sun.rmi.transport=all-unnamed。这些参数的作用是打开一些 Java 模块的访问权限,以便程序能够正常运行。 ### 回答2: 这段话是指在安装Java Development Kit(JDK)时,设置了一些参数,其中包括了三个--add-opens选项。这些选项被用来控制Java模块化系统中的包之间的可访问性,其中--add-opens选项允许将一个包的开放转移到另一个包中,以便在代码中可以访问这些包。 具体来说,这三个选项的作用如下: --add-opens=java.base/java.lang=all-unnamed:允许java.lang包下的所有类可以被所有未命名模块(即没有export声明的模块)访问。 --add-opens=java.base/java.io=all-unnamed:允许java.io包下的所有类可以被所有未命名模块访问。 --add-opens=java.rmi/sun.rmi.transport=all-unnamed:允许sun.rmi.transport包中的所有类在所有未命名模块中被访问,以支持Java远程方法调用。 这些选项都是为了解决Java模块化系统中访问控制的问题而设置的。Java模块化系统将Java SE 9中引入的的重要特性之一,用于管理和组织Java平台的各个组件,更好地支持解耦和重用代码。在这种情况下,控制代码的可见性是必然的。 总结一下,这个命令行参数的目的是允许指定的模块之间进行互操作,从而扩展了Java以支持更加灵活的应用程序。这种输入非常具体化,对Java开发人员来说非常重要,因为他们需要确保代码可以编译和运行,而在Java模块化环境中,不能访问私有的API。 ### 回答3: 这个信息是指在Java开发环境中,通过另一个程序或者脚本获取到的一组JDK参数设置,具体是关于--add-opens选项的设置。这个选项用于打开一些Java模块中的未命名包,以方便开发者在使用Java模块时更加灵活、自由地访问其中各种资源。 具体来说,这个选项设置了三个参数:--add-opens=java.base/java.lang=all-unnamed、--add-opens=java.base/java.io=all-unnamed、--add-opens=java.rmi/sun.rmi.transport=all-unnamed。第一组参数表示将Java基础模块中的java.lang包中所有未命名的资源包全部打开;第二组参数表示将Java基础模块中的java.io包中所有未命名的资源包全部打开;第三组参数则是将Java RMI(远程方法调用)模块中的sun.rmi.transport包中的所有未命名资源包打开。 这个设置的意义主要在于在使用Java开发项目时,尤其是在使用一些第三方库或者框架时,可能会需要访问到一些Java模块中的未命名资源包,例如一些未公开的API、内部实现类等等,而Java默认情况下是不允许访问这些资源包的。通过使用--add-opens选项,开发者可以将这些资源包打开,使得自己可以更加灵活地使用和定制Java环境,提高开发效率和代码可读性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值