如何理解 Golang 中的反射

歡迎訪問博客原文:http://pengtuo.tech/golang/2019/09/23/golang-reflection/

首先給大家推薦一個在線 Golang 運行環境,可以測試剪短的代碼邏輯。https://play.studygolang.com

Golang 中的反射是基于類型(type)機制的,所以需要重溫一下 Golang 中的類型機制。

一、Types and interfaces

Go 是靜態類型語言。 每個變量都有一個靜態類型,也就是在編譯時已知并固定的一種類型:int,float32,*MyType,[]byte 等。 如果我們聲明:

type MyInt int

var i int
var j MyInt

則變量 i 是 int 類型,變量 j 是 MyInt 類型。變量 i 和 j 具有不同的靜態類型,盡管它們具有相同的基礎類型,但是如果不進行轉換依然無法將其中一個變量賦值于另一個變量。

Go 中一個重要的類別是接口類型(interface),接口表示固定的方法集。接口變量可以存儲任何具體的(非接口)值,只要該值實現了接口中所有定義的方法即可。 一個重要的例子就是io.Readerio.Writer, 類型 ReaderWriter 都來自 io - The Go Programming Language

// 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 可能包含什么具體值,r 的類型始終是 io.Reader:Go是靜態類型的,而 r 的靜態類型是io.Reader

接口類型的一個非常重要的例子是空接口:

interface{}

它表示空方法集,并且任何值都滿足實現了空接口,因為任何值具有零個或多個方法,而空接口沒有方法供實現。

有人說 Go 的空接口是動態類型的,但這會產生誤導。它們是靜態類型的:接口類型的變量始終具有相同的靜態類型,即使在運行時存儲在接口變量中的值可能會更改類型,但該值也還是始終滿足接口的要求。

而之所以先重溫接口就是因為反射和接口息息相關

二、The representation of an 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)。值得注意的是,即使接口變量 r 僅提供對 Read 方法的訪問,但內部的值仍包含有關該值的所有類型信息。所以下面這個代碼也是正確的:

var w io.Writer
w = r.(io.Writer)

這個賦值操作中的表達式是類型斷言。它斷言 r 內的項也實現了 io.Writer,因此我們可以將其分配給接口變量 w。賦值后,w 也同樣包含一對信息 —— (tty,* os.File)。接口的靜態類型會決定使用接口變量調用哪些方法,即使內部的具體值可能具有更大的方法集。

強調一遍,在一個接口變量中一直都是保存一對信息,格式為 (value, concrete type),但是不能保存 (value, interface type) 格式。

在 Go 語言中,變量類型分為兩大類,concrete typeinterface type
concrete type: 指具體的變量類型,可以是基本類型,也可以是自定義類型或者結構體類型;
interface type: 指接口類型,可以是 Golang 內置的接口類型,或者是使用者自定義的接口類型;

三、關于反射

3.1. Reflection goes from interface value to reflection object.

從底層層面來說,反射是一種解釋存儲在接口類型變量中的 (type, value) 對的機制。首先,我們需要在反射包中了解兩種類型:typevalue,通過這兩種類型對接口變量內容的訪問,還有兩個對應的函數,稱為 reflect.TypeOfreflect.ValueOf,從接口值中獲取 reflect.Typereflect.Value 部分。
例如 TypeOf

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var x float64 = 3.4
    fmt.Println("type:", reflect.TypeOf(x))
    fmt.Println("value:", reflect.ValueOf(x))
}

結果輸出為:

type: float64
value: 3.4

說明:

  • reflect.TypeOf:獲得值的類型(type),如 float64、int、pointer、struct 等等真實的類型;
  • reflect.ValueOf:獲得值的內容,如1.2345這個具體數值,或者類似 &{1 “Allen.Wu” 25} 這樣的結構體 struct 的內容;
  • 說明反射可以將“接口類型變量”轉換為“反射類型對象”,反射類型指的是 reflect.Typereflect.Value 這兩個函數的返回;

reflect.TypeOf 的函數簽名包括一個空接口:

// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type

當我們調用 reflect.TypeOf(x)時,x 首先存儲在一個空接口中,然后將其作為參數傳遞; reflect.TypeOf 解壓縮該空接口以恢復類型信息。
又例如:

var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x))
fmt.Println("value:", reflect.ValueOf(x).String())

輸出結果為:

