Go語言對象模型 之 類型斷言

類型斷言是Go語言中應用在接口值上的一個神奇特性,它有兩種典型的使用方式:

1)判斷接口值的動態(tài)類型是否為某個具體類型,如果是則進一步從接口值中提取出相應具體類型的值;
這里說的“具體類型”指的就是像int、stringslice以及struct這樣有特定存儲結構的類型。與“具體類型”相對的自然就是“抽象類型”,在Go語言中就是具有方法的接口類型,其抽象了對象行為而隱藏了背后實現(xiàn)。

2)判斷接口值的動態(tài)類型是否滿足某個目標接口類型,如果滿足則進一步將其轉換為目標接口值;
這里說的“目標接口類型”指的是具有方法的接口類型,如io.Reader、fmt.Stringer,而不是不含任何方法的接口類型interface{}。我們稱不包含任何方法的interface{}為“空接口類型”,這里說的“空”是類型而不是值,注意不要和nil混淆。

在Go語言的實現(xiàn)中,“空”接口類型的值和“非空”接口類型的值在存儲結構的定義上是不相同的,空接口類型的值用runtime.eface結構來存儲,而非空接口類型的值用runtime.iface結構來存儲。下面是兩種結構的具體定義:

runtime.eface

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

runtime.iface

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

從具體定義可以發(fā)現(xiàn),兩種結構的大小是相等的,都是由兩個指針組成。兩種結構都有一個unsafe.Pointer類型的成員data,用來存儲具體類型值的地址,如果datanil,相應的接口值就為nil。

eface結構中的另一個成員是指向_type類型的指針,名字也是_type。在Go程序的編譯階段,編譯器會為每種在代碼中被使用到的具體類型生成類型元數(shù)據,元數(shù)據里包含類型名、占用存儲空間大小、自定義類型的方法列表等信息。在最終生成的可執(zhí)行文件中,每種具體類型的元數(shù)據都是唯一的,并且以一個_type類型的結構作為入口,也就意味著每種具體類型的_type結構擁有唯一的地址。eface_type字段存儲的就是data指針指向的值所屬具體類型的類型元數(shù)據地址。

var i interface{}
// 此行實際上分配了一個runtime.eface結構,等價于如下代碼:
// var i runtime.eface
//
// 你可能會疑惑,runtime.eface類型沒有被導出,但這只是對用戶代碼的限制
// 編譯器總是有權限做任何事情,就像g++能夠調用initializer_list的私有構造函數(shù)

a := 10

i = a
// 此行實際上做了兩件事情
// 1.將存儲int類型元數(shù)據的runtime._type對象地址賦給i._type
// 2.將變量a的地址賦給i.data
//
// i._type = &go.types.int
// i.data = unsafe.Pointer(&a)

所以說eface結構,本質上就是將具體類型值的地址和類型元數(shù)據地址打包在一起。在判斷eface接口值是否為某種具體類型時,直接比較_type指針是否相等即可,這也是Go語言中類型斷言的一種實現(xiàn)形式。

場景1interface{}斷言為某種具體類型:

func assert(v interface{}) {
    if i, ok := v.(int); ok {
        // todo ...
    }
}

// v的實際類型為runtime.eface,上述斷言實際上就是指針的相等性比較:
// ok := v._type == &go.types.int
//
// 這里如果不使用‘comma ok’ idiom,就會直接panic

iface中的tab字段是一個runtime.itab類型的指針,runtime.itab結構定義如下:

type itab struct {
    inter *interfacetype
    _type *_type
    hash  uint32 // copy of _type.hash. Used for type switches.
    _     [4]byte
    fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

itab類型被設計出來,最主要的目的就是實現(xiàn)Go語言的接口方法機制。要想通過接口值調用具體類型的方法,必須能夠從接口值出發(fā),找到對應方法的地址。如果像eface那樣先找到_type,再通過查詢元數(shù)據找到對應方法,運行時效率實在低下。所以itab結構就是用來解決這個問題。

itab_type字段同eface._type一樣,存儲的是接口值具體類型的元數(shù)據地址,而interfacetype結構存儲的是“非空”接口類型的類型元數(shù)據,其定義如下:

type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod
}

