Go語(yǔ)言 接口 - 通過(guò)包裹不同具體類型的非接口值來(lái)實(shí)現(xiàn)反射和多態(tài)

2023-02-16 17:38 更新

接口類型是Go中的一種很特別的類型。接口類型在Go中扮演著重要的角色。 首先,在Go中,接口值可以用來(lái)包裹非接口值;然后,通過(guò)值包裹,反射和多態(tài)得以實(shí)現(xiàn)。

自從1.18版本開始,Go已經(jīng)支持自定義泛型。 在自定義泛型中,接口類型總可以被用做類型約束。 事實(shí)上,所有的類型約束都是接口類型。 在Go 1.18版本之前,所有的接口類型均可用做值類型。 但是從Go 1.18版本開始,有些接口類型只能被用做類型約束。 可被用做值類型的接口類型稱為基本接口類型。

本文大體是在Go支持自定義泛型之前寫成的,所以本文主要講述基本接口類型。 關(guān)于非基本接口類型(只能用做類型約束的接口類型),請(qǐng)閱讀《Go自定義泛型101》一書以了解詳情。

接口類型介紹和類型集(Type Set)

一個(gè)接口類型定義了一些類型條件。 所有滿足了全部這些條件的非接口類型形成了一個(gè)類型集合。 此類型集合稱為此接口類型的類型集。

接口類型是通過(guò)內(nèi)嵌若干接口元素來(lái)定義類型條件的。 目前(Go 1.19)支持兩種接口元素:方法元素和類型元素。

  • 一個(gè)方法元素呈現(xiàn)為一個(gè)方法描述(method specification)。 內(nèi)嵌在接口類型中的方法描述不能使用空標(biāo)識(shí)符_命名。
  • 一個(gè)類型元素可以是一個(gè)類型名稱、一個(gè)類型字面表示形式、一個(gè)近似類型或者一個(gè)類型并集。 本文不過(guò)多介紹后兩者。對(duì)于前兩者,也只談及當(dāng)它們表示接口類型的情況。

舉個(gè)例子,預(yù)聲明的error接口類型的定義如下。 它內(nèi)嵌了一個(gè)方法描述Error() string。 在此定義中,interface{...}稱為接口類型的字面表示形式,其中interface為一個(gè)關(guān)鍵字。

type error interface {
        Error() string
}

我們可以說(shuō)此error接口類型(直接)指定了一個(gè)方法(描述):Error() string。 它的類型集由所有擁有此同樣描述的方法的非接口類型組成。 理論上,此類型集是一個(gè)無(wú)限集。當(dāng)然對(duì)于一個(gè)具體的Go項(xiàng)目,此集合是有限的。

下面是一些其它接口類型定義和別名聲明。

// 此接口直接指定了兩個(gè)方法和內(nèi)嵌了兩個(gè)其它接口。
// 其中一個(gè)為類型名稱,另一個(gè)為類型字面表示形式。
type ReadWriteCloser = interface {
	Read(buf []byte) (n int, err error)
	Write(buf []byte) (n int, err error)
	error                      // 一個(gè)類型名稱
	interface{ Close() error } // 一個(gè)類型字面表示形式
}

// 此接口類型內(nèi)嵌了一個(gè)近似類型。
// 它的類型集由所有底層類型為[]byte的類型組成。
type AnyByteSlice = interface {
	~[]byte
}

// 此接口類型內(nèi)嵌了一個(gè)類型并集。它的類型集包含6個(gè)類型:
// uint、uint8、uint16、uint32、uint64和uintptr。
type Unsigned interface {
	uint | uint8 | uint16 | uint32 | uint64 | uintptr
}

將一個(gè)接口類型(無(wú)論呈現(xiàn)為類型名稱還是類型字面表示形式)內(nèi)嵌到另一個(gè)接口類型中等價(jià)于將前者中的元素(遞歸)展開放入后者。 比如,別名ReadWriteCloser表示的接口類型等價(jià)于下面這個(gè)類型字面表示形式表示的直接指定了4個(gè)方法的接口類型。

interface {
	Read(buf []byte) (n int, err error)
	Write(buf []byte) (n int, err error)
	Error() string
	Close() error
}

上面這個(gè)接口類型(即別名ReadWriteCloser表示的接口類型)的類型集由所有擁有全部這4個(gè)指定方法的非接口類型組成。 從理論上,這也是一個(gè)無(wú)限集。它肯定是error接口類型的類型集的子集。

請(qǐng)注意:在Go 1.18之前,只有接口類型名稱可以內(nèi)嵌在接口類型中。

下面的代碼片段中展示的接口類型都稱為空接口類型。它們什么也沒(méi)有內(nèi)嵌。

// 一個(gè)無(wú)名空接口類型。
interface{}
	
// Nothing是一個(gè)定義空接口類型。
type Nothing interface{}

事實(shí)上,Go 1.18引入了一個(gè)預(yù)聲明的類型別名any,用來(lái)表示空接口類型interface{}

一個(gè)空接口類型的類型集由所有由非接口類型組成。

類型的方法集

