天天看點

Golang通脈之反射

官方關于反射定義:

Reflection in computing is the ability of a program to examine its own structure, particularly through types; it’s a form of metaprogramming. It’s also a great source of confusion. (在計算機領域,反射是一種讓程式——主要是通過類型——了解其自身結構的一種能力。它是元程式設計的組成之一,同時它也是一大引人困惑的難題。)

維基百科關于反射的定義:

在計算機科學中,反射是指計算機程式在運作時(Run time)可以通路、檢測和修改它本身狀态或行為的一種能力。用比喻來說,反射就是程式在運作的時候能夠“觀察”并且修改自己的行為。

《Go語言聖經》關于反射的定義:

Go 語言提供了一種機制在運作時更新變量和檢查它們的值、調用它們的方法,但是在編譯時并不知道這些變量的具體類型,這稱為反射機制。

Go 語言是靜态編譯類語言,比如在定義一個變量的時候,已經知道了它是什麼類型。但是有些事情隻有在運作時才知道。比如定義了一個函數,它有一個<code>interface{}</code>類型的參數,這也就意味着調用者可以傳遞任何類型的參數給這個函數。在這種情況下,如果想知道調用者傳遞的是什麼類型的參數,就需要用到反射。如果想知道一個結構體有哪些字段和方法,也需要反射。

根據以上定義,可以得出:

反射是指在程式運作時對程式本身進行通路和修改的能力。程式在編譯時,變量被轉換為記憶體位址,變量名不會被編譯器寫入到可執行部分。在運作程式時,程式無法擷取自身的資訊。

支援反射的語言可以在程式編譯期将變量的反射資訊,如字段名稱、類型資訊、結構體資訊等整合到可執行檔案中,并給程式提供接口通路反射資訊,這樣就可以在程式運作期擷取類型的反射資訊,并且有能力修改它們。

Go語言中的變量是分為兩部分的:

類型資訊(<code>type</code>):預先定義好的元資訊。

值資訊(<code>value</code>):程式運作過程中可動态變化的。

了解這一點就知道為什麼<code>nil != nil</code>了

<code>type</code> 包括 <code>static type</code>和<code>concrete type</code>. 簡單來說 <code>static type</code>是在編碼時确定的類型(如<code>int</code>、<code>string</code>等),<code>concrete type</code>是<code>runtime</code>系統确定的類型。

類型斷言能否成功,取決于變量的<code>concrete type</code>,而不是<code>static type</code>。是以,一個 <code>reader</code>變量如果它的<code>concrete type</code>也實作了<code>write</code>方法的話,它也可以被類型斷言為<code>writer</code>

Go是靜态類型語言。每個變量都擁有一個靜态類型,這意味着每個變量的類型在編譯時都是确定的:int,float32, *AutoType, []byte, chan []int 諸如此類。

在反射的概念中, 編譯時就知道變量類型的是靜态類型;運作時才知道一個變量類型的叫做動态類型。

靜态類型: 靜态類型就是變量聲明時的賦予的類型:

動态類型:運作時給這個變量指派時,這個值的類型(如果值為nil的時候沒有動态類型)。一個變量的動态類型在運作時可能改變,這主要依賴于它的指派(前提是這個變量是接口類型)。

Go語言的反射就是建立在類型之上的,Golang的指定類型的變量的類型是靜态的,在建立變量的時候就已經确定,反射主要與Golang的<code>interface</code>類型相關,隻有<code>interface</code>類型才有反射一說。

在Golang的實作中,每個<code>interface</code>變量都有一個對應<code>pair</code>,<code>pair</code>中記錄了實際變量的值和類型(在接口介紹時有描述):

value是實際變量值,type是實際變量的類型。一個<code>interface{}</code>類型的變量包含了2個指針,一個指針指向值的類型(對應concrete type),另外一個指針指向實際的值(對應value)。

例如,建立類型為<code>*os.File</code>的變量,然後将其賦給一個接口變量<code>r</code>:

接口變量<code>r</code>的<code>pair</code>中将記錄如下資訊:<code>(tty, *os.File)</code>,這個<code>pair</code>在接口變量的連續指派過程中是不變的,将接口變量<code>r</code>賦給另一個接口變量<code>w</code>:

接口變量<code>w</code>的<code>pair</code>與<code>r</code>的<code>pair</code>相同,都是:<code>(tty, *os.File)</code>,即使<code>w</code>是空接口類型,<code>pair</code>也是不變的。

interface及其pair的存在,是Golang中實作反射的前提,了解了pair,就更容易了解反射。反射就是用來檢測存儲在接口變量内部(值value;類型concrete type) pair對的一種機制。

是以要了解兩個基本概念 Type 和 Value,它們也是 Go語言包中 reflect 空間裡最重要的兩個類型。

Go程式在運作時使用<code>reflect</code>包通路程式的反射資訊。

