Golang通脈之反射

什么是反射

官方關于反射定義:

Reflection in computing is the ability of a program to examine its own structure, particularly through types; it’s a form of metaprogramming. It’s also a great source of confusion.
(在計算機領域,反射是一種讓程序——主要是通過類型——理解其自身結構的一種能力。它是元編程的組成之一,同時它也是一大引人困惑的難題。)

維基百科關于反射的定義:

在計算機科學中,反射是指計算機程序在運行時(Run time)可以訪問、檢測和修改它本身狀態(tài)或行為的一種能力。用比喻來說,反射就是程序在運行的時候能夠“觀察”并且修改自己的行為。

《Go語言圣經》關于反射的定義:

Go 語言提供了一種機制在運行時更新變量和檢查它們的值、調用它們的方法,但是在編譯時并不知道這些變量的具體類型,這稱為反射機制。

Go 語言是靜態(tài)編譯類語言,比如在定義一個變量的時候,已經知道了它是什么類型。但是有些事情只有在運行時才知道。比如定義了一個函數(shù),它有一個interface{}類型的參數(shù),這也就意味著調用者可以傳遞任何類型的參數(shù)給這個函數(shù)。在這種情況下,如果想知道調用者傳遞的是什么類型的參數(shù),就需要用到反射。如果想知道一個結構體有哪些字段和方法,也需要反射。

根據以上定義,可以得出:

反射是指在程序運行時對程序本身進行訪問和修改的能力。程序在編譯時,變量被轉換為內存地址,變量名不會被編譯器寫入到可執(zhí)行部分。在運行程序時,程序無法獲取自身的信息。

支持反射的語言可以在程序編譯期將變量的反射信息,如字段名稱、類型信息、結構體信息等整合到可執(zhí)行文件中,并給程序提供接口訪問反射信息,這樣就可以在程序運行期獲取類型的反射信息,并且有能力修改它們。

變量的內在機制

  1. Go語言中的變量是分為兩部分的:

    • 類型信息(type):預先定義好的元信息。
    • 值信息(value):程序運行過程中可動態(tài)變化的。

    理解這一點就知道為什么nil != nil

  2. type 包括 static typeconcrete type. 簡單來說 static type是在編碼時確定的類型(如int、string等),concrete typeruntime系統(tǒng)確定的類型。

  3. 類型斷言能否成功,取決于變量的concrete type,而不是static type。因此,一個 reader變量如果它的concrete type也實現(xiàn)了write方法的話,它也可以被類型斷言為writer

Go是靜態(tài)類型語言。每個變量都擁有一個靜態(tài)類型,這意味著每個變量的類型在編譯時都是確定的:int,float32, *AutoType, []byte, chan []int 諸如此類。

在反射的概念中, 編譯時就知道變量類型的是靜態(tài)類型;運行時才知道一個變量類型的叫做動態(tài)類型。

  • 靜態(tài)類型: 靜態(tài)類型就是變量聲明時的賦予的類型
type MyInt int // int 就是靜態(tài)類型

type A struct{
   Name string  // string就是靜態(tài)
}
var i *int  // *int就是靜態(tài)類型
  • 動態(tài)類型:運行時給這個變量賦值時,這個值的類型(如果值為nil的時候沒有動態(tài)類型)。一個變量的動態(tài)類型在運行時可能改變,這主要依賴于它的賦值(前提是這個變量是接口類型)。
var A interface{} // 靜態(tài)類型interface{}
A = 10            // 靜態(tài)類型為interface{}  動態(tài)為int
A = "String"      // 靜態(tài)類型為interface{}  動態(tài)為string
var M *int
A = M             // A的值可以改變

Go語言的反射就是建立在類型之上的,Golang的指定類型的變量的類型是靜態(tài)的,在創(chuàng)建變量的時候就已經確定,反射主要與Golang的interface類型相關,只有interface類型才有反射一說。

在Golang的實現(xiàn)中,每個interface變量都有一個對應pair,pair中記錄了實際變量的值和類型(在接口介紹時有描述):

(value, type)

value是實際變量值,type是實際變量的類型。一個interface{}類型的變量包含了2個指針,一個指針指向值的類型(對應concrete type),另外一個指針指向實際的值(對應value)。

例如,創(chuàng)建類型為*os.File的變量,然后將其賦給一個接口變量r

tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)

var r io.Reader
r = tty

接口變量rpair中將記錄如下信息:(tty, *os.File),這個pair在接口變量的連續(xù)賦值過程中是不變的,將接口變量r賦給另一個接口變量w:

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

接口變量wpairrpair相同,都是:(tty, *os.File),即使w是空接口類型,pair也是不變的。

interface及其pair的存在,是Golang中實現(xiàn)反射的前提,理解了pair,就更容易理解反射。反射就是用來檢測存儲在接口變量內部(值value;類型concrete type) pair對的一種機制。

所以要理解兩個基本概念 Type 和 Value,它們也是 Go語言包中 reflect 空間里最重要的兩個類型。

reflect

