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: 抽象成帶緩沖的流讀取(比如按行讀寫)
這些實現對于初學者來說其實比較難去記憶,在遇到實際問題的時候更是一臉蒙圈,不知如何是好。下面用實際的場景來舉例
場景舉例
0. base64編碼成字符串
encoding/base64
包中:
func NewEncoder(enc *Encoding, w io.Writer) io.WriteCloser
這個用來做base64
編碼,但是仔細觀察發現,它需要一個io.Writer作為輸出目標,并用返回的WriteCloser
的Write方法將結果寫入目標,下面是Go官方文檔的例子
input := []byte("foo\x00bar")
encoder := base64.NewEncoder(base64.StdEncoding, os.Stdout)
encoder.Write(input)
這個例子是將結果寫入到Stdout
,如果我們希望得到一個字符串呢?觀察上面的圖,不然發現可以用bytes.Buffer作為目標io.Writer
:
input := []byte("foo\x00bar")
buffer := new(bytes.Buffer)
encoder := base64.NewEncoder(base64.StdEncoding, buffer)
encoder.Write(input)
fmt.Println(string(buffer.Bytes())
1. []byte和struct之間正反序列化
這種場景經常用在基于字節的協議上,比如有一個具有固定長度的結構:
type Protocol struct {
Version uint8
BodyLen uint16
Reserved [2]byte
Unit uint8
Value uint32
}
通過一個[]byte
來反序列化得到這個Protocol
,一種思路是遍歷這個[]byte
,然后逐一賦值。其實在encoding/binary
包中有個方便的方法:
func Read(r io.Reader, order ByteOrder, data interface{}) error
這個方法從一個io.Reader
中讀取字節,并已order
指定的端模式,來給填充data
(data需要是fixed-sized的結構或者類型)。要用到這個方法首先要有一個io.Reader
,從上面的圖中不難發現,我們可以這么寫:
var p Protocol
var bin []byte
//...
binary.Read(bytes.NewReader(bin), binary.LittleEndian, &p)
換句話說,我們將一個[]byte
轉成了一個io.Reader
。
反過來,我們需要將Protocol
序列化得到[]byte
,使用encoding/binary
包中有個對應的Write
方法:
func Write(w io.Writer, order ByteOrder, data interface{}) error
通過將[]byte
轉成一個io.Writer
即可:
var p Protocol
buffer := new(bytes.Buffer)
//...
binary.Writer(buffer, binary.LittleEndian, p)
bin := buffer.Bytes()
2. 從流中按行讀取
比如對于常見的基于文本行的HTTP
協議的讀取,我們需要將一個流按照行來讀取。本質上,我們需要一個基于緩沖的讀寫機制(讀一些到緩沖,然后遍歷緩沖中我們關心的字節或字符)。在Go中有一個bufio
的包可以實現帶緩沖的讀寫:
func NewReader(rd io.Reader) *Reader
func (b *Reader) ReadString(delim byte) (string, error)
這個ReadString方法從io.Reader
中讀取字符串,直到delim
,就返回delim
和之前的字符串。如果將delim
設置為\n
,相當于按行來讀取了:
var conn net.Conn
//...
reader := NewReader(conn)
for {
line, err := reader.ReadString([]byte('\n'))
//...
}
花式技(zuo)巧(si)
string轉[]byte
a := "Hello, playground"
fmt.Println([]byte(a))
等價于
a := "Hello, playground"
buf := new(bytes.Buffer)
buf.ReadFrom(strings.NewReader(a))
fmt.Println(buf.Bytes())