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

MingsonZheng發表於2021-11-30

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/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。

相關文章