歡迎訪問博客原文: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.Reader
與io.Writer
滋戳, 類型 Reader
與 Writer
都來自 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 type
與interface 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ī)制映砖。首先间坐,我們需要在反射包中了解兩種類型:type
和 value
,通過這兩種類型對接口變量內(nèi)容的訪問邑退,還有兩個對應(yīng)的函數(shù)竹宋,稱為 reflect.TypeOf
和reflect.ValueOf
,從接口值中獲取 reflect.Type
和 reflect.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.Type
和reflect.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.Type
和 reflect.Value
都有很多方法可以讓我們檢查和操作它們单绑。 一個重要的例子是 Value
具有 Type
方法,該方法返回 reflect.Value
的 Type
曹宴。另一個是 Type
和 Value
都有 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.
)。