之前介紹過<code>interface</code>,空接口可以存儲任意類型的變量,那如何知道這個空接口儲存的資料是什麼呢? 反射就是在運作時動态的擷取一個變量的類型資訊和值資訊。

在Go語言的反射機制中,任何接口值都由是<code>一個具體類型</code>和<code>具體類型的值</code>兩部分組成的。 在Go語言中反射的相關功能由内置的<code>reflect</code>包提供,任意接口值在反射中都可以了解為由<code>reflect.Type</code>和<code>reflect.Value</code>兩部分組成,并且<code>reflect</code>包提供了<code>reflect.TypeOf</code>和<code>reflect.ValueOf</code>兩個函數來擷取任意對象的<code>Value</code>和<code>Type</code>。

<code>reflect.Value</code> 可以用于與值有關的操作中,而如果是和變量類型本身有關的操作,則最好使用 reflect.Type,比如要擷取結構體對應的字段名稱或方法。

和 reflect.Value 不同,reflect.Type 是一個接口,而不是一個結構體,是以也隻能使用它的方法。

以下 reflect.Type 接口常用的方法。從清單來看,大部分都和 reflect.Value 的方法功能相同。

其中幾個特有的方法如下:

Implements 方法用于判斷是否實作了接口 u;

AssignableTo 方法用于判斷是否可以指派給類型 u,其實就是是否可以使用 =,即指派運算符;

ConvertibleTo 方法用于判斷是否可以轉換成類型 u,其實就是是否可以進行類型轉換;

Comparable 方法用于判斷該類型是否是可比較的,其實就是是否可以使用關系運算符進行比較。

要反射擷取一個變量的 <code>reflect.Type</code>,可以通過函數 <code>reflect.TypeOf()</code>,程式通過類型對象可以通路任意值的類型資訊。

在反射中關于類型還劃分為兩種:<code>類型(Type)</code>和<code>種類(Kind)</code>。因為在Go語言中可以使用<code>type</code>關鍵字構造很多自定義類型,而<code>種類(Kind)</code>就是指底層的類型,但在反射中,當需要區分指針、結構體等大品種的類型時,就會用到<code>種類(Kind)</code>。 舉個例子,定義了兩個指針類型和兩個結構體類型,通過反射檢視它們的類型和種類。

Go語言的反射中像數組、切片、Map、指針等類型的變量,它們的<code>.Name()</code>都是傳回<code>空</code>。

在<code>reflect</code>包中定義的<code>Kind</code>類型如下:

通過 <code>reflect.Type</code> 還可以判斷是否實作了某接口。以 <code>person</code> 結構體為例,判斷它是否實作了接口 <code>fmt.Stringer</code> 和 <code>io.Writer</code>:

盡可能通過類型斷言的方式判斷是否實作了某接口,而不是通過反射。

通過 Implements 方法來判斷是否實作了 fmt.Stringer 和 io.Writer 接口,運作結果:

<code>reflect.ValueOf()</code>傳回的是<code>reflect.Value</code>類型,其中包含了原始值的值資訊。<code>reflect.Value</code>與原始值之間可以互相轉換。

<code>reflect.Value</code> 被定義為一個 <code>struct</code> 結構體,它的定義如下面所示:

<code>reflect.Value</code> 結構體的字段都是私有的,也就是說,隻能使用 <code>reflect.Value</code> 的方法。它有如下常用方法,

方法

說明

<code>Interface() interface {}</code>

将值以 interface{} 類型傳回,可以通過類型斷言轉換為指定類型

<code>Int() int64</code>

将值以 int 類型傳回,所有有符号整型均可以此方式傳回

<code>Uint() uint64</code>

将值以 uint 類型傳回,所有無符号整型均可以此方式傳回

<code>Float() float64</code>

将值以雙精度(float64)類型傳回,所有浮點數(float32、float64)均可以此方式傳回

<code>Bool() bool</code>

将值以 bool 類型傳回

<code>Bytes() []bytes</code>

将值以位元組數組 []bytes 類型傳回

<code>String() string</code>

将值以字元串類型傳回

<code>CanSet() bool</code>

是否可以修改對應的值

<code>Elem() Type</code>

擷取指針指向的值,一般用于修改對應的值

<code>Kind() Kind</code>

擷取對應的類型類别,比如Array、Slice、Map等

想要在函數中通過反射修改變量的值,需要注意函數參數傳遞的是值拷貝,必須傳遞變量位址才能修改變量值。而反射中使用專有的<code>Elem()</code>方法來擷取指針對應的值。

<code>IsNil()</code>報告v持有的值是否為nil。v持有的值的分類必須是通道、函數、接口、映射、指針、切片之一;否則IsNil函數會導緻panic。

<code>IsValid()</code>傳回v是否持有一個值。如果v是Value零值會傳回假,此時v除了IsValid、String、Kind之外的方法都會導緻panic。