Go程序在運行時使用reflect包訪問程序的反射信息。

之前介紹過interface,空接口可以存儲任意類型的變量,那如何知道這個空接口保存的數(shù)據是什么呢? 反射就是在運行時動態(tài)的獲取一個變量的類型信息和值信息。

在Go語言的反射機制中,任何接口值都由是一個具體類型具體類型的值兩部分組成的。 在Go語言中反射的相關功能由內置的reflect包提供,任意接口值在反射中都可以理解為由reflect.Typereflect.Value兩部分組成,并且reflect包提供了reflect.TypeOfreflect.ValueOf兩個函數(shù)來獲取任意對象的ValueType。

// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i.  ValueOf(nil) returns the zero 
// 翻譯一下:ValueOf用來獲取輸入參數(shù)接口中的數(shù)據的值,如果接口為空則返回0
func ValueOf(i interface{}) Value {...}

// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
// 翻譯一下:TypeOf用來動態(tài)獲取輸入參數(shù)接口中的值的類型,如果接口為空則返回nil
func TypeOf(i interface{}) Type {...}

TypeOf

Type

reflect.Value 可以用于與值有關的操作中,而如果是和變量類型本身有關的操作,則最好使用 reflect.Type,比如要獲取結構體對應的字段名稱或方法。

和 reflect.Value 不同,reflect.Type 是一個接口,而不是一個結構體,所以也只能使用它的方法。

以下 reflect.Type 接口常用的方法。從列表來看,大部分都和 reflect.Value 的方法功能相同。

type Type interface {

   Implements(u Type) bool
   AssignableTo(u Type) bool
   ConvertibleTo(u Type) bool
   Comparable() bool

   //以下這些方法和Value結構體的功能相同
   Kind() Kind

   Method(int) Method
   MethodByName(string) (Method, bool)
   NumMethod() int
   Elem() Type
   Field(i int) StructField
   FieldByIndex(index []int) StructField
   FieldByName(name string) (StructField, bool)
   FieldByNameFunc(match func(string) bool) (StructField, bool)
   NumField() int
}

其中幾個特有的方法如下:

  1. Implements 方法用于判斷是否實現(xiàn)了接口 u;
  2. AssignableTo 方法用于判斷是否可以賦值給類型 u,其實就是是否可以使用 =,即賦值運算符;
  3. ConvertibleTo 方法用于判斷是否可以轉換成類型 u,其實就是是否可以進行類型轉換;
  4. Comparable 方法用于判斷該類型是否是可比較的,其實就是是否可以使用關系運算符進行比較。

要反射獲取一個變量的 reflect.Type,可以通過函數(shù) reflect.TypeOf(),程序通過類型對象可以訪問任意值的類型信息。

func main() {
    //反射操作:通過反射,可以獲取一個接口類型變量的 類型
    var x float64 =3.4
    fmt.Println("type:",reflect.TypeOf(x)) //type: float64
}

type nametype kind

在反射中關于類型還劃分為兩種:類型(Type)種類(Kind)。因為在Go語言中可以使用type關鍵字構造很多自定義類型,而種類(Kind)就是指底層的類型,但在反射中,當需要區(qū)分指針、結構體等大品種的類型時,就會用到種類(Kind)。 舉個例子,定義了兩個指針類型和兩個結構體類型,通過反射查看它們的類型和種類。

type myInt int64

func reflectType(x interface{}) {
    t := reflect.TypeOf(x)
    fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind())
}

func main() {
    var a *float32 // 指針
    var b myInt    // 自定義類型
    var c rune     // 類型別名
    reflectType(a) // type: kind:ptr
    reflectType(b) // type:myInt kind:int64
    reflectType(c) // type:int32 kind:int32

    type person struct {
        name string
        age  int
    }
    type book struct{ title string }
    var d = person{
        name: "張三",
        age:  25,
    }
    var e = book{title: "《Go語言圣經》"}
    reflectType(d) // type:person kind:struct
    reflectType(e) // type:book kind:struct
}

Go語言的反射中像數(shù)組、切片、Map、指針等類型的變量,它們的.Name()都是返回。

reflect包中定義的Kind類型如下:

// A Kind represents the specific kind of type that a Type represents.
// The zero Kind is not a valid kind.
type Kind uint
const (
    Invalid Kind = iota  // 非法類型
    Bool                 // 布爾型
    Int                  // 有符號整型
    Int8                 // 有符號8位整型
    Int16                // 有符號16位整型
    Int32                // 有符號32位整型
    Int64                // 有符號64位整型
    Uint                 // 無符號整型
    Uint8                // 無符號8位整型
    Uint16               // 無符號16位整型
    Uint32               // 無符號32位整型
    Uint64               // 無符號64位整型
    Uintptr              // 指針
    Float32              // 單精度浮點數(shù)
    Float64              // 雙精度浮點數(shù)
    Complex64            // 64位復數(shù)類型
    Complex128           // 128位復數(shù)類型
    Array                // 數(shù)組
    Chan                 // 通道
    Func                 // 函數(shù)
    Interface            // 接口
    Map                  // 映射
    Ptr                  // 指針
    Slice                // 切片
    String               // 字符串
    Struct               // 結構體
    UnsafePointer        // 底層指針
)

