Go語言核心36講(Go語言實戰與應用十九)--學習筆記

MingsonZheng發表於2021-12-01

41 | io包中的介面和工具 (下)

上一篇文章中,我主要講到了io.Reader的擴充套件介面和實現型別。當然,io程式碼包中的核心介面不止io.Reader一個。

我們基於它引出的一條主線,只是io包型別體系中的一部分。我們很有必要再從另一個角度去探索一下,以求對io包有更加全面的瞭解。

下面的一個問題就與此有關。

知識擴充套件問題:

io包中的介面都有哪些?它們之間都有著怎樣的關係?

我們可以把沒有嵌入其他介面並且只定義了一個方法的介面叫做簡單介面。在io包中,這樣的介面一共有 11 個。

在它們之中,有的介面有著眾多的擴充套件介面和實現型別,我們可以稱之為核心介面io包中的核心介面只有 3 個,它們是:io.Reader、io.Writer和io.Closer。

我們還可以把io包中的簡單介面分為四大類。這四大類介面分別針對於四種操作,即:讀取、寫入、關閉和讀寫位置設定。前三種操作屬於基本的 I/O 操作。

關於讀取操作,我們在前面已經重點討論過核心介面io.Reader。它在io包中有 5 個擴充套件介面,並有 6 個實現型別。除了它,這個包中針對讀取操作的介面還有不少。我們下面就來梳理一下。

首先來看io.ByteReader和io.RuneReader這兩個簡單介面。它們分別定義了一個讀取方法,即:ReadByte和ReadRune。

但與io.Reader介面中Read方法不同的是,這兩個讀取方法分別只能夠讀取下一個單一的位元組和 Unicode 字元。

我們之前講過的資料型別strings.Reader和bytes.Buffer都是io.ByteReader和io.RuneReader的實現型別。

不僅如此,這兩個型別還都實現了io.ByteScanner介面和io.RuneScanner介面。

io.ByteScanner介面內嵌了簡單介面io.ByteReader,並定義了額外的UnreadByte方法。如此一來,它就抽象出了一個能夠讀取和讀回退單個位元組的功能集。

與之類似,io.RuneScanner內嵌了簡單介面io.RuneReader,並定義了額外的UnreadRune方法。它抽象的是可以讀取和讀回退單個 Unicode 字元的功能集。

再來看io.ReaderAt介面。它也是一個簡單介面,其中只定義了一個方法ReadAt。與我們在前面說過的讀取方法都不同,ReadAt是一個純粹的只讀方法。

它只去讀取其所屬值中包含的位元組,而不對這個值進行任何的改動,比如,它絕對不能去修改已讀計數的值。這也是io.ReaderAt介面與其實現型別之間最重要的一個約定。

因此,如果僅僅併發地呼叫某一個值的ReadAt方法,那麼安全性應該是可以得到保障的。

另外,還有一個讀取操作相關的介面我們沒有介紹過,它就是io.WriterTo。這個介面定義了一個名為WriteTo的方法。

千萬不要被它的名字迷惑,這個WriteTo方法其實是一個讀取方法。它會接受一個io.Writer型別的引數值,並會把其所屬值中的資料讀出並寫入到這個引數值中。

與之相對應的是io.ReaderFrom介面。它定義了一個名叫ReadFrom的寫入方法。該方法會接受一個io.Reader型別的引數值,並會從該引數值中讀出資料, 並寫入到其所屬值中。

值得一提的是,我們在前面用到過的io.CopyN函式,在複製資料的時候會先檢測其引數src的值,是否實現了io.WriterTo介面。如果是,那麼它就直接利用該值的WriteTo方法,把其中的資料拷貝給引數dst代表的值。

類似的,這個函式還會檢測dst的值是否實現了io.ReaderFrom介面。如果是,那麼它就會利用這個值的ReadFrom方法,直接從src那裡把資料拷貝進該值。

實際上,對於io.Copy函式和io.CopyBuffer函式來說也是如此,因為它們在內部做資料複製的時候用的都是同一套程式碼。

你也看到了,io.ReaderFrom介面與io.WriterTo介面對應得很規整。實際上,在io包中,與寫入操作有關的介面都與讀取操作的相關介面有著一定的對應關係。下面,我們就來說說寫入操作相關的介面。

首先當然是核心介面io.Writer。基於它的擴充套件介面除了有我們已知的io.ReadWriter、io.ReadWriteCloser和io.ReadWriteSeeker之外,還有io.WriteCloser和io.WriteSeeker。

我們之前提及的*io.pipe就是io.ReadWriter介面的實現型別。然而,在io包中並沒有io.ReadWriteCloser介面的實現,它的實現型別主要集中在net包中。

除此之外,寫入操作相關的簡單介面還有io.ByteWriter和io.WriterAt。可惜,io包中也沒有它們的實現型別。不過,有一個資料型別值得在這裡提一句,那就是*os.File。

