深刻理解Golang之interface和reflect

前言

interface(即接口),是Go語言中一個重要的概念和知識點,而功能強大的reflect正是基於interface。本文便是對Go語言中的interfacereflect相關知識較爲全面的梳理,也算是我階段學習的總結,以期溫故而知新。文章較長,請讀者作好心理準備。html

interface(接口)

定義

在Go語言中,若是自定義類型(好比struct)實現了某個interface中的全部方法,那麼就能夠說這個類型實現了這個接口。接口可以下定義:golang

type 接口名稱 interface {
    method1(參數列表) 返回值列表
    method1(參數列表) 返回值列表
    ...
}
複製代碼

interface是一組方法的集合,但並不須要實現這些方法,而且interface沒有變量interface中的方法集合能夠表示一個對象的特徵和能力,當自定義類型須要使用這些方法時,能夠根據須要把這些方法實現出來。舉個栗子:web

package main

import (
    "fmt"
)

type Animal interface {
    Eat()
    Run()
}

type Dog struct {
    Name string
}

type Cat struct {
    Name string
}

func (dog *Dog) Eat() {
    fmt.Printf("%s is eating.", dog.Name)
}

func (dog *Dog) Run() {
    fmt.Printf("%s is running.", dog.Name)
}

func (cat *Cat) Eat() {
    fmt.Printf("%s is eating.", cat.Name)
}

func (cat *Cat) Run() {
    fmt.Printf("%s is running.", cat.Name)
}

func main() {
    var animal1 Animal
    animal1 = &Dog{"doggy"}
    animal1.Eat()
    animal1.Run()

    var animal2 Animal
    animal2 = &Cat{"catty"}
    animal2.Eat()
    animal2.Run()
}
複製代碼

上面即定義了一個Animal接口,以及Dog類型和Cat類型。Dog類型和Cat類型都實現了Animal接口中的方法,因此Dog和Cat都是Animal類型。
同時接口自己不能建立實例,但從上例能夠看出,接口類型的變量能夠指向一個實現了該接口的自定義類型的實例。interface類型默認是一個指針(引用類型),若是沒有對interface初始化就使用,那麼會輸出nilsql

空接口

空接口interface{}沒有任何方法,因此全部類型都實現了空接口, 即咱們能夠把任何一個變量賦值給空接口。修改一下上面的main函數:數據庫

func main() {
    var animal interface{}
    dog := &Dog{"doggy"}
    animal = dog
    fmt.Println(animal)
}
複製代碼

運行結果:數組

&{doggy}
複製代碼

接口繼承

一個接口能夠繼承多個其餘接口,若是要實現這個接口,那麼必須將所繼承的全部接口中的方法都實現。bash

package main

import (
    "fmt"
)

type Eater interface {
    Eat()
}

type Runner interface {
    Run()
}

type Animal interface {
    Eater
    Runner
}

// 這裏定義一個Dog的struct,並實現eat方法和run方法,這樣就實現了動物的接口
type Dog struct {
    Name string
}

func (dog *Dog) Eat() {
    fmt.Printf("%s is eating.", dog.Name)
}

func (dog *Dog) Run() {
    fmt.Printf("%s is running.", dog.Name)
}

func main() {
    var animal1 Animal
    animal1 = &Dog{"doggy"}
    animal1.Eat()
    animal1.Run()
}
複製代碼

類型斷言

當咱們不肯定某個接口變量裏存儲的是什麼類型的變量時,咱們能夠利用類型斷言來判斷變量類型。app

var animal1 Animal
animal1 = &Dog{"doggy"}
dog := animal1.(*Dog)
複製代碼

在進行類型斷言時,若是類型不匹配,就會報panic, 所以須要加上檢測機制,若是成功就 ok,不然也不要報 panic函數

var animal1 Animal
animal1 = &Dog{"doggy"}

