36. 圖解:Go 語言的反射三定律,也沒什麼難的嘛

Hi,你們好,我是明哥。git

在本身學習 Golang 的這段時間裏,我寫了詳細的學習筆記放在個人我的微信公衆號 《Go編程時光》,對於 Go 語言,我也算是個初學者,所以寫的東西應該會比較適合剛接觸的同窗,若是你也是剛學習 Go 語言,不防關注一下,一塊兒學習,一塊兒成長。github

個人在線博客:golang.iswbm.comgolang

個人 Github:github.com/iswbm/GolangCodingTime編程


當我在使用 Python 的時候,我甚至能夠作到不須要知道什麼是內省,什麼是反射,就能夠當即使用內省去作一些事情。segmentfault

而在學習 Go 語言後,反射在我這卻變成了一個難點,一直感受這個 反射對象 的概念異常的抽象。微信

這篇文章仍是會跟上篇文章同樣,儘可能使用圖解來解釋一些抽象的概念,若是是我理解有誤,還但願你在文章尾部給我留言指正,謝謝。函數

關於反射的內容,我分爲了好幾篇,這一篇是入門篇,會從經典的反射三大定律入手,寫一些 demo 代碼,告訴你反射的基本內容。學習

1. 真實世界與反射世界

在本篇文章裏,爲了區分反射先後的變量值類型,我將反射前環境稱爲 真實世界,而將反射後的環境稱爲 反射世界。這種比喻不嚴謹,可是對於我理解是有幫助的,也但願對你有用。ui

在反射的世界裏,咱們擁有了獲取一個對象的類型,屬性及方法的能力。spa

2. 兩種類型:Type 和 Value

在 Go 反射的世界裏,有兩種類型很是重要,是整個反射的核心,在學習 reflect 包的使用時,先得學習下這兩種類型:

  1. reflect.Type
  2. reflect.Value

它們分別對應着真實世界裏的 type 和 value,只不過在反射對象裏,它們擁有更多的內容。

從源碼上來看,reflect.Type 是以一個接口的形式存在的

type Type interface {
    Align() int
    FieldAlign() int
    Method(int) Method
    MethodByName(string) (Method, bool)
    NumMethod() int
    Name() string
    PkgPath() string
    Size() uintptr
    String() string
    Kind() Kind
    Implements(u Type) bool
    AssignableTo(u Type) bool
    ConvertibleTo(u Type) bool
    Comparable() bool
    Bits() int
    ChanDir() ChanDir
    IsVariadic() bool
    Elem() Type
    Field(i int) StructField
    FieldByIndex(index []int) StructField
    FieldByName(name string) (StructField, bool)
    FieldByNameFunc(match func(string) bool) (StructField, bool)
    In(i int) Type
    Key() Type
    Len() int
    NumField() int
    NumIn() int
    NumOut() int
    Out(i int) Type
    common() *rtype
    uncommon() *uncommonType
}複製代碼

而 reflect.Value 是以一個結構體的形式存在,

type Value struct {
    typ *rtype
    ptr unsafe.Pointer
    flag
}複製代碼

同時它接收了不少的方法(見下表),這裏出於篇幅的限制這裏也沒辦法一一介紹。

Addr
Bool
Bytes
runes
CanAddr
CanSet
Call
CallSlice
call
Cap
Close
Complex
Elem
Field
FieldByIndex
FieldByName
FieldByNameFunc
Float
Index
Int
CanInterface
Interface
InterfaceData
IsNil
IsValid
IsZero
Kind
Len
MapIndex
MapKeys
MapRange
Method
NumMethod
MethodByName
NumField
OverflowComplex
OverflowFloat
OverflowInt
OverflowUint
Pointer
Recv
recv
Send
send
Set
SetBool
SetBytes
setRunes
SetComplex
SetFloat
SetInt
SetLen
SetCap
SetMapIndex
SetUint
SetPointer
SetString
Slice
Slice3
String
TryRecv
TrySend
Type
Uint
UnsafeAddr
assignTo
Convert複製代碼

