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