if dog, ok := animal1.(*Dog); ok {
    fmt.Println("convert success")
    dog.Run()
else {
    fmt.Println("convert fail")
}
複製代碼

另外咱們也可使用switch-type語法進行類型斷言:工具

package main

import (
    "fmt"
)

type Eater interface {
    Eat()
}

type Runner interface {
    Run()
}

type Animal interface {
    Eater
    Runner
}

type Dog struct {
    Name string
}

type Cat struct {
    Name string
}

func (dog *Dog) Eat() {
    fmt.Printf("%s is eating.", dog.Name)
}

func (dog *Dog) Run() {
    fmt.Printf("%s is running.", dog.Name)
}

func (cat *Cat) Eat() {
    fmt.Printf("%s is eating.", cat.Name)
}

func (cat *Cat) Run() {
    fmt.Printf("%s is running.", cat.Name)
}

func TypeJudge(animals ...interface{}) {
    for index, animal := range animals {
        switch animal.(type) {
        case *Dog:
            fmt.Printf("第%d個參數是Dog類型\n", index)
        case *Cat:
            fmt.Printf("第%d個參數是Cat類型\n", index)
        default:
            fmt.Println("不肯定類型")
        }
    }
}

func main() {
    var animal1 Animal
    animal1 = &Dog{"doggy"}

    var animal2 Animal
    animal2 = &Cat{"catty"}

    TypeJudge(animal1, animal2)
}
複製代碼

做用

interface對於Go語言的意義在於其實現了泛型,好比在一個函數中須要能接收不一樣類型的參數或者返回不一樣類型的值,而不是一開始就指定參數或者返回值的類型,這樣就可讓函數支持全部類型:

func FuncName(arg1 interface{}, rest ...interface{}) interface{} {
    // ...
}
複製代碼

面嚮對象語言好比C++、Java都有多態的特性,能夠說interface是Go語言中實現多態的一種形式。同一個interface,可讓不一樣的類(自定義類型)實現,從而能夠調用同一個函數名的函數但實現徹底不一樣的功能。

有時咱們可以利用interface實現很是巧妙的功能:一般咱們定義一個切片(slice)都會指定一個具體的類型,可是咱們有時須要切片中的元素能夠任何類型的變量,這個時候interface就派上用場了。下面是在go代碼中update數據庫表中數據時,利用interface實現的騷操做,讀者能夠體會一下interface帶來的便利:

func generateSQLForUpdatingArticle(article model.ArticleStruct) (string, []interface{}) {
    var columns = make([]string0)
    var arguments = make([]interface{}, 0)

    if len(article.CommentCount) > 0 {
        columns = append(columns, "comment_count = ?")
        arguments = append(arguments, article.CommentCount)
    }

    if len(article.Source) > 0 {
        columns = append(columns, "source = ?")
        arguments = append(arguments, article.Source)
    }

    if len(article.Summary) > 0 {
        columns = append(columns, "summary = ?")
        arguments = append(arguments, article.Summary)
    }

    if len(article.Content) > 0 {
        columns = append(columns, "content = ?")
        arguments = append(arguments, article.Content)
    }

    sql := fmt.Sprintf("UPDATE article_structs SET %s WHERE sid = %s", strings.Join(columns, ","), article.Sid)
    return sql, arguments
}

func UpdateArticle(article model.ArticleStruct) error {
    sql, arguments := generateSQLForUpdatingArticle(article)
    if err := db.Exec(sql, arguments...).Error; err != nil {
        log.Println("Updating article failed with error:", err)
        return err
    }
    return nil
}
複製代碼

然而,空接口interface{} 雖然能保存任意的值,但也帶來了一個問題:一個空的接口會隱藏值對應的表示方式和全部的公開的方法,所以只有咱們知道具體的動態類型才能使用類型斷言來訪問內部的值, 對於內部值並無特別可作的事情;若是咱們事先不知道空接口指向的值的具體類型,咱們可能就一籌莫展了。

這個時候咱們想要知道一個接口類型的變量具體是什麼(什麼類型),有什麼能力(有哪些方法),就須要一面「鏡子」可以反射(reflect)出這個變量的具體內容。在Go語言中也正好有這樣的工具——reflect

reflect(反射)

概念

在計算機科學領域,反射是指一類應用,它們可以自描述和自控制。也就是說,這類應用經過採用某種機制來實現對本身行爲的描述(self-representation)和監測(examination),並能根據自身行爲的狀態和結果,調整或修改應用所描述行爲的狀態和相關的語義。

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

在講反射以前,咱們須要瞭解一下Golang關於類型設計的一些原則:

變量包含兩部分:type(類型)和value(值)。

type 分爲 static typeconcrete type。其中static type是咱們在編碼階段用到的數據類型,如int、string、bool等等;而concrete type則是runtime系統看見的類型。

接口類型的變量在類型斷言時可否成功,取決於concrete type 而不是 static type

在Go語言中指定類型的變量的類型都是靜態的,即static type,其在建立變量的時候就已經肯定;而反射主要是配合interface類型變量來使用的,這些變量的類型都是concrete type

在Go的實現中,每一個interface類型的變量都有一個對應的pair, pair中記錄了實際變量的valuetype

(value, type)
複製代碼

interface類型變量包含了兩個指針,分別指向實際變量的值(value)和類型(對應concrete type)。interface及其pair的存在,是Golang實現反射的前提,而反射也正是用來檢測接口類型變量內部存儲的值和類型的一種機制。說到這裏,天然也就要引出reflect包中的兩個數據類TypeValue

reflect.Type和reflect.Value

reflect.Type

reflect包中Type接口定義以下:

type Type interface {
    // Kind返回該接口的具體分類
    Kind() Kind
    // Name返回該類型在自身包內的類型名,若是是未命名類型會返回""
    Name() string
    // PkgPath返回類型的包路徑,即明確指定包的import路徑,如"encoding/base64"
    // 若是類型爲內建類型(string, error)或未命名類型(*T, struct{}, []int),會返回""
    PkgPath() string
    // 返回類型的字符串表示。該字符串可能會使用短包名(如用base64代替"encoding/base64")
    // 也不保證每一個類型的字符串表示不一樣。若是要比較兩個類型是否相等,請直接用Type類型比較。
    String() string
    // 返回要保存一個該類型的值須要多少字節;相似unsafe.Sizeof
    Size() uintptr
    // 返回當從內存中申請一個該類型值時,會對齊的字節數
    Align() int
    // 返回當該類型做爲結構體的字段時,會對齊的字節數
    FieldAlign() int
    // 若是該類型實現了u表明的接口,會返回真
    Implements(u Type) bool
    // 若是該類型的值能夠直接賦值給u表明的類型,返回真
    AssignableTo(u Type) bool
    // 如該類型的值能夠轉換爲u表明的類型,返回真
    ConvertibleTo(u Type) bool
    // 返回該類型的字位數。若是該類型的Kind不是Int、Uint、Float或Complex,會panic
    Bits() int
    // 返回array類型的長度,如非數組類型將panic
    Len() int
    // 返回該類型的元素類型,若是該類型的Kind不是Array、Chan、Map、Ptr或Slice,會panic
    Elem() Type
    // 返回map類型的鍵的類型。如非映射類型將panic
    Key() Type
    // 返回一個channel類型的方向,如非通道類型將會panic
    ChanDir() ChanDir

    // 返回struct類型的字段數(匿名字段算做一個字段),如非結構體類型將panic
    NumField() int
    // 返回struct類型的第i個字段的類型,如非結構體或者i不在[0, NumField())內將會panic
    Field(i int) StructField
    // 返回索引序列指定的嵌套字段的類型,
    // 等價於用索引中每一個值鏈式調用本方法,如非結構體將會panic
    FieldByIndex(index []int) StructField
    // 返回該類型名爲name的字段(會查找匿名字段及其子字段),
    // 布爾值說明是否找到,如非結構體將panic
    FieldByName(name string) (StructField, bool)
    // 返回該類型第一個字段名知足函數match的字段,布爾值說明是否找到,如非結構體將會panic
    FieldByNameFunc(match func(string) bool(StructField, bool)
    // 若是函數類型的最後一個輸入參數是"..."形式的參數,IsVariadic返回真
    // 若是這樣,t.In(t.NumIn() - 1)返回參數的隱式的實際類型(聲明類型的切片)
    // 如非函數類型將panic
    IsVariadic() bool
    // 返回func類型的參數個數,若是不是函數,將會panic
    NumIn() int
    // 返回func類型的第i個參數的類型,如非函數或者i不在[0, NumIn())內將會panic
    In(i int) Type
    // 返回func類型的返回值個數,若是不是函數,將會panic
    NumOut() int
    // 返回func類型的第i個返回值的類型,如非函數或者i不在[0, NumOut())內將會panic
    Out(i int) Type
    // 返回該類型的方法集中方法的數目
    // 匿名字段的方法會被計算;主體類型的方法會屏蔽匿名字段的同名方法;
    // 匿名字段致使的歧義方法會濾除
    NumMethod() int
    // 返回該類型方法集中的第i個方法,i不在[0, NumMethod())範圍內時,將致使panic
    // 對非接口類型T或*T,返回值的Type字段和Func字段描述方法的未綁定函數狀態
    // 對接口類型,返回值的Type字段描述方法的簽名,Func字段爲nil
    Method(int) Method
    // 根據方法名返回該類型方法集中的方法,使用一個布爾值說明是否發現該方法
    // 對非接口類型T或*T,返回值的Type字段和Func字段描述方法的未綁定函數狀態
    // 對接口類型,返回值的Type字段描述方法的簽名,Func字段爲nil
    MethodByName(string) (Method, bool)
    // 內含隱藏或非導出方法
}
複製代碼

咱們能夠經過reflect.TypeOf接受任意interface{}類型,並返回對應的動態類型reflect.Type

num := reflect.TypeOf(1)
fmt.Println(num.String())
fmt.Println(num)
複製代碼

看一下TypeOf()的實現代碼:

// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {
    eface := *(*emptyInterface)(unsafe.Pointer(&i))
    return toType(eface.typ)
}
複製代碼

能夠發現TypeOf函數的參數類型是一個interface{},而且在函數內部將這裏的具體值1進行一個隱式轉換,轉換爲一個空接口類型的變量,這個變量包含兩部分信息:1這個變量的動態類型(爲int)和動態值(爲1);最後TypeOf的返回值是reflect.Type類型(咱們稱爲反射類型對象),這樣就可以調用上面Type接口的方法獲取所需的變量信息。

  • 當反射對象的類型是原始數據類型時:
func main() {
    var s string
    rString := reflect.TypeOf(s)
    fmt.Println(rString)         //string
    fmt.Println(rString.Name())  //string,返回表示類型名稱的字符串
    fmt.Println(rString.Kind())  //string,返回 reflect.Kind 類型的常量
}
複製代碼
  • 當反射對象的類型是指針類型時:
type Dog struct {
    Name string
    Age  int
}

func main() {
    dogPtr := &Dog{"doggy"}
    rDogPtr := reflect.TypeOf(dogPtr)

    fmt.Println(rDogPtr.Name())  // 爲空
    fmt.Println(rDogPtr.Kind())  // ptr

    // Elem()能夠獲取指針指向的實際變量
    rDog := rDogPtr.Elem()
    fmt.Println(rDogPtr.Name())  // Dog
    fmt.Println(rDogPtr.Kind())  // struct
}
複製代碼

能夠發現從指針獲取反射對象時,不能直接使用Name()Kind(),這樣只能獲得該指針的信息。這時可使用Elem()獲取指針指向的實際變量。

  • 當反射對象的類型是結構體類型時:

若是反射對象的類型是結構體,能夠經過 NumField()Field() 方法得到結構體成員的詳細信息。

type Dog struct {
    Name string
    Age  int
}

func main() {
    dog := Dog{"doggy"2}
    rDog := reflect.TypeOf(dog)

    fmt.Printf("%v ", rDog.Name()) // Dog
    fmt.Println(rDog.Kind())       // struct

    for index := 0; index < rDog.NumField(); index++ {
        fmt.Printf("%v ", rDog.Field(index).Name)
        fmt.Println(rDog.Field(index).Type)
    }
}
複製代碼

運行輸出:

Dog struct
Name string
Age int
複製代碼
reflect.Value

reflect包中Value類型定義以下:

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.
    flag
}
複製代碼