<code>IsNil()</code>常被用于判斷指針是否為空;<code>IsValid()</code>常被用于判定傳回值是否有效。

任意值通過<code>reflect.TypeOf()</code>獲得反射對象資訊後,如果它的類型是結構體,可以通過反射值對象(<code>reflect.Type</code>)的<code>NumField()</code>和<code>Field()</code>方法獲得結構體成員的詳細資訊。

<code>reflect.Type</code>中與擷取結構體成員相關的的方法如下表所示。

Field(i int) StructField

根據索引,傳回索引對應的結構體字段的資訊。

NumField() int

傳回結構體成員字段數量。

FieldByName(name string) (StructField, bool)

根據給定字元串傳回字元串對應的結構體字段的資訊。

FieldByIndex(index []int) StructField

多層成員通路時,根據 []int 提供的每個結構體的字段索引,傳回字段的資訊。

FieldByNameFunc(match func(string) bool) (StructField,bool)

根據傳入的比對函數比對需要的字段。

NumMethod() int

傳回該類型的方法集中方法的數目

Method(int) Method

傳回該類型方法集中的第i個方法

MethodByName(string)(Method, bool)

根據方法名傳回該類型方法集中的方法

<code>StructField</code>類型用來描述結構體中的一個字段的資訊。

<code>StructField</code>的定義如下:

當我們使用反射得到一個結構體資料之後可以通過索引依次擷取其字段資訊,也可以通過字段名去擷取指定的字段資訊。

運作結果:

接下來編寫一個函數<code>printMethod(s interface{})</code>來周遊列印s包含的方法。

根據上面對反射的大緻介紹,對反射有了一定的了解,其實反射的操作步驟非常的簡單,就是通過執行個體對象擷取反射對象(Value、Type),然後操作相應的方法

執行個體、Value、Type 三者之間的轉換關系:

Golang通脈之反射

從執行個體到<code>Value</code>

通過執行個體擷取 Value 對象,直接使用 reflect.ValueOf() 函數:

從執行個體到<code>Type</code>

通過執行個體擷取反射對象的 Type,直接使用 reflect.TypeOf() 函數:

從<code>Type</code>到<code>Value</code>

Type 裡面隻有類型資訊,是以直接從一個 Type 接口變量裡面是無法獲得執行個體的 Value 的,但可以通過該 Type 建構一個新執行個體的 Value。reflect 包提供了兩種方法,示例如下:

如果知道一個類型值的底層存放位址,則還有一個函數是可以依據 type 和該位址值恢複出 Value 的:

從<code>Value</code>到<code>Type</code>

從反射對象 Value 到 Type 可以直接調用 Value 的方法,因為 Value 内部存放着到 Type 類型的指針:

從<code>Value</code>到執行個體

Value 本身就包含類型和值資訊,reflect 提供了豐富的方法來實作從 Value 到執行個體的轉換:

從<code>Value</code>的指針到值

從一個指針類型的 Value 獲得值類型 Value 有兩種方法:

<code>Type</code> 指針和值的互相轉換

指針類型 Type 到值類型 Type:

值類型 Type 到指針類型 Type:

<code>Value</code> 值的可修改性

Value 值的修改涉及如下兩個方法:

執行個體對象傳遞給接口的是一個完全的值拷貝,如果調用反射的方法 reflect.ValueOf() 傳進去的是一個值類型變量, 則獲得的 Value 實際上是原對象的一個副本,這個 Value 是無論如何也不能被修改的。

反射是計算機語言中程式檢視其自身結構的一種方法,它屬于元程式設計的一種形式。反射靈活、強大,但也存在不安全。它可以繞過編譯器的很多靜态檢查,如果過多使用便會造成混亂。為了幫助開發者更好地了解反射,Go 語言的作者在部落格上總結了反射的三大定律。

1.Reflection goes from interface value to reflection object. 2.Reflection goes from reflection object to interface value. 3.To modify a reflection object, the value must be settable.

任何接口值 interface{} 都可以反射出反射對象,也就是 reflect.Value 和 reflect.Type,通過函數 reflect.ValueOf 和 reflect.TypeOf 獲得。

反射對象也可以還原為 interface{} 變量,也就是第 1 條定律的可逆性,通過 reflect.Value 結構體的 Interface 方法獲得。

要修改反射的對象,該值必須可設定,也就是可尋址。

任何類型的變量都可以轉換為空接口 intferface{},是以第 1 條定律中函數 reflect.ValueOf 和 reflect.TypeOf 的參數就是 interface{},表示可以把任何類型的變量轉換為反射對象。在第 2 條定律中,reflect.Value 結構體的 Interface 方法傳回的值也是 interface{},表示可以把反射對象還原為對應的類型變量。

