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

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包中的同步內存管道的運作機制是什麼?

筆記源碼

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

知識共享許可協議

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

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