能夠看到Value類型包含一個類型指針、一個值指針以及標誌信息。同時Value類型還有不少方法,其中用於獲取值方法:

func (v Value) Int() int64 // 獲取int類型值,若是 v 值不是有符號整型,則 panic

func (v Value) Uint() uint64 // 獲取unit類型的值,若是 v 值不是無符號整型(包括 uintptr),則 panic

func (v Value) Float() float64 // 獲取float類型的值,若是 v 值不是浮點型,則 panic

func (v Value) Complex() complex128 // 獲取複數類型的值,若是 v 值不是複數型,則 panic

func (v Value) Bool() bool // 獲取布爾類型的值,若是 v 值不是布爾型,則 panic

func (v Value) Len() int // 獲取 v 值的長度,v 值必須是字符串、數組、切片、映射、通道。

func (v Value) Cap() int  // 獲取 v 值的容量,v 值必須是數值、切片、通道。

func (v Value) Index(i int) reflect.Value // 獲取 v 值的第 i 個元素,v 值必須是字符串、數組、切片,i 不能超出範圍。

func (v Value) Bytes() []byte // 獲取字節類型的值,若是 v 值不是字節切片,則 panic

func (v Value) Slice(i, j int) reflect.Value // 獲取 v 值的切片,切片長度 = j - i,切片容量 = v.Cap() - i
// v 必須是字符串、數值、切片,若是是數組則必須可尋址。i 不能超出範圍。