當執行reflect.ValueOf(interface)之後,就得到了一個類型為”relfect.Value”變量,可以通過它本身的Interface()方法獲得接口變量的真實内容,然後可以通過類型判斷進行轉換,轉換為原有真實類型。不過,可能是已知原有類型,也有可能是未知原有類型:

已知類型後轉換為其對應的類型的做法如下,直接通過Interface方法然後強制轉換,如下:

轉換的時候,如果轉換的類型不完全符合,則直接panic,類型要求非常嚴格!

轉換的時候,要區分是指針還是值

也就是說反射可以将“反射類型對象”再重新轉換為“接口類型變量”

很多情況下,可能并不知道其具體類型,那麼就需要進行周遊探測其Filed來得知:

通過運作結果可以得知擷取未知類型的<code>interface</code>的具體變量及其類型的步驟為:

先擷取interface的reflect.Type,然後通過NumField進行周遊

再通過reflect.Type的Field擷取其Field

最後通過Field的Interface()得到對應的value

通過運作結果可以得知擷取未知類型的<code>interface</code>的所屬方法(函數)的步驟為:

先擷取interface的reflect.Type,然後通過NumMethod進行周遊

再分别通過reflect.Type的Method擷取對應的真實的方法(函數)

最後對結果取其Name和Type得知具體的方法名

struct 或者 struct 的嵌套都是一樣的判斷處理方式

如果是struct的話,可以使用Elem()

reflect.Value是通過reflect.ValueOf(X)獲得的,隻有當X是指針的時候,才可以通過reflec.Value修改實際變量X的值,即:要修改反射類型的對象就一定要保證其值是可尋址的。

這裡需要一個方法:

解釋起來就是:Elem傳回接口v包含的值或指針v指向的值。如果v的類型不是interface或ptr,它會恐慌。如果v為零,則傳回零值。

需要傳入的參數是* float64這個指針,然後可以通過pointer.Elem()去擷取所指向的Value,注意一定要是指針。

如果傳入的參數不是指針,而是變量,那麼

通過Elem擷取原始值對應的對象則直接panic

通過CanSet方法查詢是否可以設定傳回false

newValue.CantSet()表示是否可以重新設定其值,如果輸出的是true則可修改,否則不能修改,修改完之後再進行列印發現真的已經修改了。

reflect.Value.Elem() 表示擷取原始值對應的反射對象,隻有原始對象才能修改,目前反射對象是不能修改的

也就是說如果要修改反射類型對象,其值必須是可尋址的【對應的要傳入的是指針,同時要通過Elem方法擷取原始值對應的反射對象】

在項目應用中,另外一個常用并且屬于進階的用法,就是通過reflect來進行方法的調用。比如要做架構工程的時候,需要可以随意擴充方法,或者說使用者可以自定義方法,關鍵點在于使用者的自定義方法是未可知的,是以可以通過reflect來搞定。

<code>Call()</code>方法:

通過反射,調用方法。

通過反射,調用函數。

函數像普通的變量一樣,是可以把函數作為一種變量類型的,而且是引用類型。如果說<code>Fun()</code>是一個函數,那麼<code>f1 := Fun</code>也是可以的,那麼<code>f1</code>也是一個函數,如果直接調用<code>f1()</code>,那麼運作的就是<code>Fun()</code>函數。

那麼就先通過<code>ValueOf()</code>來擷取函數的反射對象,可以判斷它的<code>Kind</code>,是一個<code>func</code>,那麼就可以執行<code>Call()</code>進行函數的調用。

要通過反射來調用起對應的方法,必須要先通過reflect.ValueOf(interface)來擷取到reflect.Value,得到“反射類型對象”後才能做下一步處理

reflect.Value.MethodByName這個MethodByName,需要指定準确真實的方法名字,如果錯誤将直接panic,MethodByName傳回一個函數值對應的reflect.Value方法的名字。

[]reflect.Value,這個是最終需要調用的方法的參數,可以沒有或者一個或者多個,根據實際參數來定。

reflect.Value的 Call 這個方法,這個方法将最終調用真實的方法,參數務必保持一緻,如果reflect.Value.Kind不是一個方法,那麼将直接panic。

本來可以用對象通路方法直接調用的,但是如果要通過反射,那麼首先要将方法注冊,也就是MethodByName,然後通過反射調用methodValue.Call

反射是一個強大并富有表現力的工具,能寫出更靈活的代碼。但是反射不應該被濫用,原因有以下三個。

基于反射的代碼是極其脆弱的,反射中的類型錯誤會在真正運作的時候才會引發panic,那很可能是在代碼寫完的很長時間之後。

大量使用反射的代碼通常難以了解,代碼可讀性差。

反射的性能低下,基于反射實作的代碼通常比正常代碼運作速度慢一到兩個數量級。處于運作效率關鍵位置的代碼,請避免使用反射。