通過 reflect.Type 還可以判斷是否實現(xiàn)了某接口。以 person 結構體為例,判斷它是否實現(xiàn)了接口 fmt.Stringerio.Writer

func main() {
   p:=person{Name: "張三",Age: 20}
   pt:=reflect.TypeOf(p)
   stringerType:=reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
   writerType:=reflect.TypeOf((*io.Writer)(nil)).Elem()
   fmt.Println("是否實現(xiàn)了fmt.Stringer:",pt.Implements(stringerType))
   fmt.Println("是否實現(xiàn)了io.Writer:",pt.Implements(writerType))
}

盡可能通過類型斷言的方式判斷是否實現(xiàn)了某接口,而不是通過反射。

通過 Implements 方法來判斷是否實現(xiàn)了 fmt.Stringer 和 io.Writer 接口,運行結果:

是否實現(xiàn)了fmt.Stringer: false
是否實現(xiàn)了io.Writer: false

ValueOf

reflect.ValueOf()返回的是reflect.Value類型,其中包含了原始值的值信息。reflect.Value與原始值之間可以互相轉換。

reflect.Value 被定義為一個 struct 結構體,它的定義如下面所示:

// Value is the reflection interface to a Go value.
//
// Not all methods apply to all kinds of values. Restrictions,
// if any, are noted in the documentation for each method.
// Use the Kind method to find out the kind of value before
// calling kind-specific methods. Calling a method
// inappropriate to the kind of type causes a run time panic.
//
// The zero Value represents no value.
// Its IsValid method returns false, its Kind method returns Invalid,
// its String method returns "<invalid Value>", and all other methods panic.
// Most functions and methods never return an invalid value.
// If one does, its documentation states the conditions explicitly.
//
// A Value can be used concurrently by multiple goroutines provided that
// the underlying Go value can be used concurrently for the equivalent
// direct operations.
//
// To compare two Values, compare the results of the Interface method.
// Using == on two Values does not compare the underlying values
// they represent.
type Value struct {
    // typ holds the type of the value represented by a Value.
    typ *rtype

    // Pointer-valued data or, if flagIndir is set, pointer to data.
    // Valid when either flagIndir is set or typ.pointers() is true.
    ptr unsafe.Pointer

    // flag holds metadata about the value.
    // The lowest bits are flag bits:
    //  - flagStickyRO: obtained via unexported not embedded field, so read-only
    //  - flagEmbedRO: obtained via unexported embedded field, so read-only
    //  - flagIndir: val holds a pointer to the data
    //  - flagAddr: v.CanAddr is true (implies flagIndir)
    //  - flagMethod: v is a method value.
    // The next five bits give the Kind of the value.
    // This repeats typ.Kind() except for method values.
    // The remaining 23+ bits give a method number for method values.
    // If flag.kind() != Func, code can assume that flagMethod is unset.
    // If ifaceIndir(typ), code can assume that flagIndir is set.
    flag

    // A method value represents a curried method invocation
    // like r.Read for some receiver r. The typ+val+flag bits describe
    // the receiver r, but the flag's Kind bits say Func (methods are
    // functions), and the top bits of the flag give the method number
    // in r's type's method table.
}

reflect.Value 結構體的字段都是私有的,也就是說,只能使用 reflect.Value 的方法。它有如下常用方法,

方法 說明
Interface() interface {} 將值以 interface{} 類型返回,可以通過類型斷言轉換為指定類型
Int() int64 將值以 int 類型返回,所有有符號整型均可以此方式返回
Uint() uint64 將值以 uint 類型返回,所有無符號整型均可以此方式返回
Float() float64 將值以雙精度(float64)類型返回,所有浮點數(shù)(float32、float64)均可以此方式返回
Bool() bool 將值以 bool 類型返回
Bytes() []bytes 將值以字節(jié)數(shù)組 []bytes 類型返回
String() string 將值以字符串類型返回
CanSet() bool 是否可以修改對應的值
Elem() Type 獲取指針指向的值,一般用于修改對應的值
Kind() Kind 獲取對應的類型類別,比如Array、Slice、Map等

通過反射獲取值

func reflectValue(x interface{}) {
    v := reflect.ValueOf(x)
    k := v.Kind()
    switch k {
    case reflect.Int64:
        // v.Int()從反射中獲取整型的原始值,然后通過int64()強制類型轉換
        fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
    case reflect.Float32:
        // v.Float()從反射中獲取浮點型的原始值,然后通過float32()強制類型轉換
        fmt.Printf("type is float32, value is %f\n", float32(v.Float()))
    case reflect.Float64:
        // v.Float()從反射中獲取浮點型的原始值,然后通過float64()強制類型轉換
        fmt.Printf("type is float64, value is %f\n", float64(v.Float()))
    }
}
func main() {
    var a float32 = 3.14
    var b int64 = 100
    reflectValue(a) // type is float32, value is 3.140000
    reflectValue(b) // type is int64, value is 100
    // 將int類型的原始值轉換為reflect.Value類型
    c := reflect.ValueOf(10)
    fmt.Printf("type c :%T\n", c) // type c :reflect.Value
}

