Go xmas2020 学习笔记 11、io.Reader

课程地址 go-class-slides/xmas-2020 at trunk · matt4biz/go-class-slides (github.com)

主讲老师 Matt Holiday

image-20220401081031592

11-Homework #2

image-20220405145330189

package main

import (
	"bytes"
	"fmt"
	"os"
	"strings"

	"golang.org/x/net/html"
)

var raw = `
<!DOCTYPE html>
<html>
  <body>
    <h1>My First Heading</h1>
      <p>My first paragraph.</p>
      <p>HTML <a href="https://www.w3schools.com/html/html_images.asp">images</a> are defined with the img tag:</p>
      <img src="xxx.jpg" width="104" height="142">
  </body>
</html>
`

func visit(n *html.Node, words, pics *int) {

	if n.Type == html.TextNode {
		*words += len(strings.Fields(n.Data))
	} else if n.Type == html.ElementNode && n.Data == "img" {
		*pics++
	}

	for c := n.FirstChild; c != nil; c = c.NextSibling {
		visit(c, words, pics)
	}
}

func countWordsAndImages(doc *html.Node) (int, int) {
	var words, pics int

	visit(doc, &words, &pics)

	return words, pics
}

func main() {
	doc, err := html.Parse(bytes.NewReader([]byte(raw)))

	if err != nil {
		fmt.Fprintf(os.Stderr, "parse failed:%s\n", err)
		os.Exit(-1)
	}

	words, pics := countWordsAndImages(doc)

	fmt.Printf("%d words and %d images\n", words, pics)
}
14 words and 1 images

假如我去访问一个网站,我会得到一个字节的片段,将它放到阅读器中。

	doc, err := html.Parse(bytes.NewReader([]byte(raw)))

返回的\(doc\)是树节点,我们可以用 \(for\) 循环通过节点的 \(FirstChild、NextSibling\) 属性遍历整棵树。


11-Reader

Reader interface

上文出现了阅读器这个概念,我感到很模糊,于是查找相关资料进行学习。

type Reader interface {
    Read(p []byte) (n int ,err error)
}

官方文档中关于该接口方法的说明

Read 将 len(p) 个字节读取到 p 中。它返回读取的字节数 n(0 <= n <= len(p)) 以及任何遇到的错误。即使 Read 返回的 n < len(p),它也会在调用过程中使用 p 的全部作为暂存空间。若一些数据可用但不到 len(p) 个字节,Read 会照例返回可用的数据,而不是等待更多数据。

Read 在成功读取 n > 0 个字节后遇到一个错误或 EOF (end-of-file),它就会返回读取的字节数。它会从相同的调用中返回(非nil的)错误或从随后的调用中返回错误(同时 n == 0)。 一般情况的一个例子就是 Reader 在输入流结束时会返回一个非零的字节数,同时返回的 err 不是 EOF 就是nil。无论如何,下一个 Read 都应当返回 0, EOF

调用者应当总在考虑到错误 err 前处理 n > 0 的字节。这样做可以在读取一些字节,以及允许的 EOF 行为后正确地处理 I/O 错误

PS: 当Read方法返回错误时,不代表没有读取到任何数据,可能是数据被读完了时返回的io.EOF

Reader 接口的方法集(Method_sets)只包含一个 Read 方法,因此,所有实现了 Read 方法的类型都实现了io.Reader 接口,也就是说,在所有需要 io.Reader 的地方,可以传递实现了 Read()方法的类型的实例。


NewReader func

Reader Struct

NewReader创建一个从s读取数据的Reader

type Reader struct {
	s        string //对应的字符串
	i        int64  // 当前读取到的位置
	prevRune int   
}

Len 、Size,Read func

Len作用: 返回未读的字符串长度

Size的作用:返回字符串的长度

read的作用: 读取字符串信息

r := strings.NewReader("abcdefghijklmn")
fmt.Println(r.Len())   // 输出14  初始时,未读长度等于字符串长度
var buf []byte
buf = make([]byte, 5)
readLen, err := r.Read(buf)
fmt.Println("读取到的长度:", readLen) //读取到的长度5
if err != nil {
	fmt.Println("错误:", err)
}
fmt.Println(buf)            //adcde
fmt.Println(r.Len())        //9   读取到了5个 剩余未读是14-5
fmt.Println(r.Size())       //14   字符串的长度

Practice

任何实现了 Read() 函数的对象都可以作为 Reader 来使用。

围绕io.Reader/Writer,有几个常用的实现

io.reader

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

我们编写一个通用的阅读器至标准输出流方法,并分别传入对象 \(os.File、net.Conn、strings.Reader\)

func readerToStdout(r io.Reader, bufSize int) {
	buf := make([]byte, bufSize)
	for {
		n, err := r.Read(buf)
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println(err)
			break
		}
		if n > 0 {
			fmt.Println(string(buf[:n]))
		}
	}
}

\(readerToStdout\) 方法中,我们传入实现了 \(io.Reader\) 接口的对象,并规定一个每次读取数据的缓冲字节切片的大小。

需要注意的是,由于是分段读取,需要使用 \(for\) 循环,通过判断 \(io.EOF\) 退出循环,同时还需要考虑其他错误。输出至 \(os.Stdin\) 标准流时需要对字节切片进行字符串类型转换,同时字节切片应该被索引截取。\(n\)是本次读取到的字节数。


如果输出时切片不被索引截取会出现什么情况。

func fileReader() {
	f, err := os.Open("book.txt")
	if err != nil {
		panic(err)
	}
	defer f.Close()
	buf := make([]byte, 3)
	for {
		n, err := f.Read(buf)
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println(err)
			break
		}
		if n > 0 {
			fmt.Println(buf)
		}
	}
}
book.txt 内容为 abcd

[97 98 99]
[100 98 99]

第一次循环缓冲切片被正常填满,而第二次由于还剩一个字节,便将这一个字节读入缓冲切片中,而后面元素未被改变。假定文件字节数很小,缓冲切片很大,那么第一次就可以读取完成,这会导致输出字节数组后面的 \(0\) 或一些奇怪的内容。


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

	fmt.Fprint(conn, "GET /index.html HTTP/1.0\r\n\r\n")

	readerToStdout(conn, 20)
}

这里我们通过 \(net.Dial\) 方法创建一个 \(tcp\) 连接,同时我们需要使用 \(fmt.Fprint\) 方法给特定连接发送请求。\(conn\) 实现了 \(io.Reader\) 接口,可以传入 \(readerToStdout\) 方法。

func stringsReader() {
	s := strings.NewReader("very short but interesting string")
	readerToStdout(s, 5)
}

func fileReader() {
	f, err := os.Open("book.txt")
	if err != nil {
		panic(err)
	}
	defer f.Close()
	readerToStdout(f, 3)
}

我们给定 \(string\) 对象来构造 \(strings.Reader\),并传入 \(readerToStdout\) 方法。我们使用 \(os.Open\) 打开文件,所得到的 \(File\) 对象也实现了 \(os.Reader\) 接口。

posted @ 2022-04-06 07:57  小能日记  阅读(15)  评论(0编辑  收藏  举报