func (v Value) Slice3(i, j, k int) reflect.Value  // 獲取 v 值的切片,切片長度 = j - i,切片容量 = k - i
// ijk 不能超出 v 的容量。i <= j <= k
// v 必須是字符串、數值、切片,若是是數組則必須可尋址。i 不能超出範圍。

func (v Value) MapIndex(key Value) reflect.Value // 根據 key 鍵獲取 v 值的內容,v 值必須是映射。
// 若是指定的元素不存在,或 v 值是未初始化的映射,則返回零值(reflect.ValueOf(nil)

func (v Value) MapKeys() []reflect.Value // 獲取 v 值的全部鍵的無序列表,v 值必須是映射。
// 若是 v 值是未初始化的映射,則返回空列表。

func (v Value) OverflowInt(x int64) bool // 判斷 x 是否超出 v 值的取值範圍,v 值必須是有符號整型。

func (v Value) OverflowUint(x uint64) bool  // 判斷 x 是否超出 v 值的取值範圍,v 值必須是無符號整型。

func (v Value) OverflowFloat(x float64) bool  // 判斷 x 是否超出 v 值的取值範圍,v 值必須是浮點型。

func (v Value) OverflowComplex(x complex128) bool // 判斷 x 是否超出 v 值的取值範圍,v 值必須是複數型。
複製代碼