每個(gè)類型有一個(gè)方法集。

  • 對(duì)于一個(gè)非接口類型,它的方法集由為此類型(無(wú)論顯式還是隱式)聲明 所有方法的方法描述組成。
  • 對(duì)于一個(gè)接口類型,它的方法集由此接口類型(無(wú)論直接還是間接)指定的所有方法描述組成。

對(duì)于上一節(jié)中提到的接口類型,

  • 別名ReadWriteCloser表示的接口類型的方法集包含4個(gè)方法(描述)。
  • 預(yù)聲明的error接口類型的方法集包含一個(gè)方法(描述)。
  • 一個(gè)空接口類型的方法集為空。

為了方便起見,一個(gè)類型的方法集常常也稱為此方法的任何一個(gè)值的方法集。

基本接口類型

基本接口類型是指可以用做值類型的接口類型。 一個(gè)非基本接口類型只能為用做(自定義泛型中使用的)約束接口類型(即類型約束)。

目前(Go 1.19),每一個(gè)基本接口類型都可以使用一個(gè)方法集來(lái)完全定義。 換句話說(shuō),一個(gè)基本接口類型不需要內(nèi)嵌任何類型元素。

在上上一節(jié)中的例子中,別名ReadWriteCloser表示的接口類型為一個(gè)基本接口類型, 但是Unsigned接口類型和別名AnyByteSlice表示的接口類型均不是基本接口類型。 后兩者均只能用做約束接口類型。

空接口類型和預(yù)聲明的error接口類型也都是基本接口類型。

如果兩個(gè)無(wú)名基本接口類型的方法集是相同的,則這兩個(gè)類型肯定為同一個(gè)類型。 但是請(qǐng)注意:不同代碼包中的同名非導(dǎo)出方法名將總被認(rèn)為是不同名的。

類型實(shí)現(xiàn)(implementation)

如果一個(gè)非接口類型處于一個(gè)接口類型的類型集中,則我們說(shuō)此非接口類型實(shí)現(xiàn)了此接口類型。 如果一個(gè)接口類型的類型集是另一個(gè)接口類型的類型集的子集,則我們說(shuō)前者實(shí)現(xiàn)了后者。

因?yàn)橐粋€(gè)類型集的總是它自己的子集,一個(gè)接口類型總是實(shí)現(xiàn)了它自己。 類似地,如果兩個(gè)接口類型的類型集相同,則它們相互實(shí)現(xiàn)了對(duì)方。 事實(shí)上,兩個(gè)擁有相同類型集的無(wú)名接口類型為同一個(gè)接口類型。

如果一個(gè)(接口或者非接口)類型T實(shí)現(xiàn)了一個(gè)接口類型X,那么類型T的方法集肯定是接口類型X的方法集的超集。 一般說(shuō)來(lái),反之并不成立。但是如果X是一個(gè)基本接口類型,則反之也成立。 比如,在前面的例子中,別名ReadWriteCloser表示的接口類型實(shí)現(xiàn)了預(yù)聲明的error接口類型。

在Go中,實(shí)現(xiàn)關(guān)系是隱式的。 兩個(gè)類型之間的實(shí)現(xiàn)關(guān)系不需要在代碼中顯式地表示出來(lái)。 Go中沒(méi)有類似于implements的關(guān)鍵字。 Go編譯器將自動(dòng)在需要的時(shí)候檢查兩個(gè)類型之間的實(shí)現(xiàn)關(guān)系。

比如,在下面的例子中,類型*Book、MyInt*MyInt都擁有一個(gè)描述為About() string的方法,所以它們都實(shí)現(xiàn)了接口類型Aboutable

type Aboutable interface {
	About() string
}

type Book struct {
	name string
	// 更多其它字段……
}

func (book *Book) About() string {
	return "Book: " + book.name
}

type MyInt int

func (MyInt) About() string {
	return "我是一個(gè)自定義整數(shù)類型"
}

隱式實(shí)現(xiàn)關(guān)系的設(shè)計(jì)使得一個(gè)聲明在另一個(gè)代碼包(包括標(biāo)準(zhǔn)庫(kù)包)中的類型可以被動(dòng)地實(shí)現(xiàn)一個(gè)用戶代碼包中的接口類型。 比如,如果我們聲明一個(gè)像下面這樣的接口類型,則database/sql標(biāo)準(zhǔn)庫(kù)包中聲明的DBTx類型都實(shí)現(xiàn)了這個(gè)接口類型,因?yàn)樗鼈兌紦碛写私涌陬愋椭付ǖ娜齻€(gè)方法。

import "database/sql"

...

type DatabaseStorer interface {
	Exec(query string, args ...interface{}) (sql.Result, error)
	Prepare(query string) (*sql.Stmt, error)
	Query(query string, args ...interface{}) (*sql.Rows, error)
}

注意:因?yàn)榭战涌陬愋偷念愋图怂械姆墙涌陬愋?,所以所有類型均?shí)現(xiàn)了空接口類型。 這是Go中的一個(gè)重要事實(shí)。

值包裹

