1. 程式人生 > 其它 >Go xmas2020 學習筆記 11、io.Reader

Go xmas2020 學習筆記 11、io.Reader

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

主講老師 Matt Holiday

11-Homework #2

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,有幾個常用的實現

  • 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\) 方法。