通過反射設置變量的值

想要在函數(shù)中通過反射修改變量的值,需要注意函數(shù)參數(shù)傳遞的是值拷貝,必須傳遞變量地址才能修改變量值。而反射中使用專有的Elem()方法來獲取指針對應的值。

func main() {
    var a int64 = 10
    v := reflect.ValueOf(a)
    if v.Kind() == reflect.Int64 {
        v.SetInt(20) //panic: reflect: reflect.Value.SetInt using unaddressable value
    }
}
func main() {
    var a int64 = 10
    v := reflect.ValueOf(&a)    //反射獲取指針的地址
    // 反射中使用 Elem()方法獲取指針對應的值
    if v.Elem().Kind() == reflect.Int64 {
        v.Elem().SetInt(20)
    }
    fmt.Println(a)  //20
}

isNil()isValid()

isNil()

func (v Value) IsNil() bool

IsNil()報告v持有的值是否為nil。v持有的值的分類必須是通道、函數(shù)、接口、映射、指針、切片之一;否則IsNil函數(shù)會導致panic。

isValid()

func (v Value) IsValid() bool

IsValid()返回v是否持有一個值。如果v是Value零值會返回假,此時v除了IsValid、String、Kind之外的方法都會導致panic。

IsNil()常被用于判斷指針是否為空;IsValid()常被用于判定返回值是否有效。

func main() {
    // *int類型空指針
    var a *int
    fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil()) //var a *int IsNil: true
    // nil值
    fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid()) //nil IsValid: false
    // 實例化一個匿名結構體
    b := struct{}{}
    // 嘗試從結構體中查找"abc"字段
    fmt.Println("b結構體是否存在成員abc:", reflect.ValueOf(b).FieldByName("abc").IsValid()) //b結構體是否存在成員abc: false
    // 嘗試從結構體中查找"abc"方法
    fmt.Println("b結構體是否存在方法abc:", reflect.ValueOf(b).MethodByName("abc").IsValid()) //b結構體是否存在方法abc: false
    // map
    c := map[string]int{}
    // 嘗試從map中查找一個不存在的鍵
    fmt.Println("map中是否存在鍵張三:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("張三")).IsValid()) //map中是否存在鍵張三: false
}

結構體反射

與結構體相關的方法

任意值通過reflect.TypeOf()獲得反射對象信息后,如果它的類型是結構體,可以通過反射值對象(reflect.Type)的NumField()Field()方法獲得結構體成員的詳細信息。

reflect.Type中與獲取結構體成員相關的的方法如下表所示。

方法 說明
Field(i int) StructField 根據索引,返回索引對應的結構體字段的信息。
NumField() int 返回結構體成員字段數(shù)量。
FieldByName(name string) (StructField, bool) 根據給定字符串返回字符串對應的結構體字段的信息。
FieldByIndex(index []int) StructField 多層成員訪問時,根據 []int 提供的每個結構體的字段索引,返回字段的信息。
FieldByNameFunc(match func(string) bool) (StructField,bool) 根據傳入的匹配函數(shù)匹配需要的字段。
NumMethod() int 返回該類型的方法集中方法的數(shù)目
Method(int) Method 返回該類型方法集中的第i個方法
MethodByName(string)(Method, bool) 根據方法名返回該類型方法集中的方法

StructField類型

StructField類型用來描述結構體中的一個字段的信息。

StructField的定義如下:

type StructField struct {
    // 參見http://golang.org/ref/spec#Uniqueness_of_identifiers
    Name    string      // Name是字段的名字。
    PkgPath string      // PkgPath是非導出字段的包路徑,對導出字段該字段為""。
    Type      Type      // 字段的類型
    Tag       StructTag // 字段的標簽
    Offset    uintptr   // 字段在結構體中的字節(jié)偏移量
    Index     []int     // 用于Type.FieldByIndex時的索引切片
    Anonymous bool      // 是否匿名字段
}

結構體反射示例

當我們使用反射得到一個結構體數(shù)據之后可以通過索引依次獲取其字段信息,也可以通過字段名去獲取指定的字段信息。

type student struct {
    Name  string `json:"name"`
    Score int    `json:"score"`
}

func main() {
    stu := student{
        Name:  "張三",
        Score: 90,
    }

    t := reflect.TypeOf(stu)
    fmt.Println(t.Name(), t.Kind()) // student struct
    // 通過for循環(huán)遍歷結構體的所有字段信息
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
    }

    // 通過字段名獲取指定結構體字段信息
    if scoreField, ok := t.FieldByName("Score"); ok {
        fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
    }
}

運行結果:

student struct
name:Name index:[0] type:string json tag:name
name:Score index:[1] type:int json tag:score
name:Score index:[1] type:int json tag:score