重申一遍:目前(Go 1.19),接口值的類型必須為一個(gè)基本接口類型。 在本文余下的內(nèi)容里,當(dāng)一個(gè)值類型被提及,此值類型可能是一個(gè)非接口類型,也可能是一個(gè)基本接口類型,但它肯定不是一個(gè)非基本接口類型。

每個(gè)接口值都可以看作是一個(gè)用來(lái)包裹一個(gè)非接口值的盒子。 欲將一個(gè)非接口值包裹在一個(gè)接口值中,此非接口值的類型必須實(shí)現(xiàn)了此接口值的類型。

在Go中,如果類型T實(shí)現(xiàn)了一個(gè)(基本)接口類型I,則類型T的值都可以隱式轉(zhuǎn)換到類型I。 換句話說(shuō),類型T的值可以賦給類型I的可修改值。 當(dāng)一個(gè)T值被轉(zhuǎn)換到類型I(或者賦給一個(gè)I值)的時(shí)候,

  • 如果類型T是一個(gè)非接口類型,則此T值的一個(gè)復(fù)制將被包裹在結(jié)果(或者目標(biāo))I值中。 此操作的時(shí)間復(fù)雜度為O(n),其中nT值的尺寸。
  • 如果類型T也為一個(gè)接口類型,則此T值中當(dāng)前包裹的(非接口)值將被復(fù)制一份到結(jié)果(或者目標(biāo))I值中。 官方標(biāo)準(zhǔn)編譯器為此操作做了優(yōu)化,使得此操作的時(shí)間復(fù)雜度為O(1),而不是O(n)。

包裹在一個(gè)接口值中的非接口值的類型信息也和此非接口值一起被包裹在此接口值中(見下面詳解)。

當(dāng)一個(gè)非接口值被包裹在一個(gè)接口值中,此非接口值稱為此接口值的動(dòng)態(tài)值,此非接口值的類型稱為此接口值的動(dòng)態(tài)類型。

接口值的動(dòng)態(tài)值的直接部分是不可修改的,除非它的動(dòng)態(tài)值被整體替換為另一個(gè)動(dòng)態(tài)值。

接口類型的零值也用預(yù)聲明的nil標(biāo)識(shí)符來(lái)表示。 一個(gè)nil接口值中什么也沒(méi)包裹。將一個(gè)接口值修改為nil將清空包裹在此接口值中的非接口值。

(注意,在Go中,很多其它非接口類型的零值也使用nil標(biāo)識(shí)符來(lái)表示。 非接口類型的nil零值也可以被包裹在接口值中。 一個(gè)包裹了一個(gè)nil非接口值的接口值不是一個(gè)nil接口值,因?yàn)樗⒎鞘裁炊紱](méi)包裹。)

因?yàn)槿魏晤愋投紝?shí)現(xiàn)了空接口類型,所以任何非接口值都可以被包裹在任何一個(gè)空接口類型的接口值中。 (以后,一個(gè)空接口類型的接口值將被稱為一個(gè)空接口值。注意空接口值和nil接口值是兩個(gè)不同的概念。) 因?yàn)檫@個(gè)原因,空接口值可以被認(rèn)為是很多其它語(yǔ)言中的any類型。

當(dāng)一個(gè)類型不確定值(除了類型不確定的nil)被轉(zhuǎn)換為一個(gè)空接口類型(或者賦給一個(gè)空接口值),此類型不確定值將首先轉(zhuǎn)換為它的默認(rèn)類型。 (或者說(shuō),此類型不確定值將被推斷為一個(gè)它的默認(rèn)類型的類型確定值。)

下面這個(gè)例子展示了一些目標(biāo)值為接口類型的賦值。

package main

import "fmt"

type Aboutable interface {
	About() string
}

// 類型*Book實(shí)現(xiàn)了接口類型Aboutable。
type Book struct {
	name string
}
func (book *Book) About() string {
	return "Book: " + book.name
}

func main() {
	// 一個(gè)*Book值被包裹在了一個(gè)Aboutable值中。
	var a Aboutable = &Book{"Go語(yǔ)言101"}
	fmt.Println(a) // &{Go語(yǔ)言101}

	// i是一個(gè)空接口值。類型*Book實(shí)現(xiàn)了任何空接口類型。
	var i interface{} = &Book{"Rust 101"}
	fmt.Println(i) // &{Rust 101}

	// Aboutable實(shí)現(xiàn)了空接口類型interface{}。
	i = a
	fmt.Println(i) // &{Go語(yǔ)言101}
}

請(qǐng)注意,在之前的文章中多次使用過(guò)的fmt.Println函數(shù)的原型為:

func Println(a ...interface{}) (n int, err error)

這解釋了為什么任何類型的實(shí)參都可以使用在fmt.Println函數(shù)調(diào)用中。

下面是另一個(gè)展示了一個(gè)空接口類型的值包裹著各種非接口值的例子:

package main

import "fmt"

