Go36-40,41-io包中的介面和工具
字元操作和位元組操作型別的介面
首先,瞭解一下strings.Builder、strings.Reader和bytes.Buffer這三個資料型別中實現的介面。
strings.Builder型別
strings.Builder型別主要用於構建字串,它的指標型別實現的介面有:
- io.Writer
- io.ByteWriter
- fmt.Stringer
- io.stringWriter,io包的包級私有介面
strings.Reader型別
strings.Reader型別主要用於讀取字串,它的指標型別實現的介面有:
- io.Reader
- io.ReaderAt
- io.ByteReader
- io.RuneReader
- io.Seeker
- io.ByteScanner,這個是io.ByteReader介面的擴充套件
- io.RuneScanner, 這個是io.RuneReader介面的擴充套件
- io.WriterTo
bytes.Buffer型別
bytes.Buffer是集讀、寫功能於一身的資料型別,它非常適合作為位元組序列的緩衝區。它的指標型別實現的介面非常多。
該指標型別實現的讀取相關的介面有:
- io.Reader
- io.ByteReader
- io.RuneReader
- io.ByteScanner
- io.RuneScanner
- io.WriterTo
該指標型別實現的寫入相關的介面有:
- io.Writer
- io.ByteWriter
- io.stringWriter,io包的包級私有介面
- io.ReaderFrom
另外,還有一個匯出相關的介面:fmt.Stringer
驗證程式碼
下面的程式碼對公開的介面進行了驗證:
package main import ( "bytes" "fmt" "io" "strings" ) func main() { b1 := new(strings.Builder) _ = interface{}(b1).(io.Writer) _ = interface{}(b1).(io.ByteWriter) _ = interface{}(b1).(fmt.Stringer) b2 := strings.NewReader("") _ = interface{}(b2).(io.Reader) _ = interface{}(b2).(io.ReaderAt) _ = interface{}(b2).(io.ByteReader) _ = interface{}(b2).(io.RuneReader) _ = interface{}(b2).(io.Seeker) _ = interface{}(b2).(io.ByteScanner) _ = interface{}(b2).(io.RuneScanner) _ = interface{}(b2).(io.WriterTo) b3 := bytes.NewBuffer([]byte{}) _ = interface{}(b3).(io.Reader) _ = interface{}(b3).(io.ByteReader) _ = interface{}(b3).(io.RuneReader) _ = interface{}(b3).(io.ByteScanner) _ = interface{}(b3).(io.RuneScanner) _ = interface{}(b3).(io.WriterTo) _ = interface{}(b3).(io.Writer) _ = interface{}(b3).(io.ByteWriter) _ = interface{}(b3).(io.ReaderFrom) _ = interface{}(b3).(fmt.Stringer) }
io包
上面的這些型別實現了這麼多的介面,目的是為了提高不同程式實體之間的互操作性。
在io包中,有如下幾個用於拷貝資料的函式:
- io.Copy
- io.CopyBuffer
- io.CopyN
這幾個函式在功能上略有差別,但是首先都會接收2個引數:
- dst,io.Writer型別,表示資料目的
- src,io.Reader型別,表示資料來源
而這些函式的功能大致上也是把資料從src拷貝到dst。用了介面之後,不論給予引數值是什麼型別的,只要實現了介面就行。只要實現了介面,這些函式幾乎就可以正常執行了。當然,在函式中還會對必要的引數值進行有效性的檢查,如果檢查不通過,它的執行也是不能夠成功結束的。
io.CopyN函式舉例
來看下面的示例程式碼:
package main import ( "fmt" "io" "os" "strings" ) func main() { src := strings.NewReader("Happy New Year") dst := new(strings.Builder) written, err := io.CopyN(dst, src, 5) if err != nil { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) } fmt.Println(written, dst.String()) }
首先,使用了strings.NewReader建立了一個字串讀取器,並把它賦值給了變數src,然後有new了一個字串構建器,並將其賦予了變數dst。
之後,呼叫了io.CopyN函式的時候,把兩個變數的值都傳遞了進去,同時還指定了第三個引數int64型別,就是要從src中拷貝多少個位元組到dst裡。
雖然,變數src和dst型別分別是strings.Reader和strings.Builder,但是當它們被傳到io.CopyN函式的時候,就已經分別被包裝成了io.Reader型別和io.Writer型別的值。而io.CopyN函式也根本不會去在意它們的實際型別到底是什麼。為了優化的目的,io.CopyN函式中的程式碼會對引數值進行再包裝,也會檢測這些引數值是否還實現了別的介面,甚至還會去探求某個引數值被包裝後的實際型別,是否未某個特殊的型別。但是總體上來看,這些程式碼都是面向引數宣告中的介面來做的。
在上面的示例中,通過面向介面程式設計,極大地拓展了它的適用範圍和應用場景。換個角度來看,正式因為strings.Reader型別和strings.Builder型別都實現了不少介面,所以他們的值才能夠被使用在更廣闊的場景中。比如strings包和bytes包中的資料型別在實現了若干介面之後得到了很多好處,這就是面向介面程式設計帶來的優勢。
在Go語言中,對介面的擴充套件是通過型別之間的嵌入來實現的,這也常被叫做介面的組合。這個在講介面的時候也提過,Go語言提倡使用小介面加介面組合的方式,來擴充套件程式的行為以及增加程式的靈活性。io程式碼包恰恰就可以作為這樣的一個標杆,它可以成為我們運用這種技巧是的一個參考標準。
介面擴充套件和實現
以io.Reader介面為物件,來了解一下介面擴充套件和實現,以及各自的功用。
在io包中,io.Reader的擴充套件介面有下面幾種:
- io.ReadWriter,既是io.Reader的擴充套件介面,也是io.Writer的擴充套件介面。該介面定了以一組行為,包含且僅包含了基本的位元組序列讀取方法Read,和位元組序列寫入方法Write。
- io.ReadCloser,io.Reader介面和io.Closer介面的組合。除了包含基本的位元組序列讀取方法之外,還擁有一個基本的關閉方法Close。關閉方法一般用於關閉資料讀寫的通路。
- io.ReadWriteCloser,很明顯,就是io.Reader、io.Writer和io.Closer這三個介面的組合。
- io.ReadSeeker,此介面的特點就是擁有一個用於尋找讀寫位置的基本方法Seek。該方法可以根據給定的偏移量基於資料的起始位置、末尾位置,或者當前讀寫位置去尋找新的讀寫位置。Seek是io.Seeker介面唯一的一個方法。
- io.ReadWriteSeeker,顯然,就是io.Reader、io.Writer和io.Seeker這三個介面的組合。
然後是io包中的io.Reader介面的實現型別,包括以下幾項內容:
- *io.LimitedReader
- *io.SectionReader
- *io.teeReader
- *io.multiReader
- *io.pipe
- *io.PipeReader
這裡忽略掉了測試原始碼檔案中的實現型別,以及不會以任何形式直接對外暴露的那些實現型別。
*io.LimitedReader結構體型別如下:
type LimitedReader struct { R Reader // underlying reader N int64// max bytes remaining }
此型別的基本型別會包裝io.Reader型別的值,並提供一個額外的受限讀取的功能。所謂的受限讀取指的是,此型別的讀取方法和Read返回的總資料量會受到限制,無論該方法被呼叫多少次。這個限制由該型別的欄位N表明,單位是位元組。
*io.SectionReader結構體型別如下:
type SectionReader struct { rReaderAt baseint64 offint64 limit int64 }
此型別的基本型別會包裝io.ReaderAt型別的值,並且會限制它的Read方法,只能夠讀取原始資料中的某一個部分,或者說一段。這個資料段的起始位置和末尾位置,需要在它被初始化的時候就指明,並且之後無法變更。該型別值的行為與切片有些類似,它只會對外暴露在其視窗之中的那些資料。
*io.teeReader結構體型別如下:
type teeReader struct { r Reader w Writer } func TeeReader(r Reader, w Writer) Reader { return &teeReader{r, w} }
此型別是一個包級私有的資料型別,也是io.TeeReader函式結果值的實際型別,這個函式接受兩個引數r和w。*teeReader的Read方法會把r中的資料經過作為方法引數的位元組切片p寫入到w。可以說,這是一個r和w之間的資料橋樑,而那個引數p就是這座橋上的資料搬運者。
*io.multiReader結構體型別如下:
type multiWriter struct { writers []Writer } func MultiReader(readers ...Reader) Reader { r := make([]Reader, len(readers)) copy(r, readers) return &multiReader{r} }
此型別也是一個包級私有的資料型別。通過io.MultiReader函式,接受若干個io.Reader型別的引數值,返回一個例項型別為*io.multiWriter的結果值。它的Read方法被呼叫時,會順序的從前面的那些io.Reader型別的引數值中讀取資料。因此,也可以稱之為多物件讀取器 。
*io.pipe結構體型別如下:
type pipe struct { wrMu sync.Mutex // Serializes Write operations wrCh chan []byte rdCh chan int once sync.Once // Protects closing done done chan struct{} rerr atomicError werr atomicError } func Pipe() (*PipeReader, *PipeWriter) { p := &pipe{ wrCh: make(chan []byte), rdCh: make(chan int), done: make(chan struct{}), } return &PipeReader{p}, &PipeWriter{p} }
此型別為一個包級私有的資料型別,它比較複雜。不但實現了io.Reader介面,而且還實現了io.Writer介面。io.PipeReader型別和io.PipeWriter型別擁有的所有指標方法都是以它為基礎的。這些方法都只是代理了io.pipe型別值所擁有的某一個方法而已。又因為,io.Pipe函式會返回這兩個型別的指標值並分別把它們作為其生成的同步記憶體管理的兩端,所以*io.pipe型別就是io包提供的同步記憶體管道的核心實現。
*io.PipeReader結構體型別如下:
type PipeReader struct { p *pipe }
此型別可以被視為io.pipe型別的代理型別。它代理了io.pipe中一部分功能,並基於io.pipe實現了io.ReadCloser介面。同時,它還定義了同步記憶體管道的讀取端。
集中示例展示
上面所講的每一個型別都寫了一小段程式碼,展示了這些型別的一些基本用法:
package main import ( "fmt" "io" "os" "strings" "sync" ) // 統一定義一個方法來處理錯誤,這樣不會看到很多 if err != nil {} 這種 func executeIfNoErr(err error, f func()) { if err != nil { fmt.Fprintf(os.Stderr, "\tERROR: %v\n", err) return } f() } func main() { comment := "Make the plan. " + "Execute the plan. " + "Expect the plan to go off the rails. " + "Throw away the plan." fmt.Println("原生string型別:") reader1 := strings.NewReader(comment) buf1 := make([]byte, 4) n, err := reader1.Read(buf1) var offset1, index1 int64 executeIfNoErr(err, func() { fmt.Printf("\tRead(%d): %q\n", n, buf1[:n]) offset1 = int64(5) index1, err = reader1.Seek(offset1, io.SeekCurrent) }) executeIfNoErr(err, func() { fmt.Printf("\t偏移量: %d,%d\n", offset1, index1) n, err = reader1.Read(buf1) }) executeIfNoErr(err, func() { fmt.Printf("\tRead(%d): %q\n", n, buf1[:n]) }) reader1.Reset(comment) num2 := int64(15) fmt.Printf("LimitReader型別,限制資料量(%d):\n", num2) reader2 := io.LimitReader(reader1, num2) buf2 := make([]byte, 4) for i := 0; i < 6; i++ { n, err := reader2.Read(buf2) executeIfNoErr(err, func() { fmt.Printf("\tRead(%d): %q\n", n, buf2[:n]) }) } reader1.Reset(comment) offset3 := int64(33) num3 := int64(37) fmt.Printf("SectionReader型別,起始偏移量(%d),到末端的長度(%d):\n", offset3, num3) reader3 := io.NewSectionReader(reader1, offset3, num3) buf3 := make([]byte, 15) for i := 0; i < 5; i++ { n, err := reader3.Read(buf3) executeIfNoErr(err, func() { fmt.Printf("\tRead(%d): %q\n", n, buf3[:n]) }) } reader1.Reset(comment) writer4 := new(strings.Builder) fmt.Printf("teeReader型別,write4現在應該為空(%q):\n", writer4) reader4 := io.TeeReader(reader1, writer4) buf4 := make([]byte, 33) for i := 0; i < 5; i++ { n, err := reader4.Read(buf4) executeIfNoErr(err, func() { fmt.Printf("\tRead(%d): %q\n", n, buf4[:n]) fmt.Printf("\tWrite: %q\n", writer4) }) } reader5a := strings.NewReader("Make the plan.") reader5b := strings.NewReader("Execute the plan.") reader5c := strings.NewReader("Expect the plan to go off the rails.") reader5d := strings.NewReader("Throw away the plan.") fmt.Println("multiWriter型別,一共4個readers:") reader5 := io.MultiReader(reader5a, reader5b, reader5c, reader5d) buf5 := make([]byte, 15) for i := 0; i < 10; i++ { n, err := reader5.Read(buf5) executeIfNoErr(err, func() { fmt.Printf("\tRead(%d): %q\n", n, buf5[:n]) }) } fmt.Println("pipe型別:") pReader, pWriter := io.Pipe() _ = interface{}(pReader).(io.ReadCloser) // 驗證是否實現了 io.ReadCloser 介面 _ = interface{}(pWriter).(io.WriteCloser) var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() n, err := pWriter.Write([]byte(comment)) defer pWriter.Close() executeIfNoErr(err, func() { fmt.Printf("\tWrite(%d)\n", n) }) }() go func() { defer wg.Done() buf6 := make([]byte, 15) for i := 0; i < 10; i++ { n, err := pReader.Read(buf6) executeIfNoErr(err, func() { fmt.Printf("\tRead(%d): %q\n", n, buf6[:n]) }) } }() wg.Wait() fmt.Println("所有示例完成") }
io包中的介面
前面的內容,主要講的是io.Reader的擴充套件介面和實現型別。當然,io程式碼包中的核心介面不止io.Reader一個。這裡基於它引出的一條主線只是io包型別體系中的一部分。這裡再換個角度來對io包做進一步的瞭解。
可以把沒有嵌入其他介面並且只定義了一個方法的介面叫做簡單介面 。在io包中,這樣的介面共有11個。
另外,有的介面有著眾多的擴充套件介面和實現型別,可以稱為核心介面 ,io包中的核心介面只有3個:
- io.Reader
- io.Writer
- io.Closer
可以把io包中的簡單介面分為四大類。這四大類介面分別針對於四種操作:讀取、寫入、關閉和讀寫位置設定。前三種操作屬於基本的I/O操作。
關於讀取操作,已經重點講過核心介面的io.Reader。它在io包中有5個擴充套件介面,並有6個實現型別。這個包中針對讀取操作的介面還有不少。
io.ByteReader
type ByteReader interface { ReadByte() (byte, error) }
簡單介面,定義了一個讀取方法ReadByte。這個讀取方法能夠讀取下一個單一的位元組。
RuneReader
type RuneReader interface { ReadRune() (r rune, size int, err error) }
簡單介面,定義了一個讀取方法ReadRune。這個讀取方法能夠讀取下一個單一的Unicode字元。
io.ByteScanner
type ByteScanner interface { ByteReader UnreadByte() error }
該介面內嵌了簡單介面io.ByteReader,並定義了額外的UnreadByte方法。它就抽象了可以讀取和讀回退單個位元組的功能集。
io.RuneScanner
type RuneScanner interface { RuneReader UnreadRune() error }
該介面內嵌了簡單介面io.RuneReader,並定義了額外的UnreadRune方法。它抽象了可以讀取和讀回退單個Unicode字元的功能集。
io.ReaderAt
type ReaderAt interface { ReadAt(p []byte, off int64) (n int, err error) }
簡單介面,定義了一個ReadAt方法。這是一個純粹的只讀方法,它只去讀取其所屬值中包含的位元組,而不對這個值進行任何改動。比如,它絕對不能去修改已讀計數的值。這也是io.ReaderAt介面與其實現型別之間最重要的一個約定。因此,如果僅僅併發的呼叫某一個值的ReadAt方法,那麼安全性應該是可以得到保障的。
io.WriterTo
type WriterTo interface { WriteTo(w Writer) (n int64, err error) }
簡單介面,定義了一個WriteTo的讀取方法。該方法接受一個io.Writer型別的引數值,會把其所屬值中的資料讀出,並寫入到這個引數值中。
io.ReaderFrom
type ReaderFrom interface { ReadFrom(r Reader) (n int64, err error) }
簡單介面,定義了一個ReadFrom的寫入方法。該方法接受一個io.Reader型別的引數值,會從該引數值中讀取出資料,並寫入到其所屬值中。
寫入操作相關介面
從上面這些介面中,可以看出,在io包中與寫入操作有關的介面都與讀取操作相關的介面有著一定的對應關係。下面就說是寫入操作有關的介面。
io.Write
io.Write是核心介面。基於它的擴充套件介面如下:
- io.ReadWriter,實現型別有*io.pipe
- io.ReadWriteCloser
- io.ReadWriteSeeker,在io包中沒有這個介面的實現,它的實現型別主要集中在net包中。
- io.WriteCloser
- io.WriteSeker
io.ByteWriter和io.WriterAt
這兩個是寫入操作相關的簡單介面。在io包中,沒有他們的實現型別。
順便提一下這個資料型別:*io.File。這個型別不但是io.WriterAt介面的實現型別,同時還實現了io.ReadWriteCloser介面和io.ReadWriteSeeker介面。就是說,該型別支援的I/O操作非常豐富。
io.Seeker
這個介面是一個讀寫位置設定相關的簡單介面,也僅僅定義了一個Seek方法。該方法主要用於尋找並設定下一次讀取或寫入時的起始索引位置,在strings包裡講過。
在io包中,有幾個基於io.Seeker的擴充套件介面:
- io.ReadSeeker
- io.ReadWriteSeeker
- io.WriteSeeker,基於io.Writer和io.Seeker的擴充套件介面
這兩個型別的指標:strings.Reader和io.SectionReader,都實現了io.Seeker介面。順便提一下,這兩個型別的指標也都是io.ReaderAt介面的實現型別。
io.Closer
這是關閉操作相關的介面,非常通用,它的擴充套件介面和實現型別都不少。單從名稱上就能看出io包中哪些介面是它的擴充套件介面。它的實現型別,在io包中只有io.PipeReader和io.PipeWriter。
總結
本篇是為了能夠使我們牢記io包中有著網狀關係的介面和資料型別。如果暫時未能牢記,至少可以作為深刻記憶它們的開始。
在之後需要思考和時間的是:在什麼時候應該編寫哪些資料型別實現io包中的哪些介面,並以此得到最大的好處。