其中除了_type結構外,還包含包路徑pkgpath和方法列表mhdr,len(mhdr)等于接口中方法的個數(shù)。方法列表中存儲的是接口定義的所有方法的命子和類型信息。

itabfun字段是一個指針數(shù)組,長度等于接口中方法的個數(shù),里面存儲的是各個接口方法的地址,就像C++的虛函數(shù)表。itab結構有兩種分配方式:一是被編譯器靜態(tài)生成,二是由運行時動態(tài)分配,兩種情況都會為fun數(shù)組分配合適大小的空間。

我們根據itabinter字段,即接口的元數(shù)據信息,能夠知道接口定義了哪些方法;根據itab_type字段,即具體類型的元數(shù)據信息,能夠知道具體類型實現(xiàn)了哪些方法,以及方法的地址;最后我們從具體類型的方法列表里逐一查找到接口定義的方法,并將方法的地址保存到fun數(shù)組相應的位置。這樣我們就完成了一個itab的初始化,就像編譯器和運行時所做的那樣。

和C++的虛函數(shù)機制不同的是:一,不像C++的虛函數(shù)表完全在編譯階段生成,Go語言能夠在運行時分配并初始化itab;二,為了提高函數(shù)查找效率,_type中具體類型實現(xiàn)的方法列表,以及interfacetype中接口定義的方法列表mhdr都是經過排序的,查找時只需遍歷一次。

運行時負責分配和初始化itab的函數(shù)是runtime.getitab,如下函數(shù)聲明:

func getitab(inter *interfacetype, typ *_type, canfail bool) *itab

該函數(shù)的主要邏輯:

  1. 檢查參數(shù)的合法性:要求len(inter.mhdr)大于0,即接口至少定義1個方法;要求typ描述的具體類型是“自定義”類型,只有自定義類型才能有方法;
  2. 首先檢查itabTable里有沒有緩存對應于這對(inter, typ)*itab,有則直接從緩存中取出來,跳到第6步;
  3. 沒有緩存的情況下,持久內存分配itab對象,賦值itab.inter, itab._type = inter, typ,然后調用itab對象的init方法;
  4. itab.init方法會遍歷匹配inter_type的方法列表,并將找到的方法地址填寫到itab.fun數(shù)組對應位置,因為兩個列表都是排序過的,所以只遍歷一次效率很高。如果能夠匹配到接口定義的所有方法,則返回空字符串"";如果有某個接口方法在_type實現(xiàn)的方法列表中未找到,則賦值itab.fun[0] = 0,然后返回未找到方法的name;
  5. itab添加到itabTable中,使用(inter, typ)作為key;
  6. getitab根據fun[0] != 0判斷是否成功,成功則返回*itab,失敗則根據canfail參數(shù)決定返回nil或者panic。
getitab.png

由此看來,如果斷言的目標類型是一個“非空”接口類型,或者直接說存儲結構是iface,那么斷言邏輯需要檢查源具體類型實現(xiàn)的方法列表能否滿足接口定義的方法列表。從集合的角度來看,要求具體類型實現(xiàn)的方法集能夠包含接口定義的方法集,用A表示接口定義的方法集,B表示具體類型實現(xiàn)的方法集,則有:

A \subset B

到了程序運行階段,通過iface調用接口方法時,就可以直接從iface.itab.fun中按下標取到對應方法的地址,這點還是和虛函數(shù)機制很類似的。就像C++中同一個類的所有對象共享虛函數(shù)表一樣,Go語言中擁有相同interfacetype_type的所有iface共享itab,運行時有個全局的哈希表itabTable,用來緩存已經初始化過的itab

Go語言的編譯器總是嘗試在編譯階段做盡可能多的事情,這樣就能讓運行時更高效。在某些情況下,編譯器能夠在上下文中獲得足夠的信息,來靜態(tài)生成itab,就像如下代碼所示:

f, _ := os.Open("test.txt")

var rw io.ReadWriter
// 此行實際上分配了一個runtime.iface結構:
// var rw runtime.iface