func main() {
	var i interface{}
	i = []int{1, 2, 3}
	fmt.Println(i) // [1 2 3]
	i = map[string]int{"Go": 2012}
	fmt.Println(i) // map[Go:2012]
	i = true
	fmt.Println(i) // true
	i = 1
	fmt.Println(i) // 1
	i = "abc"
	fmt.Println(i) // abc

	// 將接口值i中包裹的值清除掉。
	i = nil
	fmt.Println(i) // <nil>
}

在編譯時(shí)刻,Go編譯器將構(gòu)建一個(gè)全局表用來(lái)存儲(chǔ)代碼中要用到的各個(gè)類型的信息。 對(duì)于一個(gè)類型來(lái)說(shuō),這些信息包括:此類型的種類(kind)、此類型的所有方法和字段信息、此類型的尺寸,等等。 這個(gè)全局表將在程序啟動(dòng)的時(shí)候被加載到內(nèi)存中。

在運(yùn)行時(shí)刻,當(dāng)一個(gè)非接口值被包裹到一個(gè)接口值,Go運(yùn)行時(shí)(至少對(duì)于官方標(biāo)準(zhǔn)運(yùn)行時(shí)來(lái)說(shuō))將分析和構(gòu)建這兩個(gè)值的類型的實(shí)現(xiàn)關(guān)系信息,并將此實(shí)現(xiàn)關(guān)系信息存入到此接口值內(nèi)。 對(duì)每一對(duì)這樣的類型,它們的實(shí)現(xiàn)關(guān)系信息將僅被最多構(gòu)建一次。并且為了程序效率考慮,此實(shí)現(xiàn)關(guān)系信息將被緩存在內(nèi)存中的一個(gè)全局映射中,以備后用。 所以此全局映射中的條目數(shù)永不減少。 事實(shí)上,一個(gè)非零接口值在內(nèi)部只是使用一個(gè)指針字段來(lái)引用著此全局映射中的一個(gè)實(shí)現(xiàn)關(guān)系信息條目。

對(duì)于一個(gè)非接口類型和接口類型對(duì),它們的實(shí)現(xiàn)關(guān)系信息包括兩部分的內(nèi)容:

  1. 動(dòng)態(tài)類型(即此非接口類型)的信息。
  2. 一個(gè)方法表(切片類型),其中存儲(chǔ)了所有此接口類型指定的并且為此非接口類型(動(dòng)態(tài)類型)聲明的方法。

這兩部分的內(nèi)容對(duì)于實(shí)現(xiàn)Go中的兩個(gè)特性起著至關(guān)重要的作用。

  1. 動(dòng)態(tài)類型信息是實(shí)現(xiàn)反射的關(guān)鍵。
  2. 方法表是實(shí)現(xiàn)多態(tài)(見下一節(jié))的關(guān)鍵。

多態(tài)(polymorphism)

多態(tài)是接口的一個(gè)關(guān)鍵功能和Go語(yǔ)言的一個(gè)重要特性。

當(dāng)非接口類型T的一個(gè)值t被包裹在接口類型I的一個(gè)接口值i中,通過(guò)i調(diào)用接口類型I指定的一個(gè)方法時(shí),事實(shí)上為非接口類型T聲明的對(duì)應(yīng)方法將通過(guò)非接口值t被調(diào)用。 換句話說(shuō),調(diào)用一個(gè)接口值的方法實(shí)際上將調(diào)用此接口值的動(dòng)態(tài)值的對(duì)應(yīng)方法。 比如,當(dāng)方法i.m被調(diào)用時(shí),其實(shí)被調(diào)用的是方法t.m。 一個(gè)接口值可以通過(guò)包裹不同動(dòng)態(tài)類型的動(dòng)態(tài)值來(lái)表現(xiàn)出各種不同的行為,這稱為多態(tài)。

當(dāng)方法i.m被調(diào)用時(shí),i存儲(chǔ)的實(shí)現(xiàn)關(guān)系信息的方法表中的方法t.m將被找到并被調(diào)用。 此方法表是一個(gè)切片,所以此尋找過(guò)程只不過(guò)是一個(gè)切片元素訪問(wèn)操作,不會(huì)消耗很多時(shí)間。

注意,在nil接口值上調(diào)用方法將產(chǎn)生一個(gè)恐慌,因?yàn)闆](méi)有具體的方法可被調(diào)用。

一個(gè)例子:

package main

import "fmt"

type Filter interface {
	About() string
	Process([]int) []int
}

// UniqueFilter用來(lái)刪除重復(fù)的數(shù)字。
type UniqueFilter struct{}
func (UniqueFilter) About() string {
	return "刪除重復(fù)的數(shù)字"
}
func (UniqueFilter) Process(inputs []int) []int {
	outs := make([]int, 0, len(inputs))
	pusheds := make(map[int]bool)
	for _, n := range inputs {
		if !pusheds[n] {
			pusheds[n] = true
			outs = append(outs, n)
		}
	}
	return outs
}

// MultipleFilter用來(lái)只保留某個(gè)整數(shù)的倍數(shù)數(shù)字。
type MultipleFilter int
func (mf MultipleFilter) About() string {
	return fmt.Sprintf("保留%v的倍數(shù)", mf)
}
func (mf MultipleFilter) Process(inputs []int) []int {
	var outs = make([]int, 0, len(inputs))
	for _, n := range inputs {
		if n % int(mf) == 0 {
			outs = append(outs, n)
		}
	}
	return outs
}