用於設置值方法:

func (v Value) SetUint(x uint64)  // 設置無符號整型的值

func (v Value) SetFloat(x float64) // 設置浮點類型的值

func (v Value) SetComplex(x complex128) //設置複數類型的值

func (v Value) SetBool(x bool) //設置布爾類型的值

func (v Value) SetString(x string) //設置字符串類型的值

func (v Value) SetLen(n int)  // 設置切片的長度,n 不能超出範圍,不能爲負數。

func (v Value) SetCap(n int) //設置切片的容量

func (v Value) SetBytes(x []byte) //設置字節類型的值

func (v Value) SetMapIndex(key, val reflect.Value) //設置mapkeyvalue,前提必須是初始化之後,存在覆蓋、不存在添加

func (v Value) Set(x Value) // 將v的持有值修改成x的持有值。若是v.CanSet()返回假,會panicx的持有值必須能直接賦給v持有值的類型。
複製代碼

其餘方法:

結構體相關:
func (v Value) NumField() int // 獲取結構體字段(成員)數量

func (v Value) Field(i int) reflect.Value  //根據索引獲取結構體字段

func (v Value) FieldByIndex(index []int) reflect.Value // 根據索引鏈獲取結構體嵌套字段

func (v Value) FieldByName(string) reflect.Value // 根據名稱獲取結構體的字段,不存在返回reflect.ValueOf(nil)