rw = f
// 此行也是做了兩件事情,用偽代碼表示:
// rw.tab = &go.itab.*os.File,io.ReadWriter
// rw.data = unsafe.Pointer(&f)
//
// 編譯器有足夠的信息在編譯階段靜態(tài)生成itab:目標接口類型和源具體類型均可從上下文得到
// 這里rw.tab.fun數(shù)組的大小為2,存儲的分別是*os.File的Read和Write方法的地址

在某些情況下,編譯器從上下文中無法得到足夠的信息,所以就只能依靠運行時分配并初始化itab。

場景2interface{}斷言為某種“非空”接口類型

func assert(v interface{}) {
    if w, ok := v.(io.Writer); ok {
        // todo ...
    }
}

// v的實際類型為runtime.eface,v.data動態(tài)類型在編譯階段不可知,無法靜態(tài)生成itab
//
// 從eface到iface,’comma ok’形式,通過runtime.assertE2I2函數(shù)進行處理

在Go語言運行時中,有4個函數(shù)用來處理目標類型為“非空”接口的斷言:

// 判斷iface的具體類型是否實現(xiàn)了inter要求的方法集,否則panic
func assertI2I(inter *interfacetype, i iface) (r iface)

// 判斷iface的具體類型是否實現(xiàn)了inter要求的方法集,否則返回false,'comma ok'形式
func assertI2I2(inter *interfacetype, i iface) (r iface, b bool)

// 判斷eface的具體類型是否實現(xiàn)了inter要求的方法集,否則panic
func assertE2I(inter *interfacetype, e eface) (r iface)

// 判斷eface的具體類型是否實現(xiàn)了inter要求的方法集,否則返回false,'comma ok'形式
func assertE2I2(inter *interfacetype, e eface) (r iface, b bool)

這4個函數(shù)主要邏輯都是通過runtime.getitab來實現(xiàn)的。

場景3 從一種“非空”接口類型斷言為另一種“非空”接口類型

func assert(w io.Writer) {
    if rw, ok := w.(io.ReadWriter); ok {
        // todo ...
    }
}

// w的實際類型為runtime.iface
// 本質上就是檢查w.itab._type是否實現(xiàn)了io.ReadWriter定義的兩個方法
// 通過runtime.assertI2I2函數(shù)進行處理

而對于源為“非空”接口類型,目標為具體類型的斷言,編譯器是能夠在編譯階段生成itab的,所以不需要運行時動態(tài)處理。

場景4 從“非空”接口類型斷言為某種具體類型

func assert(w io.Writer) {
    if f, ok := w.(*os.File); ok {
        // todo ...
    }
}

// v的實際類型為runtime.iface
// 具體類型為*os.File,接口類型為io.Writer,兩個都能確定可以在編譯階段生成itab
//
// 本質上上述斷言被實現(xiàn)為指針的相等性比較:
// ok := v.itab == &go.itab.*os.File,io.Writer

上面之所以把4個場景的示例代碼都放在單獨的函數(shù)中,是為了避免額外的上下文信息造成編譯器的進一步優(yōu)化,從而干擾實驗效果。例如如下代碼是不會用到runtime.assertI2I的:

f, _ := os.Open("test.txt")
var w io.Writer = f
var rw io.ReadWriter = w.(io.ReadWriter)
// 編譯器可以通過分析上下文,得知w的動態(tài)類型為*os.File,從而靜態(tài)生成rw.itab

本文介紹了“空”接口類型值和“非空”接口類型值的不同存儲結構runtime.efaceruntime.iface,以及具體類型元數(shù)據_type和“非空”接口類型元數(shù)據interfacetype在類型斷言中起到的作用。在探討類型斷言的具體實現(xiàn)時,按照源類型是“空”或“非空”接口類型、目標類型是具體類型或者“非空”接口類型,分成了4種情況來研究。在目標類型為具體類型時,編譯器可以在編譯階段生成需要的結構,運行階段只需比較指針相等性;目標類型為“非空”接口類型時,需要通過runtime.assert系列4個函數(shù)進行處理。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容