經過上一節的內容(),咱們知道了一個接口變量,實際上都是由一 pair 對(type 和 data)組合而成,pair 對中記錄着實際變量的值和類型。也就是說在真實世界裏,type 和 value 是合併在一塊兒組成 接口變量的。

而在反射的世界裏,type 和 data 倒是分開的,他們分別由 reflect.Type 和 reflect.Value 來表現。

3. 解讀反射的三大定律

Go 語言裏有個反射三定律,是你在學習反射時,很重要的參考:

  1. Reflection goes from interface value to reflection object.
  2. Reflection goes from reflection object to interface value.
  3. To modify a reflection object, the value must be settable.

翻譯一下,就是:

  1. 反射能夠將接口類型變量 轉換爲「反射類型對象」;
  2. 反射能夠將 「反射類型對象」轉換爲 接口類型變量;
  3. 若是要修改 「反射類型對象」 其類型必須是 可寫的;

第必定律

Reflection goes from interface value to reflection object.

爲了實現從接口變量到反射對象的轉換,須要提到 reflect 包裏很重要的兩個方法:

  1. reflect.TypeOf(i) :得到接口值的類型
  2. reflect.ValueOf(i):得到接口值的值

這兩個方法返回的對象,咱們稱之爲反射對象:Type object 和 Value object。

golang reflection

舉個例子,看下這兩個方法是如何使用的?

package main

import (
"fmt"
"reflect"
)

func main() {
    var age interface{} = 25

    fmt.Printf("原始接口變量的類型爲 %T,值爲 %v \n", age, age)

    t := reflect.TypeOf(age)
    v := reflect.ValueOf(age)

    // 從接口變量到反射對象
    fmt.Printf("從接口變量到反射對象:Type對象的類型爲 %T \n", t)
    fmt.Printf("從接口變量到反射對象:Value對象的類型爲 %T \n", v)

}複製代碼

輸出以下

原始接口變量的類型爲 int,值爲 25 
從接口變量到反射對象:Type對象的類型爲 *reflect.rtype 
從接口變量到反射對象:Value對象的類型爲 reflect.Value 複製代碼

如此咱們完成了從接口類型變量到反射對象的轉換。

等等,上面咱們定義的 age 不是 int 類型的嗎?第一法則裏怎麼會說是接口類型的呢?

關於這點,其實在上一節(關於接口的三個 『潛規則』)已經提到過了,因爲 TypeOf 和 ValueOf 兩個函數接收的是 interface{} 空接口類型,而 Go 語言函數都是值傳遞,所以Go語言會將咱們的類型隱式地轉換成接口類型。

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

// ValueOf returns a new Value initialized to the concrete value stored in the interface i. ValueOf(nil) returns the zero Value.
func ValueOf(i interface{}) Value複製代碼

第二定律

Reflection goes from reflection object to interface value.

和第必定律恰好相反,第二定律描述的是,從反射對象到接口變量的轉換。

golang reflection

