各位學習Go語言的朋友,週末好,這次跟大家聊一聊Go語言的一個高級話題:反射。
這篇文章是從我過去的學習筆記修改來的,內容主要來自Go Blog的一篇文章《The law of reflection》。
這篇文章主要介紹反射和接口的關係,解釋內在的關係和原理。
反射來自元編程,指通過類型檢查變量本身數據結構的方式,只有部分編程語言支持反射。
類型
反射構建在類型系統之上,Go是靜態類型語言,每一個變量都有靜態類型,在編譯時就確定下來了。
比如:
type MyInt int
var i int
var j MyInt
i和j的底層類型都是int
,但i的靜態類型是int
,j的靜態類型是MyInt
,這兩個是不同類型,是不能直接賦值的,需要類型強制轉換。
接口類型比較特殊,接口類型的變量被多種對象類型賦值,看起來像動態語言的特性,但變量類型始終是接口類型,Go是靜態的。舉例:
var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on
雖然r被3種類型的變量賦值,但r的類型始終是io.Reader
。
最特別:空接口interface{}
的變量可以被任何類型的值賦值,但類型一直都是interface{}
。
接口的表示
Russ Cox(Go語言創始人)在他的博客詳細介紹了Go語言接口,結論是:
接口類型的變量存儲的是一對數據:
- 變量實際的值
- 變量的靜態類型
例子:
var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return nil, err
}
r = tty
r是接口類型變量,保存了值tty和tty的類型*os.File
,所以才能使用類型斷言判斷r保存的值的靜態類型:
var w io.Writer
w = r.(io.Writer)
雖然r中包含了tty和它的類型,包含了tty的所有函數,但r是接口類型,決定了r只能調用接口io.Reader
中包含的函數。
記住:接口變量保存的不是接口類型的值,還是英語說起來更方便:Interfaces do not hold interface values.
反射的3條定律
定律1:從接口值到反射對象
反射是一種檢測存儲在接口變量中值和類型的機制。通過reflect
包的一些函數,可以把接口轉換爲反射定義的對象。
掌握reflect
包的以下函數:
-
reflect.ValueOf({}interface) reflect.Value
:獲取某個變量的值,但值是通過reflect.Value
對象描述的。 -
reflect.TypeOf({}interface) reflect.Type
:獲取某個變量的靜態類型,但值是通過reflect.Type
對象描述的,是可以直接使用Println
打印的。 -
reflect.Value.Kind() Kind
:獲取變量值的底層類型(類別),注意不是類型,是Int、Float,還是Struct,還是Slice,具體見此。 -
reflect.Value.Type() reflect.Type
:獲取變量值的類型,效果等同於reflect.TypeOf
。
再解釋下Kind和Type的區別,比如:
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
v.Kind()得到的是Int,而Type得到是MyInt
。
定律2:從反射對象到接口值
定律2是定律1的逆向過程,上面我們學了:普通變量 -> 接口變量 -> 反射對象
的過程,這是從反射對象 -> 接口變量
的過程,使用的是Value
的Interface
函數,是把實際的值賦值給空接口變量,它的聲明如下:
func (v Value) Interface() (i interface{})
回憶一下:接口變量存儲了實際的值和值的類型,Println
可以根據接口變量實際存儲的類型自動識別其值並打印。
注意事項:如果Value是結構體的非導出字段,調用該函數會導致panic。
定律3:當反射對象所存的值是可設置時,反射對象纔可修改
從定律1入手理解,定律3就不再那麼難懂。
Settability is a property of a reflection Value, and not all reflection Values have it.
可設置指的是,可以通過Value設置原始變量的值。
通過函數的例子思考一下可設置:
func f(x int)
在調用f的時候,傳入了參數x,從函數內部修改x的值,外部的變量的值並不會發生改變,因爲這種是傳值,是拷貝的傳遞方式。
func f(p *int)
函數f的入參是指針類型,在函數內部的修改變量的值,函數外部變量的值也會跟着變化。
使用反射也是這個原理,如果創建Value時傳遞的是變量,則Value是不可設置的。如果創建Value時傳遞的是變量地址,則Value是可設置的。
可以使用Value.CanSet()
檢測是否可以通過此Value修改原始變量的值。
x := 10
v1 := reflect.ValueOf(x)
fmt.Println("setable:", v1.CanSet())
p := reflect.ValueOf(&x)
fmt.Println("setable:", p.CanSet())
v2 := p.Elem()
fmt.Println("setable:", v2.CanSet())
如何通過Value設置原始對象值呢?
Value.SetXXX()
系列函數可設置Value中原始對象的值。
系列函數有:
- Value.SetInt()
- Value.SetUint()
- Value.SetBool()
- Value.SetBytes()
- Value.SetFloat()
- Value.SetString()
- ...
設置函數這麼多,到底該選用哪個Set函數?
根據Value.Kind()
的結果去獲得變量的底層類型,然後選用該類別的Set函數。
參考資料
- 如果這篇文章對你有幫助,請點個贊/喜歡,感謝。
- 本文作者:大彬
- 如果喜歡本文,隨意轉載,但請保留此原文鏈接:http://lessisbetter.site/2019/02/24/go-law-of-reflect/