// 在多態(tài)特性的幫助下,只需要一個(gè)filteAndPrint函數(shù)。
func filteAndPrint(fltr Filter, unfiltered []int) []int {
	// 在fltr參數(shù)上調(diào)用方法其實(shí)是調(diào)用fltr的動(dòng)態(tài)值的方法。
	filtered := fltr.Process(unfiltered)
	fmt.Println(fltr.About() + ":\n\t", filtered)
	return filtered
}

func main() {
	numbers := []int{12, 7, 21, 12, 12, 26, 25, 21, 30}
	fmt.Println("過(guò)濾之前:\n\t", numbers)

	// 三個(gè)非接口值被包裹在一個(gè)Filter切片
	// 的三個(gè)接口元素中。
	filters := []Filter{
		UniqueFilter{},
		MultipleFilter(2),
		MultipleFilter(3),
	}

	// 每個(gè)切片元素將被賦值給類型為Filter的
	// 循環(huán)變量fltr。每個(gè)元素中的動(dòng)態(tài)值也將
	// 被同時(shí)復(fù)制并被包裹在循環(huán)變量fltr中。
	for _, fltr := range filters {
		numbers = filteAndPrint(fltr, numbers)
	}
}

輸出結(jié)果:

過(guò)濾之前:
	 [12 7 21 12 12 26 25 21 30]
刪除重復(fù)的數(shù)字:
	 [12 7 21 26 25 30]
保留2的倍數(shù):
	 [12 26 30]
保留3的倍數(shù):
	 [12 30]

在上面這個(gè)例子中,多態(tài)使得我們不必為每個(gè)過(guò)濾器類型寫一個(gè)單獨(dú)的filteAndPrint函數(shù)。

除了上述這個(gè)好處,多態(tài)也使得一個(gè)代碼包的開發(fā)者可以在此代碼包中聲明一個(gè)接口類型并聲明一個(gè)擁有此接口類型參數(shù)的函數(shù)(或者方法),從而此代碼包的一個(gè)用戶可以在用戶包中聲明一個(gè)實(shí)現(xiàn)了此接口類型的用戶類型,并且將此用戶類型的值做為實(shí)參傳遞給此代碼包中聲明的函數(shù)(或者方法)的調(diào)用。 此代碼包的開發(fā)者并不用關(guān)心一個(gè)用戶類型具體是如何聲明的,只要此用戶類型滿足此代碼包中聲明的接口類型規(guī)定的行為即可。

事實(shí)上,多態(tài)對(duì)于一個(gè)語(yǔ)言來(lái)說(shuō)并非一個(gè)不可或缺的特性。我們可以通過(guò)其它途徑來(lái)實(shí)現(xiàn)多態(tài)的作用。 但是,多態(tài)可以使得我們的代碼更加簡(jiǎn)潔和優(yōu)雅。

反射(reflection)

一個(gè)接口值中存儲(chǔ)的動(dòng)態(tài)類型信息可以被用來(lái)檢視此接口值的動(dòng)態(tài)值和操縱此動(dòng)態(tài)值所引用的值。 這稱為反射。

本篇文章將不介紹reflect標(biāo)準(zhǔn)包中提供的各種反射功能。 請(qǐng)閱讀后面的Go中的反射一文來(lái)了解如何使用此包。 本文下面將只介紹Go中的內(nèi)置反射機(jī)制。在Go中,內(nèi)置反射機(jī)制包括類型斷言(type assertion)和type-switch流程控制代碼塊。

類型斷言

Go中有四種接口相關(guān)的類型轉(zhuǎn)換情形:

  1. 將一個(gè)非接口值轉(zhuǎn)換為一個(gè)接口類型。在這樣的轉(zhuǎn)換中,此非接口值的類型必須實(shí)現(xiàn)了此接口類型。
  2. 將一個(gè)接口值轉(zhuǎn)換為另一個(gè)接口類型(前者接口值的類型實(shí)現(xiàn)了后者目標(biāo)接口類型)。
  3. 將一個(gè)接口值轉(zhuǎn)換為一個(gè)非接口類型(此非接口類型必須實(shí)現(xiàn)了此接口值的接口類型)。
  4. 將一個(gè)接口值轉(zhuǎn)換為另一個(gè)接口類型(前者接口值的類型未實(shí)現(xiàn)后者目標(biāo)接口類型,但是前者的動(dòng)態(tài)類型有可能實(shí)現(xiàn)了目標(biāo)接口類型)。

前兩種情形已經(jīng)在前面幾節(jié)中介紹過(guò)了。這兩種情形都要求源值的類型必須實(shí)現(xiàn)了目標(biāo)接口類型。 它們都是通過(guò)普通類型轉(zhuǎn)換(無(wú)論是隱式的還是顯式的)來(lái)達(dá)成的。這兩種情形的合法性是在編譯時(shí)刻驗(yàn)證的。

