如何理解 Golang 中的反射

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

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

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

一、Types and interfaces

Go 是靜態(tài)類型語言芍瑞。 每個變量都有一個靜態(tài)類型挟鸠,也就是在編譯時已知并固定的一種類型:int童太,float32,*MyType栓始,[]byte 等务冕。 如果我們聲明:

type MyInt int

var i int
var j MyInt

則變量 i 是 int 類型,變量 j 是 MyInt 類型幻赚。變量 i 和 j 具有不同的靜態(tài)類型禀忆,盡管它們具有相同的基礎(chǔ)類型臊旭,但是如果不進(jìn)行轉(zhuǎn)換依然無法將其中一個變量賦值于另一個變量。

Go 中一個重要的類別是接口類型(interface)箩退,接口表示固定的方法集离熏。接口變量可以存儲任何具體的(非接口)值,只要該值實(shí)現(xiàn)了接口中所有定義的方法即可戴涝。 一個重要的例子就是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)
}

任何只要實(shí)現(xiàn)了 Read 或者 Write 方法的類型都算作實(shí)現(xiàn)了 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是靜態(tài)類型的,而 r 的靜態(tài)類型是io.Reader郑什。

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

interface{}

它表示空方法集府喳,并且任何值都滿足實(shí)現(xiàn)了空接口,因為任何值具有零個或多個方法蘑拯,而空接口沒有方法供實(shí)現(xiàn)钝满。

有人說 Go 的空接口是動態(tài)類型的,但這會產(chǎn)生誤導(dǎo)申窘。它們是靜態(tài)類型的:接口類型的變量始終具有相同的靜態(tài)類型弯蚜,即使在運(yùn)行時存儲在接口變量中的值可能會更改類型,但該值也還是始終滿足接口的要求剃法。

而之所以先重溫接口就是因為反射和接口息息相關(guān)

二碎捺、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) 對收厨,內(nèi)容為 (tty, *os.File)。值得注意的是优构,即使接口變量 r 僅提供對 Read 方法的訪問诵叁,但內(nèi)部的值仍包含有關(guān)該值的所有類型信息。所以下面這個代碼也是正確的:

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

這個賦值操作中的表達(dá)式是類型斷言钦椭。它斷言 r 內(nèi)的項也實(shí)現(xiàn)了 io.Writer拧额,因此我們可以將其分配給接口變量 w。賦值后彪腔,w 也同樣包含一對信息 —— (tty侥锦,* os.File)。接口的靜態(tài)類型會決定使用接口變量調(diào)用哪些方法德挣,即使內(nèi)部的具體值可能具有更大的方法集恭垦。

強(qiáng)調(diào)一遍,在一個接口變量中一直都是保存一對信息,格式為 (value, concrete type)署照,但是不能保存 (value, interface type) 格式祸泪。

在 Go 語言中,變量類型分為兩大類建芙,concrete typeinterface type
concrete type: 指具體的變量類型没隘,可以是基本類型,也可以是自定義類型或者結(jié)構(gòu)體類型禁荸;
interface type: 指接口類型右蒲,可以是 Golang 內(nèi)置的接口類型,或者是使用者自定義的接口類型赶熟;

三瑰妄、關(guān)于反射

3.1. Reflection goes from interface value to reflection object.

從底層層面來說,反射是一種解釋存儲在接口類型變量中的 (type, value) 對的機(jī)制映砖。首先间坐,我們需要在反射包中了解兩種類型:typevalue,通過這兩種類型對接口變量內(nèi)容的訪問邑退,還有兩個對應(yīng)的函數(shù)竹宋,稱為 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))
}

結(jié)果輸出為:

type: float64
value: 3.4

說明:

  • reflect.TypeOf:獲得值的類型(type)蜈七,如 float64、int莫矗、pointer飒硅、struct 等等真實(shí)的類型;
  • reflect.ValueOf:獲得值的內(nèi)容作谚,如1.2345這個具體數(shù)值三娩,或者類似 &{1 “Allen.Wu” 25} 這樣的結(jié)構(gòu)體 struct 的內(nèi)容;
  • 說明反射可以將“接口類型變量”轉(zhuǎn)換為“反射類型對象”食磕,反射類型指的是 reflect.Typereflect.Value 這兩個函數(shù)的返回尽棕;

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

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

當(dāng)我們調(diào)用 reflect.TypeOf(x)時喳挑,x 首先存儲在一個空接口中彬伦,然后將其作為參數(shù)傳遞; reflect.TypeOf 解壓縮該空接口以恢復(fù)類型信息伊诵。
又例如:

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

輸出結(jié)果為:

value: 3.4
value: <float64 Value>