這個型別不但是io.WriterAt介面的實現型別,還同時實現了io.ReadWriteCloser介面和io.ReadWriteSeeker介面。也就是說,該型別支援的 I/O 操作非常的豐富。

io.Seeker介面作為一個讀寫位置設定相關的簡單介面,也僅僅定義了一個方法,名叫Seek。

我在講strings.Reader型別的時候還專門說過這個Seek方法,當時還給出了一個與已讀計數估算有關的例子。該方法主要用於尋找並設定下一次讀取或寫入時的起始索引位置。

io包中有幾個基於io.Seeker的擴充套件介面,包括前面講過的io.ReadSeeker和io.ReadWriteSeeker,以及還未曾提過的io.WriteSeeker。io.WriteSeeker是基於io.Writer和io.Seeker的擴充套件介面。

我們之前多次提到的兩個指標型別strings.Reader和io.SectionReader都實現了io.Seeker介面。順便說一句,這兩個型別也都是io.ReaderAt介面的實現型別。

最後,關閉操作相關的介面io.Closer非常通用,它的擴充套件介面和實現型別都不少。我們單從名稱上就能夠一眼看出io包中的哪些介面是它的擴充套件介面。至於它的實現型別,io包中只有io.PipeReader和io.PipeWriter。

package main

import (
	"io"
	"strings"
)

func main() {
	comment := "Because these interfaces and primitives wrap lower-level operations with various implementations, " +
		"unless otherwise informed clients should not assume they are safe for parallel execution."
	basicReader := strings.NewReader(comment)
	basicWriter := new(strings.Builder)

	// 示例1。
	reader1 := io.LimitReader(basicReader, 98)
	_ = interface{}(reader1).(io.Reader)

	// 示例2。
	reader2 := io.NewSectionReader(basicReader, 98, 89)
	_ = interface{}(reader2).(io.Reader)
	_ = interface{}(reader2).(io.ReaderAt)
	_ = interface{}(reader2).(io.Seeker)

	// 示例3。
	reader3 := io.TeeReader(basicReader, basicWriter)
	_ = interface{}(reader3).(io.Reader)

	// 示例4。
	reader4 := io.MultiReader(reader1)
	_ = interface{}(reader4).(io.Reader)

	// 示例5。
	writer1 := io.MultiWriter(basicWriter)
	_ = interface{}(writer1).(io.Writer)

	// 示例6。
	pReader, pWriter := io.Pipe()
	_ = interface{}(pReader).(io.Reader)
	_ = interface{}(pReader).(io.Closer)
	_ = interface{}(pWriter).(io.Writer)
	_ = interface{}(pWriter).(io.Closer)
}

總結

我們來總結一下這兩篇的內容。在 Go 語言中,對介面的擴充套件是通過介面型別之間的嵌入來實現的,這也常被叫做介面的組合。而io程式碼包恰恰就可以作為介面擴充套件的一個標杆,它可以成為我們運用這種技巧時的一個參考標準。

在本文中,我根據介面定義的方法的數量以及是否有介面嵌入,把io包中的介面分為了簡單介面和擴充套件介面。

同時,我又根據這些簡單介面的擴充套件介面和實現型別的數量級,把它們分為了核心介面和非核心介面。

在io包中,稱得上核心介面的簡單介面只有 3 個,即:io.Reader、io.Writer和io.Closer。這些核心介面在 Go 語言標準庫中的實現型別都在 200 個以上。

另外,根據針對的 I/O 操作的不同,我還把簡單介面分為了四大類。這四大類介面針對的操作分別是:讀取、寫入、關閉和讀寫位置設定。

其中,前三種操作屬於基本的 I/O 操作。基於此,我帶你梳理了每個類別的簡單介面,並講解了它們在io包中的擴充套件介面,以及具有代表性的實現型別。

image

( io 包中的介面體系)

除此之外,我還從多個維度為你描述了一些重要程式實體的功用和機理,比如:資料段讀取器io.SectionReader、作為同步記憶體管道核心實現的io.pipe型別,以及用於資料拷貝的io.CopyN函式,等等。

我如此詳盡且多角度的闡釋,正是為了讓你能夠記牢io程式碼包中有著網狀關係的介面和資料型別。我希望這個目的已經達到了,最起碼,本文可以作為你深刻記憶它們的開始。

最後再強調一下,io包中的簡單介面共有 11 個。其中,讀取操作相關的介面有 5 個,寫入操作相關的介面有 4 個,而與關閉操作有關的介面只有 1 個,另外還有一個讀寫位置設定相關的介面。

此外,io包還包含了 9 個基於這些簡單介面的擴充套件介面。你需要在今後思考和實踐的是,你在什麼時候應該編寫哪些資料型別實現io包中的哪些介面,並以此得到最大的好處。

思考題

今天的思考題是:io包中的同步記憶體管道的運作機制是什麼?

筆記原始碼

https://github.com/MingsonZheng/go-core-demo

知識共享許可協議

本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。

歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含連結: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。

相關文章