本節(jié)將介紹后兩種情形。這兩種情形的合法性是在運(yùn)行時(shí)刻通過(guò)類型斷言來(lái)驗(yàn)證的。 事實(shí)上,類型斷言也適用于上面列出的第二種情形。

一個(gè)類型斷言表達(dá)式的語(yǔ)法為i.(T),其中i為一個(gè)接口值,T為一個(gè)類型名或者類型字面表示。 類型T可以為

  • 任意一個(gè)非接口類型。
  • 或者一個(gè)任意接口類型。

在一個(gè)類型斷言表達(dá)式i.(T)中,i稱為斷言值,T稱為斷言類型。 一個(gè)斷言可能成功或者失敗。

  • 對(duì)于T是一個(gè)非接口類型的情況,如果斷言值i的動(dòng)態(tài)類型存在并且此動(dòng)態(tài)類型和T為同一類型,則此斷言將成功;否則,此斷言失敗。 當(dāng)此斷言成功時(shí),此類型斷言表達(dá)式的估值結(jié)果為斷言值i的動(dòng)態(tài)值的一個(gè)復(fù)制。 我們可以把此種情況看作是一次拆封動(dòng)態(tài)值的嘗試。
  • 對(duì)于T是一個(gè)接口類型的情況,當(dāng)斷言值i的動(dòng)態(tài)類型存在并且此動(dòng)態(tài)類型實(shí)現(xiàn)了接口類型T,則此斷言將成功;否則,此斷言失敗。 當(dāng)此斷言成功時(shí),此類型斷言表達(dá)式的估值結(jié)果為一個(gè)包裹了斷言值i的動(dòng)態(tài)值的一個(gè)復(fù)制的T值。

一個(gè)失敗的類型斷言的估值結(jié)果為斷言類型的零值。

按照上述規(guī)則,如果一個(gè)類型斷言中的斷言值是一個(gè)零值nil接口值,則此斷言必定失敗。

對(duì)于大多數(shù)場(chǎng)合,一個(gè)類型斷言被用做一個(gè)單值表達(dá)式。 但是,當(dāng)一個(gè)類型斷言被用做一個(gè)賦值語(yǔ)句中的唯一源值時(shí),此斷言可以返回一個(gè)可選的第二個(gè)結(jié)果并被視作為一個(gè)多值表達(dá)式。 此可選的第二個(gè)結(jié)果為一個(gè)類型不確定的布爾值,用來(lái)表示此斷言是否成功了。

注意:如果一個(gè)斷言失敗并且它的可選的第二個(gè)結(jié)果未呈現(xiàn),則此斷言將造成一個(gè)恐慌。

一個(gè)展示了如何使用類型斷言的例子(斷言類型為非接口類型):

package main

import "fmt"

func main() {
	// 編譯器將把123的類型推斷為內(nèi)置類型int。
	var x interface{} = 123

	// 情形一:
	n, ok := x.(int)
	fmt.Println(n, ok) // 123 true
	n = x.(int)
	fmt.Println(n) // 123

	// 情形二:
	a, ok := x.(float64)
	fmt.Println(a, ok) // 0 false

	// 情形三:
	a = x.(float64) // 將產(chǎn)生一個(gè)恐慌
}

另一個(gè)展示了如何使用類型斷言的例子(斷言類型為接口類型):

package main

import "fmt"

type Writer interface {
	Write(buf []byte) (int, error)
}

type DummyWriter struct{}
func (DummyWriter) Write(buf []byte) (int, error) {
	return len(buf), nil
}

func main() {
	var x interface{} = DummyWriter{}
	// y的動(dòng)態(tài)類型為內(nèi)置類型string。
	var y interface{} = "abc"
	var w Writer
	var ok bool

	// DummyWriter既實(shí)現(xiàn)了Writer,也實(shí)現(xiàn)了interface{}。
	w, ok = x.(Writer)
	fmt.Println(w, ok) // {} true
	x, ok = w.(interface{})
	fmt.Println(x, ok) // {} true

	// y的動(dòng)態(tài)類型為string。string類型并沒(méi)有實(shí)現(xiàn)Writer。
	w, ok = y.(Writer)
	fmt.Println(w, ok) //  false
	w = y.(Writer)     // 將產(chǎn)生一個(gè)恐慌
}

事實(shí)上,對(duì)于一個(gè)動(dòng)態(tài)類型為T的接口值i,方法調(diào)用i.m(...)等價(jià)于i.(T).m(...)。

type-switch流程控制代碼塊

type-switch流程控制的語(yǔ)法或許是Go中最古怪的語(yǔ)法。 它可以被看作是類型斷言的增強(qiáng)版。它和switch-case流程控制代碼塊有些相似。 一個(gè)type-switch流程控制代碼塊的語(yǔ)法如下所示:

switch aSimpleStatement; v := x.(type) {
case TypeA:
	...
case TypeB, TypeC:
	...
case nil:
	...
default:
	...
}

其中aSimpleStatement;部分是可選的。 aSimpleStatement必須是一個(gè)簡(jiǎn)單語(yǔ)句。 x必須為一個(gè)估值結(jié)果為接口值的表達(dá)式,它稱為此代碼塊中的斷言值。 v稱為此代碼塊中的斷言結(jié)果,它必須出現(xiàn)在一個(gè)短變量聲明形式中。