經過源碼可知, reflect.Value 的結構體會接收 Interface 方法,返回了一個 interface{} 類型的變量(注意:只有 Value 才能逆向轉換,而 Type 則不行,這也很容易理解,若是 Type 能逆向,那麼逆向成什麼呢?

// Interface returns v's current value as an interface{}.
// It is equivalent to:
//    var i interface{} = (v's underlying value)
// It panics if the Value was obtained by accessing
// unexported struct fields.
func (v Value) Interface() (i interface{}) {
    return valueInterface(v, true)
}複製代碼

這個函數就是咱們用來實現將反射對象轉換成接口變量的一個橋樑。

例子以下

package main

import (
"fmt"
"reflect"
)

func main() {
    var age interface{} = 25

    fmt.Printf("原始接口變量的類型爲 %T,值爲 %v \n", age, age)

    t := reflect.TypeOf(age)
    v := reflect.ValueOf(age)

    // 從接口變量到反射對象
    fmt.Printf("從接口變量到反射對象:Type對象的類型爲 %T \n", t)
    fmt.Printf("從接口變量到反射對象:Value對象的類型爲 %T \n", v)

    // 從反射對象到接口變量
    i := v.Interface()
    fmt.Printf("從反射對象到接口變量:新對象的類型爲 %T 值爲 %v \n", i, i)

}複製代碼

輸出以下

原始接口變量的類型爲 int,值爲 25 
從接口變量到反射對象:Type對象的類型爲 *reflect.rtype 
從接口變量到反射對象:Value對象的類型爲 reflect.Value 
從反射對象到接口變量:新對象的類型爲 int 值爲 25 複製代碼

固然了,最後轉換後的對象,靜態類型爲 interface{} ,若是要轉成最初的原始類型,須要再類型斷言轉換一下,關於這點,我已經在上一節裏講解過了,你能夠點此前往復習:()。

i := v.Interface().(int)複製代碼

至此,咱們已經學習了反射的兩大定律,對這兩個定律的理解,我畫了一張圖,你能夠用下面這張圖來增強理解,方便記憶。

第三定律

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

反射世界是真實世界的一個『映射』,是個人一個描述,但這並不嚴格,由於並非你在反射世界裏所作的事情都會還原到真實世界裏。

第三定律引出了一個 settable (可設置性,或可寫性)的概念。

其實早在之前的文章中,咱們就一直在說,Go 語言裏的函數都是值傳遞,只要你傳遞的不是變量的指針,你在函數內部對變量的修改是不會影響到原始的變量的。

回到反射上來,當你使用 reflect.Typeof 和 reflect.Valueof 的時候,若是傳遞的不是接口變量的指針,反射世界裏的變量值始終將只是真實世界裏的一個拷貝,你對該反射對象進行修改,並不能反映到真實世界裏。

所以在反射的規則裏

  • 不是接收變量指針建立的反射對象,是不具有『可寫性』的
  • 是否具有『可寫性』,可以使用 CanSet() 來獲取得知
  • 對不具有『可寫性』的對象進行修改,是沒有意義的,也認爲是不合法的,所以會報錯。
package main

import (
    "fmt"
    "reflect"
)

func main() {
    var name string = "Go編程時光"

    v := reflect.ValueOf(name)
    fmt.Println("可寫性爲:", v.CanSet())
}複製代碼

輸出以下

可寫性爲: false複製代碼

要讓反射對象具有可寫性,須要注意兩點

  1. 建立反射對象時傳入變量的指針
  2. 使用 Elem()函數返回指針指向的數據

完整代碼以下

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var name string = "Go編程時光"
    v1 := reflect.ValueOf(&name)
    fmt.Println("v1 可寫性爲:", v1.CanSet())

    v2 := v1.Elem()
    fmt.Println("v2 可寫性爲:", v2.CanSet())
}複製代碼

輸出以下

v1 可寫性爲: false
v2 可寫性爲: true複製代碼

知道了如何使反射的世界裏的對象具備可寫性後,接下來是時候瞭解一下如何對修改更新它。

反射對象,都會有以下幾個以 Set 單詞開頭的方法

這些方法就是咱們修改值的入口。

來舉個例子

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var name string = "Go編程時光"
    fmt.Println("真實世界裏 name 的原始值爲:", name)

    v1 := reflect.ValueOf(&name)
    v2 := v1.Elem()

    v2.SetString("Python編程時光")
    fmt.Println("經過反射對象進行更新後,真實世界裏 name 變爲:", name)
}複製代碼

輸出以下

真實世界裏 name 的原始值爲: Go編程時光
經過反射對象進行更新後,真實世界裏 name 變爲: Python編程時光複製代碼

參考文章


相關文章
相關標籤/搜索