func (v Value) FieldByNameFunc(match func(string) boolValue // 根據匹配函數 match 獲取字段,若是沒有匹配的字段,則返回零值(reflect.ValueOf(nil)


通道相關:
func (v Value) Send(x reflect.Value)// 發送數據(會阻塞),v 值必須是可寫通道。

func (v Value) Recv() (x reflect.Value, ok bool) // 接收數據(會阻塞),v 值必須是可讀通道。

func (v Value) TrySend(x reflect.Value) bool // 嘗試發送數據(不會阻塞),v 值必須是可寫通道。

func (v Value) TryRecv() (x reflect.Value, ok bool) // 嘗試接收數據(不會阻塞),v 值必須是可讀通道。

func (v Value) Close() // 關閉通道


函數相關
func (v Value) Call(in []Value) (r []Value) // 經過參數列表 in 調用 v 值所表明的函數(或方法)。函數的返回值存入 r 中返回。
// 要傳入多少參數就在 in 中存入多少元素。
// Call 便可以調用定參函數(參數數量固定),也能夠調用變參函數(參數數量可變)。

func (v Value) CallSlice(in []Value) []Value // 調用變參函數
複製代碼

一樣地,咱們能夠經過reflect.ValueOf接受任意interface{}類型,並返回對應的動態類型reflect.Value

v := reflect.ValueOf(2)
fmt.Println(v)  // 2
fmt.Println(v.String()) // <int Value>
複製代碼

看一下reflect.ValueOf的實現代碼:

func ValueOf(i interface{}) Value {
    if i == nil {
        return Value{}
    }

    // TODO: Maybe allow contents of a Value to live on the stack.
    // For now we make the contents always escape to the heap. It
    // makes life easier in a few places (see chanrecv/mapassign
    // comment below).
    escapes(i)

    return unpackEface(i)
}

// unpackEface converts the empty interface i to a Value.
func unpackEface(i interface{}) Value {
    e := (*emptyInterface)(unsafe.Pointer(&i))
    // NOTE: don't read e.word until we know whether it is really a pointer or not.
    t := e.typ
    if t == nil {
        return Value{}
    }
    f := flag(t.Kind())
    if ifaceIndir(t) {
        f |= flagIndir
    }
    return Value{t, e.word, f}
}
複製代碼

escapes() 涉及棧和堆的對象分配以及逃逸分析,有興趣的能夠看 William Kennedy 寫的系列文章: Go 語言機制之逃逸分析

reflect.TypeOf相似,ValueOf函數的參數類型是一個interface{},在函數內部將入參進行一個隱式轉換,轉換爲一個空接口類型的變量,最終返回一個Value對象,而且reflect.ValueOf返回值也是反射類型對象

能夠注意到Value對象中也包含了實際值的類型信息,經過ValueType() 方法將返回具體類型所對應的reflect.Type:

v := reflect.ValueOf(2)
t := v.Type()
fmt.Println(t) // int
fmt.Println(t.String()) // int
複製代碼

經過`relfect.Value`獲取實際變量的信息

如今咱們知道了經過reflect.ValueOf能夠將接口類型變量轉換成反射類型變量,固然咱們也能夠經過reflect.Value.Interface方法逆操做回去,而後經過斷言的方式獲得實際值:

v := reflect.ValueOf(2)
i := v.Interface()
if num, ok := i.(int); ok { // 類型斷言
    fmt.Println(num)
}
複製代碼

但一般在實際場景中,咱們其實並不知道原始值的類型,這裏就須要利用reflect.Typereflect.Value的方法探索原始值的信息。下面經過一個例子說明:

package main

import (
    "fmt"
    "reflect"
)

type Dog struct {
    Name string
    Age  int
}

func (dog *Dog) Eat() {
    fmt.Printf("%s is eating.", dog.Name)
}

func (dog *Dog) Run() {
    fmt.Printf("%s is running.", dog.Name)
}

func (dog Dog) Sleep() {
    fmt.Printf("%s is sleeping.", dog.Name)
}

func (dog Dog) Jump() {
    fmt.Printf("%s is jumping.", dog.Name)
}

func main() {
    doggy := Dog{"doggy"2}
    checkFieldAndMethod(doggy)

    fmt.Println("")
    tommy := &Dog{"tommy"2}
    checkFieldAndMethod(tommy)
}

func checkFieldAndMethod(input interface{}) {
    inputType := reflect.TypeOf(input)
    fmt.Println("Type of input is :", inputType.Name())
    inputValue := reflect.ValueOf(input)
    fmt.Println("Value of input is :", inputValue)

    // 若是input原始類型時指針,經過Elem()方法或者Indirect()獲取指針指向的值
    if inputValue.Kind() == reflect.Ptr {
        inputValue = inputValue.Elem()
        // inputValue = reflect.Indirect(inputValue)
        fmt.Println("Value input points to is :", inputValue)
    }

    //使用NumField()獲得結構體中字段的數量,遍歷獲得字段的值Field(i)和類型Field(i).Type()
    for i := 0; i < inputValue.NumField(); i++ {
        field := inputValue.Type().Field(i)
        value := inputValue.Field(i).Interface()
        fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
    }

    // 獲取方法
    for i := 0; i < inputType.NumMethod(); i++ {
        m := inputType.Method(i)
        fmt.Printf("%s: %v\n", m.Name, m.Type)
    }
}
複製代碼

運行以後輸出:

Type of input is : Dog
Value of input is : {doggy 2}
Namestring = doggy
Age: int = 2
Jump: func(main.Dog)
Sleep: func(main.Dog)

Type of input is : 
Value of input is : &{tommy 2}
Value input points to is : {tommy 2}
Namestring = tommy
Age: int = 2
Eat: func(*main.Dog)
Jump: func(*main.Dog)
Run: func(*main.Dog)
Sleep: func(*main.Dog)
複製代碼

利用反射獲取原始值得類型和方法的步驟以下:

  • 判斷原始值是值變量仍是指針變量,若是是指針變量,則經過Elem()方法或者Indirect()獲取指針指向的值;
  • 使用NumField()獲得結構體中字段的數量,遍歷獲得字段的值Field(i)和類型Field(i).Type()
  • 使用NumMethod()獲得結構體的方法,遍歷獲得方法的名稱和類型。

另外,在使用reflect.Value過程有時會對Elem()方法和Indirect()有些迷惑,搞不清這兩個方法的區別,這裏總結一下:

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

// Indirect returns the value that v points to.
// If v is a nil pointerIndirect returns a zero Value.
// If v is not a pointerIndirect returns v.
func Indirect(v Value) Value
複製代碼
  • Elem返回v持有的接口保管的值的Value封裝,或者v持有的指針指向的值的Value封裝。若是v的Kind不是InterfacePtrpanic;若是v持有的值爲nil,會返回Value零值。
  • Indirect返回v持有的指針指向的值的Value封裝。若是v持有的值爲nil,會返回Value零值。若是v持有的變量不是指針,那麼將返回原值v。

也就是說,當v持有的變量是指針時,Elem()方法和Indirect()是等價的。

細心的讀者可能發現對於值變量和指針變量,經過反射獲取到的變量方法有些差別,這個問題就留給讀者本身思考吧。

經過`relfect.Value`修改實際變量的信息

當經過relfect.Value修改實際變量的信息是經常使用到如下反射值對象的方法:

func (v Value) Elem() Value  
//Elem()返回v持有的接口保管的值的Value封裝,或者v持有的指針指向的值的Value封裝,相似於*操做,此時的Value表示的是Value的元素且能夠尋址。

func (v Value) Addr() Value 
//Addr()返回一個持有指向v變量地址的指針的Value封裝,相似於&操做。

func (v Value) CanAddr() bool
//CanAddr()返回是否能夠獲取v持有值的指針。能夠獲取指針的值被稱爲可尋址的。

func (v Value) CanSet() bool
//CanSet()返回v持有的值是否能夠被修改
複製代碼

然而,值得注意的是並非全部reflect.Value類型的反射值均可以修改,考慮下面這個例子:

package main 

import(
    "fmt"
    "reflect"
)

func main() {
    a := 1
    rA := reflect.ValueOf(a)
    fmt.Println(rA.CanSet()) //false

    rAptr := reflect.ValueOf(&a)
    rA2 := rAptr.Elem()
    fmt.Println(rA2.CanSet()) //true
    rA2.SetInt(2)
    fmt.Println(rA2.Int()) //2
}
複製代碼

修改反射類型變量的值有兩個條件:

  • 反射類型變量的值是addressable的,便可取地址的;
  • 反射類型變量的值來自導出字段。

有一些修改反射類型變量是可尋址的,有一些則不是:

package main

import (
    "reflect"
    "fmt"
)

func main() {
    x := 2
    a := reflect.ValueOf(2)
    b := reflect.ValueOf(x)
    c := reflect.ValueOf(&x)
    d := c.Elem()
    fmt.Println(a.CanAddr()) // false
    fmt.Println(b.CanAddr()) // false
    fmt.Println(c.CanAddr()) // false
    fmt.Println(d.CanAddr()) // true

}
複製代碼

對於非指針變量x,經過reflect.ValueOf(x)返回的 reflect.Value是不可取地址的。可是對於d,它是c的解引用方式生成的,指向另外一個變量,所以是可 取地址的。咱們能夠經過調用reflect.ValueOf(&x).Elem(),獲取到x對應的可取地址的反射值。

對於結構體類型變量,若是成員字段沒有導出,那麼雖然能夠被訪問,但不能經過反射修改:

package main

import (
    "fmt"
    "reflect"
)

type Dog struct {
    Name string
    Age  int
    sex  string
}

func main() {
    rDog := reflect.ValueOf(&Dog{}).Elem()
    vAge := rDog.FieldByName("Age")
    vAge.SetInt(1)

    vSex := rDog.FieldByName("sex")
    vSex.SetString("male")
}
複製代碼

運行出現報錯:SetString使用的值來自於一個未導出的字段。

panic: reflect: reflect.Value.SetString using value obtained using unexported field
複製代碼

爲了能修改這個值,須要將該字段導出。將Dog類型中的 sex成員首字母大寫便可。

修改可取地址的reflect.Value持有的變量值,除了能夠經過反射的Set系列方法,還能夠經過從反射類型變量獲取實際值的指針來修改:

package main

import (
    "reflect"
    "fmt"
)

func main() {
    x := 1
    v := reflect.ValueOf(&x).Elem()
    px := v.Addr().Interface().(*int)
    *px = 2
    fmt.Print(x) //2
}
複製代碼

首先調用Addr()方法,返回 一個持有指向變量的指針的Value;而後在Value上調用Interface()方法,返回一個 interface{},裏面包含指向變量的指針;最後經過類型斷言獲得普通指針來修改變量的值。

經過反射調用函數

若是反射值對象(reflect.Value)持有值的類型爲函數時,能夠經過 reflect.Value 調用該函數。

func (v Value) Call(in []Value) []Value
複製代碼

Call方法使用輸入的參數in調用v持有的函數。參數in是反射值對象的切片,即[]reflect.Value;調用完成時,函數的返回值經過 []reflect.Value 返回。

package main 

import(
    "fmt"
    "reflect"
)
func add(a, b int) int {

    return a + b
}

func main() {

    // 將函數add包裝爲反射值對象
    funcValue := reflect.ValueOf(add)

    // 構造函數add的參數, 傳入兩個整型值
    paramList := []reflect.Value{reflect.ValueOf(5), reflect.ValueOf(10)}

    // 反射調用函數Call()
    retList := funcValue.Call(paramList)

    // 獲取第一個返回值, 取整數值
    fmt.Println(retList[0].Int()) //返回 15
}
複製代碼

若是須要經過反射調用結構體的方法,能夠利用MethodByName方法來完成:

func (v Value) MethodByName(name string) Value
//返回v的名爲name的方法的已綁定(到v的持有值的)狀態的函數形式的Value封裝。
複製代碼

舉例:

package main 

import(
    "fmt"
    "reflect"
)

type Dog struct {
    Name string
    Age  int
}

func (dog *Dog) SetName(name string){
    dog.Name = name
}

func main() {
    dog := Dog{}
    rDog := reflect.ValueOf(&dog)
    paramList1 := []reflect.Value{reflect.ValueOf("doggy")}
    rDog.MethodByName("SetName").Call(paramList1)
    fmt.Println(dog.Name) //doggy
}
複製代碼

值得注意的是,反射調用函數的過程須要構造大量的 reflect.Value 和中間變量,對函數參數值進行逐一檢查,還須要將調用參數複製到調用函數的參數內存中。調用完畢後,還須要將返回值轉換爲 reflect.Value,用戶還須要從中取出調用值。所以反射調用函數的性能問題尤其突出,不建議大量使用反射函數調用。

總結

本文介紹了Go語言中interface的定義、用法以及反作用,並由此引入reflect,經過大量示例詳細介紹了reflect的概念,經過reflect獲取值、修改值的用法,以及調用函數的用法。內容上能夠說至關詳實具體了,在此過程當中也讓筆者本身對這部分的知識有了更深入的認識,也但願有幸能帶給讀者一點幫助吧。

參考資料

【Golang標準庫文檔】

【Golang的反射reflect深刻理解和示例】

【Go addressable 詳解】

相關文章
相關標籤/搜索