在一個(gè)type-switch代碼塊中,每個(gè)case關(guān)鍵字后可以跟隨預(yù)聲明的nil標(biāo)識(shí)符或者一個(gè)由逗號(hào)分割的若干個(gè)類型名和類型字面表示形式組成的類型列表。 在同一個(gè)type-switch代碼塊中,這些跟隨在所有case關(guān)鍵字后的條目不可重復(fù)。

如果跟隨在某個(gè)case關(guān)鍵字后的條目為一個(gè)非接口類型(用一個(gè)類型名或類型字面表示),則此非接口類型必須實(shí)現(xiàn)了斷言值x的(接口)類型。

下面是一個(gè)使用了type-switch代碼塊的例子:

package main

import "fmt"

func main() {
	values := []interface{}{
		456, "abc", true, 0.33, int32(789),
		[]int{1, 2, 3}, map[int]bool{}, nil,
	}
	for _, x := range values {
		// 這里,雖然變量v只被聲明了一次,但是它在
		// 不同分支中可以表現(xiàn)為多個(gè)類型的變量值。
		switch v := x.(type) {
		case []int: // 一個(gè)類型字面表示
			// 在此分支中,v的類型為[]int。
			fmt.Println("int slice:", v)
		case string: // 一個(gè)類型名
			// 在此分支中,v的類型為string。
			fmt.Println("string:", v)
		case int, float64, int32: // 多個(gè)類型名
			// 在此分支中,v的類型為x的類型interface{}。
			fmt.Println("number:", v)
		case nil:
			// 在此分支中,v的類型為x的類型interface{}。
			fmt.Println(v)
		default:
			// 在此分支中,v的類型為x的類型interface{}。
			fmt.Println("others:", v)
		}
		// 注意:在最后的三個(gè)分支中,v均為接口值x的一個(gè)復(fù)制。
	}
}

輸出結(jié)果:

number: 456
string: abc
others: true
number: 0.33
number: 789
int slice: [1 2 3]
others: map[]
<nil>

上面這個(gè)例子程序在邏輯上等價(jià)于下面這個(gè):

package main

import "fmt"

func main() {
	values := []interface{}{
		456, "abc", true, 0.33, int32(789),
		[]int{1, 2, 3}, map[int]bool{}, nil,
	}
	for _, x := range values {
		if v, ok := x.([]int); ok {
			fmt.Println("int slice:", v)
		} else if v, ok := x.(string); ok {
			fmt.Println("string:", v)
		} else if x == nil {
			v := x
			fmt.Println(v)
		} else {
			_, isInt := x.(int)
			_, isFloat64 := x.(float64)
			_, isInt32 := x.(int32)
			if isInt || isFloat64 || isInt32 {
				v := x
				fmt.Println("number:", v)
			} else {
				v := x
				fmt.Println("others:", v)
			}
		}
	}
}

如果我們不關(guān)心一個(gè)type-switch代碼塊中的斷言結(jié)果,則此type-switch代碼塊可簡(jiǎn)寫為switch x.(type) {...}。

type-switch代碼塊和switch-case代碼塊有很多共同點(diǎn):

  • 在一個(gè)type-switch代碼塊中,最多只能有一個(gè)default分支存在。
  • 在一個(gè)type-switch代碼塊中,如果default分支存在,它可以為最后一個(gè)分支、第一個(gè)分支或者中間的某個(gè)分支。
  • 一個(gè)type-switch代碼塊可以不包含任何分支,它可以被視為一個(gè)空操作。

但是,和switch-case代碼塊不一樣,fallthrough語(yǔ)句不能使用在type-switch代碼塊中。

更多接口相關(guān)的內(nèi)容

接口值相關(guān)的比較

接口值相關(guān)的比較有兩種情形:

  1. 比較一個(gè)非接口值和接口值;
  2. 比較兩個(gè)接口值。

對(duì)于第一種情形,非接口值的類型必須實(shí)現(xiàn)了接口值的類型(假設(shè)此接口類型為I),所以此非接口值可以被隱式轉(zhuǎn)化為(包裹到)一個(gè)I值中。 這意味著非接口值和接口值的比較可以轉(zhuǎn)化為兩個(gè)接口值的比較。所以下面我們只探討兩個(gè)接口值比較的情形。

比較兩個(gè)接口值其實(shí)是比較這兩個(gè)接口值的動(dòng)態(tài)類型和和動(dòng)態(tài)值。

下面是(使用==比較運(yùn)算符)比較兩個(gè)接口值的步驟:

  1. 如果其中一個(gè)接口值是一個(gè)nil接口值,則比較結(jié)果為另一個(gè)接口值是否也為一個(gè)nil接口值。
  2. 如果這兩個(gè)接口值的動(dòng)態(tài)類型不一樣,則比較結(jié)果為false。
  3. 對(duì)于這兩個(gè)接口值的動(dòng)態(tài)類型一樣的情形,
    • 如果它們的動(dòng)態(tài)類型為一個(gè)不可比較類型,則將產(chǎn)生一個(gè)恐慌。
    • 否則,比較結(jié)果為它們的動(dòng)態(tài)值的比較結(jié)果。