接下來編寫一個函數(shù)printMethod(s interface{})來遍歷打印s包含的方法。

func (s student) Study() string {
    msg := "學習"
    fmt.Println(msg)
    return msg
}

func (s student) Sleep() string {
    msg := "睡覺"
    fmt.Println(msg)
    return msg
}

func printMethod(x interface{}) {
    t := reflect.TypeOf(x)
    v := reflect.ValueOf(x)

    fmt.Println(t.NumMethod())
    for i := 0; i < v.NumMethod(); i++ {
        methodType := v.Method(i).Type()
        fmt.Printf("method name:%s\n", t.Method(i).Name)
        fmt.Printf("method:%s\n", methodType)
        // 通過反射調用方法傳遞的參數(shù)必須是 []reflect.Value 類型
        args := []reflect.Value{}
        v.Method(i).Call(args)
    }
}

運行結果:

2
method name:Sleep
method:func() string
睡覺
method name:Study
method:func() string
學習

反射的規(guī)則

根據上面對反射的大致介紹,對反射有了一定的了解,其實反射的操作步驟非常的簡單,就是通過實例對象獲取反射對象(Value、Type),然后操作相應的方法

實例、Value、Type 三者之間的轉換關系:

image
  1. 從實例到Value

通過實例獲取 Value 對象,直接使用 reflect.ValueOf() 函數(shù):

func ValueOf(i interface {}) Value
  1. 從實例到Type

通過實例獲取反射對象的 Type,直接使用 reflect.TypeOf() 函數(shù):

func TypeOf(i interface{}) Type
  1. TypeValue

Type 里面只有類型信息,所以直接從一個 Type 接口變量里面是無法獲得實例的 Value 的,但可以通過該 Type 構建一個新實例的 Value。reflect 包提供了兩種方法,示例如下:

//New 返回的是一個 Value,該 Value 的 type 為 PtrTo(typ),即 Value 的 Type 是指定 typ 的指針類型
func New(typ Type) Value
//Zero 返回的是一個 typ 類型的零佳,注意返回的 Value 不能尋址,位不可改變
func Zero(typ Type) Value

如果知道一個類型值的底層存放地址,則還有一個函數(shù)是可以依據 type 和該地址值恢復出 Value 的:

func NewAt(typ Type, p unsafe.Pointer) Value
  1. ValueType

從反射對象 Value 到 Type 可以直接調用 Value 的方法,因為 Value 內部存放著到 Type 類型的指針:

func (v Value) Type() Type
  1. Value到實例

Value 本身就包含類型和值信息,reflect 提供了豐富的方法來實現(xiàn)從 Value 到實例的轉換:

//該方法最通用,用來將 Value 轉換為空接口,該空接口內部存放具體類型實例
//可以使用接口類型查詢去還原為具體的類型
func (v Value) Interface() (i interface{})

//Value 自身也提供豐富的方法,直接將 Value 轉換為簡單類型實例,如果類型不匹配,則直接引起 panic
func (v Value) Bool () bool
func (v Value) Float() float64
func (v Value) Int() int64
func (v Value) Uint() uint64
......
  1. Value的指針到值

從一個指針類型的 Value 獲得值類型 Value 有兩種方法:

//如果 v 類型是接口,則 Elem() 返回接口綁定的實例的 Value,如采 v 類型是指針,則返回指針值的 Value,否則引起 panic
func (v Value) Elem() Value
//如果 v 是指針,則返回指針值的 Value,否則返回 v 自身,該函數(shù)不會引起 panic
func Indirect(v Value) Value
  1. Type 指針和值的相互轉換

指針類型 Type 到值類型 Type:

//t 必須是 Array、Chan、Map、Ptr、Slice,否則會引起 panic
//Elem 返回的是其內部元素的 Type
func (t *rtype) Elem() Type

值類型 Type 到指針類型 Type:

//PtrTo 返回的是指向 t 的指針型 Type
func PtrTo(t Type) Type
  1. Value 值的可修改性

Value 值的修改涉及如下兩個方法:

//通過 CanSet 判斷是否能修改
func (v Value ) CanSet() bool
//通過 Set 進行修改
func (v Value ) Set(x Value)

實例對象傳遞給接口的是一個完全的值拷貝,如果調用反射的方法 reflect.ValueOf() 傳進去的是一個值類型變量, 則獲得的 Value 實際上是原對象的一個副本,這個 Value 是無論如何也不能被修改的。

反射是計算機語言中程序檢視其自身結構的一種方法,它屬于元編程的一種形式。反射靈活、強大,但也存在不安全。它可以繞過編譯器的很多靜態(tài)檢查,如果過多使用便會造成混亂。為了幫助開發(fā)者更好地理解反射,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. 任何接口值 interface{} 都可以反射出反射對象,也就是 reflect.Value 和 reflect.Type,通過函數(shù) reflect.ValueOf 和 reflect.TypeOf 獲得。
  2. 反射對象也可以還原為 interface{} 變量,也就是第 1 條定律的可逆性,通過 reflect.Value 結構體的 Interface 方法獲得。
  3. 要修改反射的對象,該值必須可設置,也就是可尋址。