value: 3.4
value: <float64 Value>

reflect.Typereflect.Value 都有很多方法可以讓我們檢查和操作它們。 一個重要的例子是 Value 具有 Type 方法,該方法返回 reflect.ValueType。另一個是 TypeValue 都有 Kind 方法,該方法返回一個常量,指示存儲的項目類型:Uint,Float64,Slice等。

反射庫具有幾個值得一提的屬性。

首先,為使 API 保持簡單,Value 的 “getter” 和 “setter” 方法在可以容納該值的最大類型上運行:例如,所有有符號整數的 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())     

第二個屬性是反射對象的 Kind() 方法描述基礎類型,而不是靜態類型。例如:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    type MyInt int      // 反射對象包含用戶定義的整數類型的值
    var x MyInt = 7
    v := reflect.TypeOf(x)
    fmt.Println(v)
    fmt.Println(v.Kind())
}

則會輸出:

main.MyInt
int

3.2. Reflection goes from reflection object to interface value.

Golang 的反射也有其逆向過程。

給定一個 reflect.Value ,我們可以使用 Interface() 方法恢復接口值,該方法將 type 和 value 信息打包回接口表示形式并返回結果:

// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}

例如:

func main() {
    var xx float64 = 3.4
    v := reflect.ValueOf(xx)     // v is a reflection object
    y := v.Interface().(float64) // y will have type float64.
    fmt.Println(y)
    fmt.Printf("%T", y)
}

輸出結果為:

3.4
float64

簡而言之,Interface方法與ValueOf函數相反,但其結果始終是靜態類型 interface{}

所以綜上述兩點可得知,Golang 中的反射可理解為包含兩個過程,一個是接口值到反射對象的過程,另一個則是反向的反射對象到接口值的過程。

3.3. To modify a reflection object, the value must be settable.

第三條規律則是如果想要修改一個反射對象(reflection object),那么這個對象的值必須是可設置的。直接這樣說會比較困惑,從例子出發:

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.

如果運行上述這個代碼,則會報錯提示:

panic: reflect: reflect.Value.SetFloat using unaddressable value

在這個例子中,反射對象 v 的值就是不可設置的,執行下述代碼:

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())

則會顯示:

settability of v: false

那么什么是可設置的呢,在 Golang 官網原文有這么一句

Settability is determined by whether the reflection object holds the original item.

翻譯過來就是可設置性由反射對象是否保留原始對象確定。我們都知道在 Go 中的參數傳遞都是使用的值傳遞的方法,即將原有值的拷貝傳遞,在剛剛的例子中,我們是傳遞了一個 x 對象的拷貝到 reflect.ValueOf 函數中,而不是 x 對象本身,剛剛的 SetFloat 將更新存儲在反射對象內的 x 的副本,并且 x本身將不受影響,在 Go 中這是不合理的,可設置性就是避免此問題的屬性。

而如果我們想要修改其內容,很簡單,將對象的指針傳入其中,于是剛剛的代碼可以改為:

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())
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())
fmt.Println("----------------")
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(z)

此時輸出:

float64type of p: *float64
settability of p: false
settability of v: true
----------------
7.1
7.1

Structs
反射修改內容一個經常使用的地方就是通過指針修改傳入的結構體的字段值,只要我們能夠獲得該結構體對象的指針。

一個簡單的示例。

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())
}

這里使用結構的地址創建了反射對象,然后稍后將要對其進行修改。將 typeOfT 設置為其類型,并使用簡單的方法調用對字段進行迭代。請注意,我們從結構類型中提取了字段的名稱,但是字段本身是常規的 reflect.Value 對象。這里結果輸出為:

0: A int = 23
1: B string = skidoo

這里有一點要注意的是,結構體 T 的字段名首字母都是大寫,在 Go 中首字母大寫的變量或者函數才是可導出的(exported),相當于 Java 中的 public,而首字母小寫的變量或者函數則是包外不可使用,對應 Java 的 protected。 而只有可導出的結構體字段此方式才能修改。
現在我們可以試著修改結構體 T

s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)

// output is "t is now {77 Sunset Strip}"

四、總結

反射的三條規律:

  • 反射包括從接口值到反射對象的過程;
  • 反射也包括從反射對象到接口值的過程;
  • 要修改反射對象,該值必須可設置(To modify a reflection object, the value must be settable.)。

【參考文獻】

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內容