Reflection(反射)在計算機中表示 程序可以檢查自身結構的能力,尤爲是類型。它是元編程的一種形式,也是最容易讓人迷惑的一部分。golang
本文中,咱們將解釋Go語言中反射的運做機制。每一個編程語言的反射模型不大相同,不少語言索性就不支持反射(C、C++)。因爲本文是介紹Go語言的,因此當咱們談到「反射」時,默認爲是Go語言中的反射。編程
本文中,咱們將解釋Go語言中反射的運做機制。每一個編程語言的反射模型不大相同,不少語言索性就不支持反射(C、C++)。微信
因爲本文是介紹Go語言的,因此當咱們談到「反射」時,默認爲是Go語言中的反射。編程語言
雖然Go語言沒有繼承的概念,但爲了便於理解,若是一個struct A 實現了 interface B的全部方法時,咱們稱之爲「繼承」。函數
反射創建在類型系統之上,所以咱們從類型基礎知識提及。工具
Go是靜態類型語言。每一個變量都有且只有一個靜態類型,在編譯時就已經肯定。好比 int、float3二、*MyType、[]byte。 若是咱們作出以下聲明:ui
type MyInt int var i int var j MyInt
上面的代碼中,變量 i 的類型是 int,j 的類型是 MyInt。 因此,儘管變量 i 和 j 具備共同的底層類型 int,但它們的靜態類型並不同。不通過類型轉換直接相互賦值時,編譯器會報錯。spa
關於類型,一個重要的分類是 接口類型(interface),每一個接口類型都表明固定的方法集合。一個接口變量就能夠存儲(或「指向」,接口變量相似於指針)任何類型的具體值,只要這個值實現了該接口類型的全部方法。一組廣爲人知的例子是 io.Reader 和 io.Writer, Reader 和 Writer 類型來源於 io包,聲明以下:翻譯
// Reader is the interface that wraps the basic Read method. type Reader interface { Read(p []byte) (n int, err error) } // Writer is the interface that wraps the basic Write method. type Writer interface { Write(p []byte) (n int, err error) }
任何實現了 Read(Write)方法的類型,咱們都稱之爲繼承了 io.Reader(io.Writer)接口。換句話說, 一個類型爲 io.Reader 的變量 能夠指向(接口變量相似於指針)任何類型的變量,只要這個類型實現了Read 方法:設計
var r io.Reader r = os.Stdin r = bufio.NewReader(r) r = new(bytes.Buffer) // and so on
要時刻牢記:無論變量 r 指向的具體值是什麼,它的類型永遠是 io.Reader。再重複一次:Go語言是靜態類型語言,變量 r 的靜態類型是 io.Reader。
一個很是很是重要的接口類型是空接口,即:
interface{}
它表明一個空集,沒有任何方法。因爲任何具體的值都有 零或更多個方法,所以類型爲interface{} 的變量可以存儲任何值。
有人說,Go的接口是動態類型的。這個說法是錯的!接口變量也是靜態類型的,它永遠只有一個相同的靜態類型。若是在運行時它存儲的值發生了變化,這個值也必須知足接口類型的方法集合。
因爲反射和接口二者的關係很密切,咱們必須澄清這一點。
Russ Cox 在2009年寫了一篇文章介紹 Go中接口變量的表示形式,具體參考文章末尾的連接「Go語言接口的表示」。這裏咱們不須要重複全部的細節,只作一個簡單的總結。
Interface變量存儲一對值:賦給該變量的具體的值、值類型的描述符。更準確一點來講,值就是實現該接口的底層數據,類型是底層數據類型的描述。舉個例子:
var r io.Reader tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) if err != nil { return nil, err } r = tty
在這個例子中,變量 r 在結構上包含一個 (value, type) 對:(tty, os.File)。注意:類型 os.File 不只僅實現了 Read 方法。雖然接口變量只提供 Read 函數的調用權,可是底層的值包含了關於這個值的全部類型信息。因此咱們可以作這樣的類型轉換:
var w io.Writer w = r.(io.Writer)
上面代碼的第二行是一個類型斷言,它判定變量 r 內部的實際值也繼承了 io.Writer接口,因此才能被賦值給 w。賦值以後,w 就指向了 (tty, *os.File) 對,和變量 r 指向的是同一個 (value, type) 對。無論底層具體值的方法集有多大,因爲接口的靜態類型限制,接口變量只能調用特定的一些方法。
咱們繼續往下看:
var empty interface{} empty = w
這裏的空接口變量 empty 也包含 (tty, *os.File) 對。這一點很容易理解:空接口變量能夠存儲任何具體值以及該值的全部描述信息。
細心的朋友可能會發現,這裏沒有使用類型斷言,由於變量 w 知足 空接口的全部方法(傳說中的「無招勝有招」)。在前一個例子中,咱們把一個具體值 從 io.Reader 轉換爲 io.Writer 時,須要顯式的類型斷言,是由於 io.Writer 的方法集合 不是 io.Reader 的子集。
另外須要注意的一點是,(value, type) 對中的 type 必須是 具體的類型(struct或基本類型),不能是 接口類型。 接口類型不能存儲接口變量。
關於接口,咱們就介紹到這裏,下面咱們看看Go語言的反射三定律。
注:這裏反射類型指 reflect.Type
和 reflect.Value
。
從用法上來說,反射提供了一種機制,容許程序在運行時檢查接口變量內部存儲的 (value, type) 對。在最開始,咱們先了解下 reflect 包的兩種類型:Type 和 Value。這兩種類型使訪問接口內的數據成爲可能。它們對應兩個簡單的方法,分別是 reflect.TypeOf 和 reflect.ValueOf,分別用來讀取接口變量的 reflect.Type 和 reflect.Value 部分。固然,從 reflect.Value 也很容易獲取到 reflect.Type。目前咱們先將它們分開。
首先,咱們下看 reflect.TypeOf:
package main import ( "fmt" "reflect" ) func main() { var x float64 = 3.4 fmt.Println("type:", reflect.TypeOf(x)) }
這段代碼會打印出:
type: float64
你可能會疑惑:爲何沒看到接口?這段代碼看起來只是把一個 float64類型的變量 x 傳遞給 reflect.TypeOf,並無傳遞接口。事實上,接口就在那裏。查閱一下TypeOf 的文檔,你會發現 reflect.TypeOf 的函數簽名裏包含一個空接口:
// TypeOf returns the reflection Type of the value in the interface{}. func TypeOf(i interface{}) Type
咱們調用 reflect.TypeOf(x) 時,x 被存儲在一個空接口變量中被傳遞過去; 而後reflect.TypeOf 對空接口變量進行拆解,恢復其類型信息。
函數 reflect.ValueOf 也會對底層的值進行恢復(這裏咱們忽略細節,只關注可執行的代碼):
var x float64 = 3.4 fmt.Println("value:", reflect.ValueOf(x))
上面這段代碼打印出:
value: 3.4
類型 reflect.Type 和 reflect.Value 都有不少方法,咱們能夠檢查和使用它們。這裏咱們舉幾個例子。類型 reflect.Value 有一個方法 Type(),它會返回一個 reflect.Type 類型的對象。Type和 Value都有一個名爲 Kind 的方法,它會返回一個常量,表示底層數據的類型,常見值有:Uint、Float6四、Slice等。Value類型也有一些相似於Int、Float的方法,用來提取底層的數據。Int方法用來提取 int64, Float方法用來提取 float64,參考下面的代碼:
var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) fmt.Println("kind is float64:", v.Kind() == reflect.Float64) fmt.Println("value:", v.Float())
上面這段代碼會打印出:
type: float64 kind is float64: true value: 3.4
還有一些用來修改數據的方法,好比SetInt、SetFloat,在討論它們以前,咱們要先理解「可修改性」(settability),這一特性會在「反射第三定律」中進行詳細說明。
反射庫提供了不少值得列出來單獨討論的屬性。首先是介紹下Value 的 getter 和 setter 方法。爲了保證API 的精簡,這兩個方法操做的是某一組類型範圍最大的那個。好比,處理任何含符號整型數,都使用 int64。也就是說 Value 類型的Int 方法返回值爲 int64類型,SetInt 方法接收的參數類型也是 int64 類型。實際使用時,可能須要轉化爲實際的類型:
var x uint8 = 'x' v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) // uint8. fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true. x = uint8(v.Uint()) // v.Uint returns a uint64.
第二個屬性是反射類型變量(reflection object)的 Kind 方法 會返回底層數據的類型,而不是靜態類型。若是一個反射類型對象包含一個用戶定義的整型數,看代碼:
type MyInt int var x MyInt = 7 v := reflect.ValueOf(x)
上面的代碼中,雖然變量 v 的靜態類型是MyInt,不是 int,Kind 方法仍然返回 reflect.Int。換句話說, Kind 方法不會像 Type 方法同樣區分 MyInt 和 int。
和物理學中的反射相似,Go語言中的反射也能創造本身反面類型的對象。
根據一個 reflect.Value 類型的變量,咱們可使用 Interface 方法恢復其接口類型的值。事實上,這個方法會把 type 和 value 信息打包並填充到一個接口變量中,而後返回。其函數聲明以下:
// Interface returns v's value as an interface{}. func (v Value) Interface() interface{}
而後,咱們能夠經過斷言,恢復底層的具體值:
y := v.Interface().(float64) // y will have type float64. fmt.Println(y)
上面這段代碼會打印出一個 float64 類型的值,也就是 反射類型變量 v 所表明的值。
事實上,咱們能夠更好地利用這一特性。標準庫中的 fmt.Println 和 fmt.Printf 等函數都接收空接口變量做爲參數,fmt 包內部會對接口變量進行拆包(前面的例子中,咱們也作過相似的操做)。所以,fmt 包的打印函數在打印 reflect.Value 類型變量的數據時,只須要把 Interface 方法的結果傳給 格式化打印程序:
fmt.Println(v.Interface())
你可能會問:問什麼不直接打印 v ,好比 fmt.Println(v)? 答案是 v 的類型是 reflect.Value,咱們須要的是它存儲的具體值。因爲底層的值是一個 float64,咱們能夠格式化打印:
fmt.Printf("value is %7.1e\n", v.Interface())
上面代碼的打印結果是:
3.4e+00
一樣,此次也不須要對 v.Interface() 的結果進行類型斷言。空接口值內部包含了具體值的類型信息,Printf 函數會恢復類型信息。
簡單來講,Interface 方法和 ValueOf 函數做用剛好相反,惟一一點是,返回值的靜態類型是 interface{}。
咱們從新表述一下:Go的反射機制能夠將「接口類型的變量」轉換爲「反射類型的對象」,而後再將「反射類型對象」轉換過去。
這條定律很微妙,也很容易讓人迷惑。可是若是你從第一條定律開始看,應該比較容易理解。
下面這段代碼不能正常工做,可是很是值得研究:
var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat(7.1) // Error: will panic.
若是你運行這段代碼,它會拋出拋出一個奇怪的異常:
panic: reflect.Value.SetFloat using unaddressable value
這裏問題不在於值 7.1 不能被尋址,而是由於變量 v 是「不可寫的」。「可寫性」是反射類型變量的一個屬性,但不是全部的反射類型變量都擁有這個屬性。
咱們能夠經過 CanSet 方法檢查一個 reflect.Value 類型變量的「可寫性」。對於上面的例子,能夠這樣寫:
var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("settability of v:", v.CanSet())
上面這段代碼打印結果是:
settability of v: false
對於一個不具備「可寫性」的 Value類型變量,調用 Set 方法會報出錯誤。首先,咱們要弄清楚什麼「可寫性」。
「可寫性」有些相似於尋址能力,可是更嚴格。它是反射類型變量的一種屬性,賦予該變量修改底層存儲數據的能力。「可寫性」最終是由一個事實決定的:反射對象是否存儲了原始值。舉個代碼例子:
var x float64 = 3.4 v := reflect.ValueOf(x)
這裏咱們傳遞給 reflect.ValueOf 函數的是變量 x 的一個拷貝,而非 x 自己。想象一下,若是下面這行代碼可以成功執行:
v.SetFloat(7.1)
答案是:若是這行代碼可以成功執行,它不會更新 x ,雖然看起來變量 v 是根據 x 建立的。相反,它會更新 x 存在於 反射對象 v 內部的一個拷貝,而變量 x 自己徹底不受影響。這會形成迷惑,而且沒有任何意義,因此是不合法的。「可寫性」就是爲了不這個問題而設計的。
這看起來很詭異,事實上並不是如此,並且相似的狀況很常見。考慮下面這行代碼:
f(x)
上面的代碼中,咱們把變量 x 的一個拷貝傳遞給函數,所以不指望它會改變 x 的值。若是指望函數 f 可以修改變量 x,咱們必須傳遞 x 的地址(即指向 x 的指針)給函數 f,以下:
f(&x)
你應該很熟悉這行代碼,反射的工做機制是同樣的。若是你想經過反射修改變量 x,就咬吧想要修改的變量的指針傳遞給 反射庫。
首先,像一般同樣初始化變量 x,而後建立一個指向它的 反射對象,名字爲 p:
var x float64 = 3.4 p := reflect.ValueOf(&x) // Note: take the address of x. fmt.Println("type of p:", p.Type()) fmt.Println("settability of p:", p.CanSet())
這段代碼的輸出是:
type of p: *float64 settability of p: false
反射對象 p 是不可寫的,可是咱們也不像修改 p,事實上咱們要修改的是 *p。爲了獲得 p 指向的數據,能夠調用 Value 類型的 Elem 方法。Elem 方法可以對指針進行「解引用」,而後將結果存儲到反射 Value類型對象 v中:
v := p.Elem() fmt.Println("settability of v:", v.CanSet())
在上面這段代碼中,變量 v 是一個可寫的反射對象,代碼輸出也驗證了這一點:
settability of v: true
因爲變量 v 表明 x, 所以咱們可使用 v.SetFloat 修改 x 的值:
v.SetFloat(7.1) fmt.Println(v.Interface()) fmt.Println(x)
上面代碼的輸出以下:
7.1 7.1
反射不太容易理解,reflect.Type 和 reflect.Value 會混淆正在執行的程序,可是它作的事情正是編程語言作的事情。你只須要記住:只要反射對象要修改它們表示的對象,就必須獲取它們表示的對象的地址。
在前面的例子中,變量 v 自己並非指針,它只是從指針衍生而來。把反射應用到結構體時,經常使用的方式是 使用反射修改一個結構體的某些字段。只要擁有結構體的地址,咱們就能夠修改它的字段。
下面經過一個簡單的例子對結構體類型變量 t 進行分析。
首先,咱們建立了反射類型對象,它包含一個結構體的指針,由於後續會修改。
而後,咱們設置 typeOfT 爲它的類型,並遍歷全部的字段。
注意:咱們從 struct 類型提取出每一個字段的名字,可是每一個字段自己也是常規的 reflect.Value 對象。
type T struct { A int B string } t := T{23, "skidoo"} s := reflect.ValueOf(&t).Elem() typeOfT := s.Type() for i := 0; i < s.NumField(); i++ { f := s.Field(i) fmt.Printf("%d: %s %s = %v\n", i, typeOfT.Field(i).Name, f.Type(), f.Interface()) }
上面這段代碼的輸出以下:
0: A int = 23 1: B string = skidoo
這裏還有一點須要指出:變量 T 的字段都是首字母大寫的(暴露到外部),由於struct中只有暴露到外部的字段纔是「可寫的」。
因爲變量 s 包含一個「可寫的」反射對象,咱們能夠修改結構體的字段:
f.Interface())s.Field(0).SetInt(77) s.Field(1).SetString("Sunset Strip") fmt.Println("t is now", t)
上面代碼的輸出以下:
t is now {77 Sunset Strip}
若是變量 s 是經過 t ,而不是 &t 建立的,調用 SetInt 和 SetString 將會失敗,由於 t 的字段不是「可寫的」。
最後再次重複一遍反射三定律:
一旦你理解了這些定律,使用反射將會是一件很是簡單的事情。它是一件強大的工具,使用時務必謹慎使用,更不要濫用。
關於反射,咱們還有不少內容沒有討論,包括基於管道的發送和接收、內存分配、使用slice和map、調用方法和函數,因爲本文已經很是長了,這些話題在後續的文章中介紹。
原做者 Rob Pike,翻譯Oscar
相關連接:
掃碼關注微信公衆號「深刻Go語言」