任何類型的變量都可以轉換為空接口 intferface{},所以第 1 條定律中函數(shù) reflect.ValueOf 和 reflect.TypeOf 的參數(shù)就是 interface{},表示可以把任何類型的變量轉換為反射對象。在第 2 條定律中,reflect.Value 結構體的 Interface 方法返回的值也是 interface{},表示可以把反射對象還原為對應的類型變量。

反射的使用

從relfect.Value中獲取接口interface的信息

當執(zhí)行reflect.ValueOf(interface)之后,就得到了一個類型為”relfect.Value”變量,可以通過它本身的Interface()方法獲得接口變量的真實內容,然后可以通過類型判斷進行轉換,轉換為原有真實類型。不過,可能是已知原有類型,也有可能是未知原有類型:

已知原有類型

已知類型后轉換為其對應的類型的做法如下,直接通過Interface方法然后強制轉換,如下:

realValue := value.Interface().(已知的類型)

func main() {
    var num float64 = 1.2345

    pointer := reflect.ValueOf(&num)
    value := reflect.ValueOf(num)

    // 可以理解為“強制轉換”,但是需要注意的時候,轉換的時候,如果轉換的類型不完全符合,則直接panic
    // Golang 對類型要求非常嚴格,類型一定要完全符合
    // 如下兩個,一個是*float64,一個是float64,如果弄混,則會panic
    convertPointer := pointer.Interface().(*float64)
    convertValue := value.Interface().(float64)

    fmt.Println(convertPointer)
    fmt.Println(convertValue)
}

運行結果:

0xc000098000
1.2345

說明

  1. 轉換的時候,如果轉換的類型不完全符合,則直接panic,類型要求非常嚴格!
  2. 轉換的時候,要區(qū)分是指針還是值
  3. 也就是說反射可以將“反射類型對象”再重新轉換為“接口類型變量”

未知原有類型

很多情況下,可能并不知道其具體類型,那么就需要進行遍歷探測其Filed來得知:

type Person struct {
    Name string
    Age int
    Sex string
}

func (p Person) Say(msg string)  {
    fmt.Println("hello,",msg)
}
func (p Person) PrintInfo()  {
    fmt.Printf("姓名:%s,年齡:%d,性別:%s\n",p.Name,p.Age,p.Sex)
}



func main() {
    p1 := Person{"張三",25,"男"}
    DoFiledAndMethod(p1)
}

// 通過接口來獲取任意參數(shù)
func DoFiledAndMethod(input interface{}) {

    getType := reflect.TypeOf(input) //先獲取input的類型
    fmt.Println("get Type is :", getType.Name()) // Person
    fmt.Println("get Kind is : ", getType.Kind()) // struct

    getValue := reflect.ValueOf(input)
    fmt.Println("get all Fields is:", getValue) //{張三 25 男}

    // 獲取方法字段
    // 1. 先獲取interface的reflect.Type,然后通過NumField進行遍歷
    // 2. 再通過reflect.Type的Field獲取其Field
    // 3. 最后通過Field的Interface()得到對應的value
    for i := 0; i < getType.NumField(); i++ {
        field := getType.Field(i)
        value := getValue.Field(i).Interface() //獲取第i個值
        fmt.Printf("字段名稱:%s, 字段類型:%s, 字段數(shù)值:%v \n", field.Name, field.Type, value)
    }

    // 通過反射,操作方法
    // 1. 先獲取interface的reflect.Type,然后通過.NumMethod進行遍歷
    // 2. 再公國reflect.Type的Method獲取其Method
    for i := 0; i < getType.NumMethod(); i++ {
        method := getType.Method(i)
        fmt.Printf("方法名稱:%s, 方法類型:%v \n", method.Name, method.Type)
    }
}

運行結果:

get Type is : Person
get Kind is :  struct
get all Fields is: {張三 25 男}
字段名稱:Name, 字段類型:string, 字段數(shù)值:張三 
字段名稱:Age, 字段類型:int, 字段數(shù)值:25 
字段名稱:Sex, 字段類型:string, 字段數(shù)值:男 
方法名稱:PrintInfo, 方法類型:func(main.Person) 
方法名稱:Say, 方法類型:func(main.Person, string) 

說明

通過運行結果可以得知獲取未知類型的interface的具體變量及其類型的步驟為:

  1. 先獲取interface的reflect.Type,然后通過NumField進行遍歷
  2. 再通過reflect.Type的Field獲取其Field
  3. 最后通過Field的Interface()得到對應的value

通過運行結果可以得知獲取未知類型的interface的所屬方法(函數(shù))的步驟為:

  1. 先獲取interface的reflect.Type,然后通過NumMethod進行遍歷
  2. 再分別通過reflect.Type的Method獲取對應的真實的方法(函數(shù))
  3. 最后對結果取其Name和Type得知具體的方法名
  4. 也就是說反射可以將“反射類型對象”再重新轉換為“接口類型變量”
  5. struct 或者 struct 的嵌套都是一樣的判斷處理方式