reflect.Typereflect.Value 都有很多方法可以讓我們檢查和操作它們单绑。 一個重要的例子是 Value 具有 Type 方法,該方法返回 reflect.ValueType曹宴。另一個是 TypeValue 都有 Kind 方法搂橙,該方法返回一個常量,指示存儲的項目類型:Uint笛坦,F(xiàn)loat64区转,Slice等苔巨。

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

首先废离,為使 API 保持簡單侄泽,Value 的 “getter” 和 “setter” 方法在可以容納該值的最大類型上運(yùn)行:例如,所有有符號整數(shù)的 int64蜻韭。 也就是說悼尾,Value 的 Int 方法返回一個 int64,而 SetInt 值采用一個 int64肖方; 可能需要轉(zhuǎn)換為涉及的實(shí)際類型:

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() 方法描述基礎(chǔ)類型闺魏,而不是靜態(tài)類型。例如:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    type MyInt int      // 反射對象包含用戶定義的整數(shù)類型的值
    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() 方法恢復(fù)接口值,該方法將 type 和 value 信息打包回接口表示形式并返回結(jié)果:

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

輸出結(jié)果為:

3.4
float64

簡而言之艰垂,Interface方法與ValueOf函數(shù)相反烹骨,但其結(jié)果始終是靜態(tài)類型 interface{}

所以綜上述兩點(diǎn)可得知材泄,Golang 中的反射可理解為包含兩個過程沮焕,一個是接口值到反射對象的過程,另一個則是反向的反射對象到接口值的過程拉宗。

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

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

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

如果運(yùn)行上述這個代碼魁巩,則會報錯提示:

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

在這個例子中,反射對象 v 的值就是不可設(shè)置的姐浮,執(zhí)行下述代碼:

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

則會顯示:

settability of v: false

那么什么是可設(shè)置的呢谷遂,在 Golang 官網(wǎng)原文有這么一句

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

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

而如果我們想要修改其內(nèi)容,很簡單莲绰,將對象的指針傳入其中欺旧,于是剛剛的代碼可以改為:

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
反射修改內(nèi)容一個經(jīng)常使用的地方就是通過指針修改傳入的結(jié)構(gòu)體的字段值,只要我們能夠獲得該結(jié)構(gòu)體對象的指針蛤签。

一個簡單的示例切端。

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

這里使用結(jié)構(gòu)的地址創(chuàng)建了反射對象,然后稍后將要對其進(jìn)行修改顷啼。將 typeOfT 設(shè)置為其類型踏枣,并使用簡單的方法調(diào)用對字段進(jìn)行迭代。請注意钙蒙,我們從結(jié)構(gòu)類型中提取了字段的名稱茵瀑,但是字段本身是常規(guī)的 reflect.Value 對象。這里結(jié)果輸出為:

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

這里有一點(diǎn)要注意的是躬厌,結(jié)構(gòu)體 T 的字段名首字母都是大寫马昨,在 Go 中首字母大寫的變量或者函數(shù)才是可導(dǎo)出的(exported),相當(dāng)于 Java 中的 public扛施,而首字母小寫的變量或者函數(shù)則是包外不可使用鸿捧,對應(yīng) Java 的 protected。 而只有可導(dǎo)出的結(jié)構(gòu)體字段此方式才能修改疙渣。
現(xiàn)在我們可以試著修改結(jié)構(gòu)體 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}"

四匙奴、總結(jié)

反射的三條規(guī)律:

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

【參考文獻(xiàn)】

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末啦租,一起剝皮案震驚了整個濱河市哗伯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌篷角,老刑警劉巖焊刹,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異恳蹲,居然都是意外死亡虐块,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門阱缓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來非凌,“玉大人举农,你說我怎么就攤上這事荆针。” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵航背,是天一觀的道長喉悴。 經(jīng)常有香客問我,道長玖媚,這世上最難降的妖魔是什么箕肃? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮今魔,結(jié)果婚禮上勺像,老公的妹妹穿的比我還像新娘。我一直安慰自己错森,他們只是感情好吟宦,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著涩维,像睡著了一般殃姓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瓦阐,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天蜗侈,我揣著相機(jī)與錄音,去河邊找鬼睡蟋。 笑死踏幻,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的戳杀。 我是一名探鬼主播叫倍,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼豺瘤!你這毒婦竟也來了吆倦?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤坐求,失蹤者是張志新(化名)和其女友劉穎蚕泽,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體桥嗤,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡须妻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了泛领。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片荒吏。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖渊鞋,靈堂內(nèi)的尸體忽然破棺而出绰更,到底是詐尸還是另有隱情瞧挤,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布儡湾,位于F島的核電站特恬,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏徐钠。R本人自食惡果不足惜癌刽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望尝丐。 院中可真熱鬧显拜,春花似錦、人聲如沸爹袁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽呢簸。三九已至矮台,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間根时,已是汗流浹背瘦赫。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蛤迎,地道東北人确虱。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像替裆,于是被迫代替她去往敵國和親校辩。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容