40 | io包中的接口和工具 (上)
我們在前幾篇文章中,主要討論了strings.Builder、strings.Reader和bytes.Buffer這三個數據類型。
知識回顧
還記得嗎?當時我還問過你“它們都實現了哪些接口”。在我們繼續講解io包中的接口和工具之前,我先來解答一下這個問題。
strings.Builder類型主要用於構建字符串,它的指針類型實現的接口有io.Writer、io.ByteWriter和fmt.Stringer。另外,它其實還實現了一個io包的包級私有接口io.stringWriter(自 Go 1.12 起它會更名為io.StringWriter)。
strings.Reader類型主要用於讀取字符串,它的指針類型實現的接口比較多,包括:
- io.Reader;
- io.ReaderAt;
- io.ByteReader;
- io.RuneReader;
- io.Seeker;
- io.ByteScanner;
- io.RuneScanner;
- io.WriterTo;
共有 8 個,它們都是io包中的接口。
其中,io.ByteScanner是io.ByteReader的擴展接口,而io.RuneScanner又是io.RuneReader的擴展接口。
bytes.Buffer是集讀、寫功能於一身的數據類型,它非常適合作為字節序列的緩沖區。 它的指針類型實現的接口就更多了。
更具體地說,該指針類型實現的讀取相關的接口有下面幾個。
- io.Reader;
- io.ByteReader;
- io.RuneReader;
- io.ByteScanner;
- io.RuneScanner;
- io.WriterTo;
共有 6 個。而其實現的寫入相關的接口則有這些。
- io.Writer;
- io.ByteWriter;
- io.stringWriter;
- io.ReaderFrom;
共 4 個。此外,它還實現了導出相關的接口fmt.Stringer。
前導內容:io 包中接口的好處與優勢
那么,這些類型實現了這么多的接口,其動機(或者說目的)究竟是什么呢?
簡單地說,這是為了提高不同程序實體之間的互操作性。遠的不說,我們就以io包中的一些函數為例。
在io包中,有這樣幾個用於拷貝數據的函數,它們是:
- io.Copy;
- io.CopyBuffer;
- io.CopyN。
雖然這幾個函數在功能上都略有差別,但是它們都首先會接受兩個參數,即:用於代表數據目的地、io.Writer類型的參數dst,以及用於代表數據來源的、io.Reader類型的參數src。這些函數的功能大致上都是把數據從src拷貝到dst。
不論我們給予它們的第一個參數值是什么類型的,只要這個類型實現了io.Writer接口即可。
同樣的,無論我們傳給它們的第二個參數值的實際類型是什么,只要該類型實現了io.Reader接口就行。
一旦我們滿足了這兩個條件,這些函數幾乎就可以正常地執行了。當然了,函數中還會對必要的參數值進行有效性的檢查,如果檢查不通過,它的執行也是不能夠成功結束的。
下面來看一段示例代碼:
src := strings.NewReader(
"CopyN copies n bytes (or until an error) from src to dst. " +
"It returns the number of bytes copied and " +
"the earliest error encountered while copying.")
dst := new(strings.Builder)
written, err := io.CopyN(dst, src, 58)
if err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Printf("Written(%d): %q\n", written, dst.String())
}
我先使用strings.NewReader創建了一個字符串讀取器,並把它賦給了變量src,然后我又new了一個字符串構建器,並將其賦予了變量dst。
之后,我在調用io.CopyN函數的時候,把這兩個變量的值都傳了進去,同時把給這個函數的第三個參數值設定為了58。也就是說,我想從src中拷貝前58個字節到dst那里。
雖然,變量src和dst的類型分別是strings.Reader和strings.Builder,但是當它們被傳到io.CopyN函數的時候,就已經分別被包裝成了io.Reader類型和io.Writer類型的值。io.CopyN函數也根本不會去在意,它們的實際類型到底是什么。
為了優化的目的,io.CopyN函數中的代碼會對參數值進行再包裝,也會檢測這些參數值是否還實現了別的接口,甚至還會去探求某個參數值被包裝后的實際類型,是否為某個特殊的類型。
但是,從總體上來看,這些代碼都是面向參數聲明中的接口來做的。io.CopyN函數的作者通過面向接口編程,極大地拓展了它的適用范圍和應用場景。
換個角度看,正因為strings.Reader類型和strings.Builder類型都實現了不少接口,所以它們的值才能夠被使用在更廣闊的場景中。
換句話說,如此一來,Go 語言的各種庫中,能夠操作它們的函數和數據類型明顯多了很多。
這就是我想要告訴你的,strings包和bytes包中的數據類型在實現了若干接口之后得到的最大好處。
也可以說,這就是面向接口編程帶來的最大優勢。這些數據類型和函數的做法,也是非常值得我們在編程的過程中去效仿的。
可以看到,前文所述的幾個類型實現的大都是io代碼包中的接口。實際上,io包中的接口,對於 Go 語言的標准庫和很多第三方庫而言,都起着舉足輕重的作用。它們非常基礎也非常重要。
就拿io.Reader和io.Writer這兩個最核心的接口來說,它們是很多接口的擴展對象和設計源泉。同時,單從 Go 語言的標准庫中統計,實現了它們的數據類型都(各自)有上百個,而引用它們的代碼更是都(各自)有 400 多處。
很多數據類型實現了io.Reader接口,是因為它們提供了從某處讀取數據的功能。類似的,許多能夠把數據寫入某處的數據類型,也都會去實現io.Writer接口。
其實,有不少類型的設計初衷都是:實現這兩個核心接口的某個,或某些擴展接口,以提供比單純的字節序列讀取或寫入,更加豐富的功能,就像前面講到的那幾個strings包和bytes包中的數據類型那樣。
在 Go 語言中,對接口的擴展是通過接口類型之間的嵌入來實現的,這也常被叫做接口的組合。
我在講接口的時候也提到過,Go 語言提倡使用小接口加接口組合的方式,來擴展程序的行為以及增加程序的靈活性。io代碼包恰恰就可以作為這樣的一個標桿,它可以成為我們運用這種技巧時的一個參考標准。
package main
import (
"bytes"
"fmt"
"io"
"strings"
)
func main() {
// 示例1。
builder := new(strings.Builder)
_ = interface{}(builder).(io.Writer)
_ = interface{}(builder).(io.ByteWriter)
_ = interface{}(builder).(fmt.Stringer)
// 示例2。
reader := strings.NewReader("")
_ = interface{}(reader).(io.Reader)
_ = interface{}(reader).(io.ReaderAt)
_ = interface{}(reader).(io.ByteReader)
_ = interface{}(reader).(io.RuneReader)
_ = interface{}(reader).(io.Seeker)
_ = interface{}(reader).(io.ByteScanner)
_ = interface{}(reader).(io.RuneScanner)
_ = interface{}(reader).(io.WriterTo)
// 示例3。
buffer := bytes.NewBuffer([]byte{})
_ = interface{}(buffer).(io.Reader)
_ = interface{}(buffer).(io.ByteReader)
_ = interface{}(buffer).(io.RuneReader)
_ = interface{}(buffer).(io.ByteScanner)
_ = interface{}(buffer).(io.RuneScanner)
_ = interface{}(buffer).(io.WriterTo)
_ = interface{}(buffer).(io.Writer)
_ = interface{}(buffer).(io.ByteWriter)
_ = interface{}(buffer).(io.ReaderFrom)
_ = interface{}(buffer).(fmt.Stringer)
// 示例4。
src := strings.NewReader(
"CopyN copies n bytes (or until an error) from src to dst. " +
"It returns the number of bytes copied and " +
"the earliest error encountered while copying.")
dst := new(strings.Builder)
written, err := io.CopyN(dst, src, 58)
if err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Printf("Written(%d): %q\n", written, dst.String())
}
}
下面,我就以io.Reader接口為對象提出一個與接口擴展和實現有關的問題。如果你研究過這個核心接口以及相關的數據類型的話,這個問題回答起來就並不困難。
我們今天的問題是:在io包中,io.Reader的擴展接口和實現類型都有哪些?它們分別都有什么功用?
這道題的典型回答是這樣的。在io包中,io.Reader的擴展接口有下面幾種。
1、io.ReadWriter:此接口既是io.Reader的擴展接口,也是io.Writer的擴展接口。換句話說,該接口定義了一組行為,包含且僅包含了基本的字節序列讀取方法Read,和字節序列寫入方法Write。
2、io.ReadCloser:此接口除了包含基本的字節序列讀取方法之外,還擁有一個基本的關閉方法Close。后者一般用於關閉數據讀寫的通路。這個接口其實是io.Reader接口和io.Closer接口的組合。
3、io.ReadWriteCloser:很明顯,此接口是io.Reader、io.Writer和io.Closer這三個接口的組合。
4、io.ReadSeeker:此接口的特點是擁有一個用於尋找讀寫位置的基本方法Seek。更具體地說,該方法可以根據給定的偏移量基於數據的起始位置、末尾位置,或者當前讀寫位置去尋找新的讀寫位置。這個新的讀寫位置用於表明下一次讀或寫時的起始索引。Seek是io.Seeker接口唯一擁有的方法。
5、io.ReadWriteSeeker:顯然,此接口是另一個三合一的擴展接口,它是io.Reader、io.Writer和io.Seeker的組合。
再來說說io包中的io.Reader接口的實現類型,它們包括下面幾項內容。
1、*io.LimitedReader:此類型的基本類型會包裝io.Reader類型的值,並提供一個額外的受限讀取的功能。所謂的受限讀取指的是,此類型的讀取方法Read返回的總數據量會受到限制,無論該方法被調用多少次。這個限制由該類型的字段N指明,單位是字節。
2、*io.SectionReader:此類型的基本類型可以包裝io.ReaderAt類型的值,並且會限制它的Read方法,只能夠讀取原始數據中的某一個部分(或者說某一段)。這個數據段的起始位置和末尾位置,需要在它被初始化的時候就指明,並且之后無法變更。該類型值的行為與切片有些類似,它只會對外暴露在其窗口之中的那些數據。
3、*io.teeReader:此類型是一個包級私有的數據類型,也是io.TeeReader函數結果值的實際類型。這個函數接受兩個參數r和w,類型分別是io.Reader和io.Writer。其結果值的Read方法會把r中的數據經過作為方法參數的字節切片p寫入到w。可以說,這個值就是r和w之間的數據橋梁,而那個參數p就是這座橋上的數據搬運者。
4、*io.multiReader:此類型也是一個包級私有的數據類型。類似的,io包中有一個名為MultiReader的函數,它可以接受若干個io.Reader類型的參數值,並返回一個實際類型為io.multiReader的結果值。當這個結果值的Read方法被調用時,它會順序地從前面那些io.Reader類型的參數值中讀取數據。因此,我們也可以稱之為多對象讀取器。
5、io.pipe:此類型為一個包級私有的數據類型,它比上述類型都要復雜得多。它不但實現了io.Reader接口,而且還實現了io.Writer接口。實際上,io.PipeReader類型和io.PipeWriter類型擁有的所有指針方法都是以它為基礎的。這些方法都只是代理了io.pipe類型值所擁有的某一個方法而已。又因為io.Pipe函數會返回這兩個類型的指針值並分別把它們作為其生成的同步內存管道的兩端,所以可以說,io.pipe類型就是io包提供的同步內存管道的核心實現。
6、*io.PipeReader:此類型可以被視為io.pipe類型的代理類型。它代理了后者的一部分功能,並基於后者實現了io.ReadCloser接口。同時,它還定義了同步內存管道的讀取端。
注意,我在這里忽略掉了測試源碼文件中的實現類型,以及不會以任何形式直接對外暴露的那些實現類型。
問題解析
我問這個問題的目的主要是評估你對io包的熟悉程度。這個代碼包是 Go 語言標准庫中所有 I/O 相關 API 的根基,所以,我們必須對其中的每一個程序實體都有所了解。
然而,由於該包包含的內容眾多,因此這里的問題是以io.Reader接口作為切入點的。通過io.Reader接口,我們應該能夠梳理出基於它的類型樹,並知曉其中每一個類型的功用。
io.Reader可謂是io包乃至是整個 Go 語言標准庫中的核心接口,所以我們可以從它那里牽扯出很多擴展接口和實現類型。
我在本問題的典型回答中,為你羅列和介紹了io包范圍內的相關數據類型。
這些類型中的每一個都值得你認真去理解,尤其是那幾個實現了io.Reader接口的類型。它們實現的功能在細節上都各有不同。
在很多時候,我們可以根據實際需求將它們搭配起來使用。
例如,對施加在原始數據之上的(由Read方法提供的)讀取功能進行多層次的包裝(比如受限讀取和多對象讀取等),以滿足較為復雜的讀取需求。
在實際的面試中,只要應聘者能夠從某一個方面出發,說出io.Reader的擴展接口及其存在意義,或者說清楚該接口的三五個實現類型,那么就可以算是基本回答正確了。
比如,從讀取、寫入、關閉這一系列的基本功能出發,描述清楚:io.ReadWriter;io.ReadCloser;io.ReadWriteCloser;這幾個接口。
- io.ReadWriter;
- io.ReadCloser;
- io.ReadWriteCloser;
這幾個接口。
又比如,說明白io.LimitedReader和io.SectionReader這兩個類型之間的異同點。
再比如,闡述*io.SectionReader類型實現io.ReadSeeker接口的具體方式,等等。不過,這只是合格的門檻,應聘者回答得越全面越好。
我在示例文件 demo82.go 中寫了一些代碼,以展示上述類型的一些基本用法,供你參考。
package main
import (
"fmt"
"io"
"strings"
"sync"
"time"
)
func main() {
comment := "Package io provides basic interfaces to I/O primitives. " +
"Its primary job is to wrap existing implementations of such primitives, " +
"such as those in package os, " +
"into shared public interfaces that abstract the functionality, " +
"plus some other related primitives."
// 示例1。
fmt.Println("New a string reader and name it \"reader1\" ...")
reader1 := strings.NewReader(comment)
buf1 := make([]byte, 7)
n, err := reader1.Read(buf1)
var offset1, index1 int64
executeIfNoErr(err, func() {
fmt.Printf("Read(%d): %q\n", n, buf1[:n])
offset1 = int64(53)
index1, err = reader1.Seek(offset1, io.SeekCurrent)
})
executeIfNoErr(err, func() {
fmt.Printf("The new index after seeking from current with offset %d: %d\n",
offset1, index1)
n, err = reader1.Read(buf1)
})
executeIfNoErr(err, func() {
fmt.Printf("Read(%d): %q\n", n, buf1[:n])
})
fmt.Println()
// 示例2。
reader1.Reset(comment)
num1 := int64(7)
fmt.Printf("New a limited reader with reader1 and number %d ...\n", num1)
reader2 := io.LimitReader(reader1, 7)
buf2 := make([]byte, 10)
for i := 0; i < 3; i++ {
n, err = reader2.Read(buf2)
executeIfNoErr(err, func() {
fmt.Printf("Read(%d): %q\n", n, buf2[:n])
})
}
fmt.Println()
// 示例3。
reader1.Reset(comment)
offset2 := int64(56)
num2 := int64(72)
fmt.Printf("New a section reader with reader1, offset %d and number %d ...\n", offset2, num2)
reader3 := io.NewSectionReader(reader1, offset2, num2)
buf3 := make([]byte, 20)
for i := 0; i < 5; i++ {
n, err = reader3.Read(buf3)
executeIfNoErr(err, func() {
fmt.Printf("Read(%d): %q\n", n, buf3[:n])
})
}
fmt.Println()
// 示例4。
reader1.Reset(comment)
writer1 := new(strings.Builder)
fmt.Println("New a tee reader with reader1 and writer1 ...")
reader4 := io.TeeReader(reader1, writer1)
buf4 := make([]byte, 40)
for i := 0; i < 8; i++ {
n, err = reader4.Read(buf4)
executeIfNoErr(err, func() {
fmt.Printf("Read(%d): %q\n", n, buf4[:n])
})
}
fmt.Println()
// 示例5。
reader5a := strings.NewReader(
"MultiReader returns a Reader that's the logical concatenation of " +
"the provided input readers.")
reader5b := strings.NewReader("They're read sequentially.")
reader5c := strings.NewReader("Once all inputs have returned EOF, " +
"Read will return EOF.")
reader5d := strings.NewReader("If any of the readers return a non-nil, " +
"non-EOF error, Read will return that error.")
fmt.Println("New a multi-reader with 4 readers ...")
reader5 := io.MultiReader(reader5a, reader5b, reader5c, reader5d)
buf5 := make([]byte, 50)
for i := 0; i < 8; i++ {
n, err = reader5.Read(buf5)
executeIfNoErr(err, func() {
fmt.Printf("Read(%d): %q\n", n, buf5[:n])
})
}
fmt.Println()
// 示例6。
fmt.Println("New a synchronous in-memory pipe ...")
pReader, pWriter := io.Pipe()
_ = interface{}(pReader).(io.ReadCloser)
_ = interface{}(pWriter).(io.WriteCloser)
comments := [][]byte{
[]byte("Pipe creates a synchronous in-memory pipe."),
[]byte("It can be used to connect code expecting an io.Reader "),
[]byte("with code expecting an io.Writer."),
}
// 這里添加這個同步工具純屬為了保證下面示例中的打印語句都能夠執行完成。
// 在實際使用中沒有必要這樣做。
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for _, d := range comments {
time.Sleep(time.Millisecond * 500)
n, err := pWriter.Write(d)
if err != nil {
fmt.Printf("write error: %v\n", err)
break
}
fmt.Printf("Written(%d): %q\n", n, d)
}
pWriter.Close()
}()
go func() {
defer wg.Done()
wBuf := make([]byte, 55)
for {
n, err := pReader.Read(wBuf)
if err != nil {
fmt.Printf("read error: %v\n", err)
break
}
fmt.Printf("Read(%d): %q\n", n, wBuf[:n])
}
}()
wg.Wait()
}
func executeIfNoErr(err error, f func()) {
if err != nil {
fmt.Printf("error: %v\n", err)
return
}
f()
}
總結
我們今天一直在討論和梳理io代碼包中的程序實體,尤其是那些重要的接口及其實現類型。
io包中的接口對於 Go 語言的標准庫和很多第三方庫而言,都起着舉足輕重的作用。其中最核心的io.Reader接口和io.Writer接口,是很多接口的擴展對象或設計源泉。我們下一節會繼續講解io包中的接口內容。
筆記源碼
https://github.com/MingsonZheng/go-core-demo
本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。
歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含鏈接: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改后的作品務必以相同的許可發布。