如果是struct的話,可以使用Elem()

tag := t.Elem().Field(0).Tag //獲取定義在struct里面的Tag屬性
name := v.Elem().Field(0).String() //獲取存儲在第一個字段里面的值

通過reflect.Value設置實際變量的值

reflect.Value是通過reflect.ValueOf(X)獲得的,只有當X是指針的時候,才可以通過reflec.Value修改實際變量X的值,即:要修改反射類型的對象就一定要保證其值是可尋址的。

這里需要一個方法:

// Elem returns the value that the interface v contains
// or that the pointer v points to.
// It panics if v's Kind is not Interface or Ptr.
// It returns the zero Value if v is nil.
func (v Value) Elem() Value

解釋起來就是:Elem返回接口v包含的值或指針v指向的值。如果v的類型不是interface或ptr,它會恐慌。如果v為零,則返回零值。

func main() {

    var num float64 = 1.2345
    fmt.Println("old value of pointer:", num)

    // 通過reflect.ValueOf獲取num中的reflect.Value,注意,參數(shù)必須是指針才能修改其值
    pointer := reflect.ValueOf(&num)
    newValue := pointer.Elem()

    fmt.Println("type of pointer:", newValue.Type())
    fmt.Println("settability of pointer:", newValue.CanSet())

    // 重新賦值
    newValue.SetFloat(77)
    fmt.Println("new value of pointer:", num)

    ////////////////////
    // 如果reflect.ValueOf的參數(shù)不是指針,會如何?
    //pointer = reflect.ValueOf(num)
    //newValue = pointer.Elem() // 如果非指針,這里直接panic,“panic: reflect: call of reflect.Value.Elem on float64 Value”
}

運行結果:

old value of pointer: 1.2345
type of pointer: float64
settability of pointer: true
new value of pointer: 77

說明

  1. 需要傳入的參數(shù)是* float64這個指針,然后可以通過pointer.Elem()去獲取所指向的Value,注意一定要是指針。
  2. 如果傳入的參數(shù)不是指針,而是變量,那么
    • 通過Elem獲取原始值對應的對象則直接panic
    • 通過CanSet方法查詢是否可以設置返回false
  3. newValue.CantSet()表示是否可以重新設置其值,如果輸出的是true則可修改,否則不能修改,修改完之后再進行打印發(fā)現(xiàn)真的已經修改了。
  4. reflect.Value.Elem() 表示獲取原始值對應的反射對象,只有原始對象才能修改,當前反射對象是不能修改的
  5. 也就是說如果要修改反射類型對象,其值必須是可尋址的【對應的要傳入的是指針,同時要通過Elem方法獲取原始值對應的反射對象】
  6. struct 或者 struct 的嵌套都是一樣的判斷處理方式

通過reflect.Value來進行方法的調用

在項目應用中,另外一個常用并且屬于高級的用法,就是通過reflect來進行方法的調用。比如要做框架工程的時候,需要可以隨意擴展方法,或者說用戶可以自定義方法,關鍵點在于用戶的自定義方法是未可知的,因此可以通過reflect來搞定。

Call()方法:

// Call calls the function v with the input arguments in.
// For example, if len(in) == 3, v.Call(in) represents the Go call v(in[0], in[1], in[2]).
// Call panics if v's Kind is not Func.
// It returns the output results as Values.
// As in Go, each input argument must be assignable to the
// type of the function's corresponding input parameter.
// If v is a variadic function, Call creates the variadic slice parameter
// itself, copying in the corresponding values.
func (v Value) Call(in []Value) []Value

通過反射,調用方法。

type Person struct {
    Name string
    Age int
    Sex string
}

func (p Person) Say(msg string)  {
    fmt.Println("hello,",msg)
}
func (p Person) PrintInfo()  {
    fmt.Printf("姓名:%s,年齡:%d,性別:%s\n",p.Name,p.Age,p.Sex)
}

func (p Person) Test(i,j int,s string){
    fmt.Println(i,j,s)
}


// 如何通過反射來進行方法的調用?
// 本來可以用結構體對象.方法名稱()直接調用的,
// 但是如果要通過反射,
// 那么首先要將方法注冊,也就是MethodByName,然后通過反射調動mv.Call