簡(jiǎn)而言之,兩個(gè)接口值的比較結(jié)果只有在下面兩種任一情況下才為true

  1. 這兩個(gè)接口值都為nil接口值。
  2. 這兩個(gè)接口值的動(dòng)態(tài)類型相同、動(dòng)態(tài)類型為可比較類型、并且動(dòng)態(tài)值相等。

根據(jù)此規(guī)則,兩個(gè)包裹了不同非接口類型的nil零值的接口值是不相等的。 一個(gè)例子:

package main

import "fmt"

func main() {
	var a, b, c interface{} = "abc", 123, "a"+"b"+"c"
	fmt.Println(a == b) // 第二步的情形。輸出"false"。
	fmt.Println(a == c) // 第三步的情形。輸出"true"。

	var x *int = nil
	var y *bool = nil
	var ix, iy interface{} = x, y
	var i interface{} = nil
	fmt.Println(ix == iy) // 第二步的情形。輸出"false"。
	fmt.Println(ix == i)  // 第一步的情形。輸出"false"。
	fmt.Println(iy == i)  // 第一步的情形。輸出"false"。

	var s []int = nil // []int為一個(gè)不可比較類型。
	i = s
	fmt.Println(i == nil) // 第一步的情形。輸出"false"。
	fmt.Println(i == i)   // 第三步的情形。將產(chǎn)生一個(gè)恐慌。
}

接口值的內(nèi)部結(jié)構(gòu)

對(duì)于標(biāo)準(zhǔn)編譯器/運(yùn)行時(shí),空接口值和非空接口值是使用兩種內(nèi)部結(jié)構(gòu)來(lái)表示的。 詳情請(qǐng)閱讀值部一文。

指針動(dòng)態(tài)值和非指針動(dòng)態(tài)值

標(biāo)準(zhǔn)編譯器/運(yùn)行時(shí)對(duì)接口值的動(dòng)態(tài)值為指針類型的情況做了特別的優(yōu)化。 此優(yōu)化使得接口值包裹指針動(dòng)態(tài)值比包裹非指針動(dòng)態(tài)值的效率更高。 對(duì)于小尺寸值,此優(yōu)化的作用不大; 但是對(duì)于大尺寸值,包裹它的指針比包裹此值本身的效率高得多。 對(duì)于類型斷言,此結(jié)論亦成立。

所以盡量避免在接口值中包裹大尺寸值。對(duì)于大尺寸值,應(yīng)該盡量包裹它的指針。

一個(gè)[]T類型的值不能直接被轉(zhuǎn)換為類型[]I,即使類型T實(shí)現(xiàn)了接口類型I

比如,我們不能直接將一個(gè)[]string值轉(zhuǎn)換為類型[]interface{}。 我們必須使用一個(gè)循環(huán)來(lái)實(shí)現(xiàn)此轉(zhuǎn)換:

package main

import "fmt"

func main() {
	words := []string{
		"Go", "is", "a", "high",
		"efficient", "language.",
	}

	// fmt.Println函數(shù)的原型為:
	// func Println(a ...interface{}) (n int, err error)
	// 所以words...不能傳遞給此函數(shù)的調(diào)用。

	// fmt.Println(words...) // 編譯不通過(guò)

	// 將[]string值轉(zhuǎn)換為類型[]interface{}。
	iw := make([]interface{}, 0, len(words))
	for _, w := range words {
		iw = append(iw, w)
	}
	fmt.Println(iw...) // 編譯沒(méi)問(wèn)題
}

一個(gè)接口類型每個(gè)指定的每一個(gè)方法都對(duì)應(yīng)著一個(gè)隱式聲明的函數(shù)

如果接口類型I指定了一個(gè)名為m的方法描述,則編譯器將隱式聲明一個(gè)與之對(duì)應(yīng)的函數(shù)名為I.m的函數(shù)。 此函數(shù)比m的方法描述中的參數(shù)多一個(gè)。此多出來(lái)的參數(shù)為函數(shù)I.m的第一個(gè)參數(shù),它的類型為I。 對(duì)于一個(gè)類型為I的值i,方法調(diào)用i.m(...)和函數(shù)調(diào)用I.m(i, ...)是等價(jià)的。

一個(gè)例子:

package main

import "fmt"

type I interface {
	m(int)bool
}

type T string
func (t T) m(n int) bool {
	return len(t) > n
}

func main() {
	var i I = T("gopher")
	fmt.Println(i.m(5))                          // true
	fmt.Println(I.m(i, 5))                       // true
	fmt.Println(interface {m(int) bool}.m(i, 5)) // true

	// 下面這幾行被執(zhí)行的時(shí)候都將會(huì)產(chǎn)生一個(gè)恐慌。
	I(nil).m(5)
	I.m(nil, 5)
	interface {m(int) bool}.m(nil, 5)
}


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)