func main() {
    p2 := Person{"張三",25,"男"}
    // 1. 要通過反射來調用起對應的方法,必須要先通過reflect.ValueOf(interface)來獲取到reflect.Value,
    // 得到“反射類型對象”后才能做下一步處理
    getValue := reflect.ValueOf(p2)

    // 2.一定要指定參數(shù)為正確的方法名
    // 先看看沒有參數(shù)的調用方法

    methodValue1 := getValue.MethodByName("PrintInfo")
    fmt.Printf("Kind : %s, Type : %s\n",methodValue1.Kind(),methodValue1.Type())
    methodValue1.Call(nil) //沒有參數(shù),直接寫nil

    args1 := make([]reflect.Value, 0) //或者創(chuàng)建一個空的切片也可以
    methodValue1.Call(args1)

    // 有參數(shù)的方法調用
    methodValue2 := getValue.MethodByName("Say")
    fmt.Printf("Kind : %s, Type : %s\n",methodValue2.Kind(),methodValue2.Type())
    args2 := []reflect.Value{reflect.ValueOf("反射機制")}
    methodValue2.Call(args2)

    methodValue3 := getValue.MethodByName("Test")
    fmt.Printf("Kind : %s, Type : %s\n",methodValue3.Kind(),methodValue3.Type())
    args3 := []reflect.Value{reflect.ValueOf(100), reflect.ValueOf(200),reflect.ValueOf("Hello")}

    methodValue3.Call(args3)
}

運行結果:

Kind : func, Type : func()
姓名:張三,年齡:25,性別:男
姓名:張三,年齡:25,性別:男
Kind : func, Type : func(string)
hello, 反射機制
Kind : func, Type : func(int, int, string)
100 200 Hello

通過反射,調用函數(shù)。

函數(shù)像普通的變量一樣,是可以把函數(shù)作為一種變量類型的,而且是引用類型。如果說Fun()是一個函數(shù),那么f1 := Fun也是可以的,那么f1也是一個函數(shù),如果直接調用f1(),那么運行的就是Fun()函數(shù)。

那么就先通過ValueOf()來獲取函數(shù)的反射對象,可以判斷它的Kind,是一個func,那么就可以執(zhí)行Call()進行函數(shù)的調用。

func main() {
    //函數(shù)的反射
    f1 := fun1
    value := reflect.ValueOf(f1)
    fmt.Printf("Kind : %s , Type : %s\n",value.Kind(),value.Type()) //Kind : func , Type : func()

    value2 := reflect.ValueOf(fun2)
    fmt.Printf("Kind : %s , Type : %s\n",value2.Kind(),value2.Type()) //Kind : func , Type : func(int, string)


    //通過反射調用函數(shù)
    value.Call(nil)

    value2.Call([]reflect.Value{reflect.ValueOf(100),reflect.ValueOf("hello")})

}

func fun1(){
    fmt.Println("函數(shù)fun1(),無參。。")
}

func fun2(i int, s string){
    fmt.Println("函數(shù)fun2(),有參數(shù)。。",i,s)
}

說明

  1. 要通過反射來調用起對應的方法,必須要先通過reflect.ValueOf(interface)來獲取到reflect.Value,得到“反射類型對象”后才能做下一步處理
  2. reflect.Value.MethodByName這個MethodByName,需要指定準確真實的方法名字,如果錯誤將直接panic,MethodByName返回一個函數(shù)值對應的reflect.Value方法的名字。
  3. []reflect.Value,這個是最終需要調用的方法的參數(shù),可以沒有或者一個或者多個,根據實際參數(shù)來定。
  4. reflect.Value的 Call 這個方法,這個方法將最終調用真實的方法,參數(shù)務必保持一致,如果reflect.Value.Kind不是一個方法,那么將直接panic。
  5. 本來可以用對象訪問方法直接調用的,但是如果要通過反射,那么首先要將方法注冊,也就是MethodByName,然后通過反射調用methodValue.Call

反射是把雙刃劍

反射是一個強大并富有表現(xiàn)力的工具,能寫出更靈活的代碼。但是反射不應該被濫用,原因有以下三個。

  1. 基于反射的代碼是極其脆弱的,反射中的類型錯誤會在真正運行的時候才會引發(fā)panic,那很可能是在代碼寫完的很長時間之后。
  2. 大量使用反射的代碼通常難以理解,代碼可讀性差。
  3. 反射的性能低下,基于反射實現(xiàn)的代碼通常比正常代碼運行速度慢一到兩個數(shù)量級。處于運行效率關鍵位置的代碼,請避免使用反射。
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 編程語言中反射的概念 在計算機科學領域,反射是指一類應用,它們能夠自描述和自控制。也就是說,這類應用通過采用某種機...
    豆瓣奶茶閱讀 12,876評論 0 31
  • 序言 第一次接觸反射技術是在很多年前學習設計模式的時候,那時在優(yōu)化Java版簡單工廠的實現(xiàn),當讀取配置信息中的的類...
    _張曉龍_閱讀 4,767評論 2 21
  • 本文轉載自https://github.com/KeKe-Li/For-learning-Go-Tutorial/...
    雪域迷影閱讀 446評論 0 0
  • 在Go語言中,反射就是用來檢查儲存在接口變量內部pair對的一種機制,pair對是以值(value)和實際類型(c...
    env107閱讀 4,055評論 0 2
  • 一、概述 在golang中,reflect是一個比較高級的話題,本文將盡可能簡單而又清楚的介紹相關內容。 本文將從...
    神奇的考拉閱讀 26,356評論 2 14

友情鏈接更多精彩內容