重學Golang系列(一): 深入理解 interface和reflect

  • 2019 年 10 月 13 日
  • 筆記

前言

interface(即介面),是Go語言中一個重要的概念和知識點,而功能強大的reflect正是基於interface。本文即是對Go語言中的interfacereflect基礎概念和用法的一次梳理,也算是我階段學習的總結,以期溫故而知新。

interface(介面)

定義

在Go語言中,如果自定義類型(比如struct)實現了某個interface中的所有方法,那麼就可以說這個類型實現了這個介面。介面可如下定義:

type 介面名稱 interface {      method1(參數列表) 返回值列表      method1(參數列表) 返回值列表      ...  }

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

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初始化就使用,那麼會輸出nil

空介面

空介面interface{}沒有任何方法,所以所有類型都實現了空介面, 即我們可以把任何一個變數賦值給空介面。修改一下上面的main函數:

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

運行結果:

&{doggy}

介面繼承

一個介面可以繼承多個其他介面,如果要實現這個介面,那麼必須將所繼承的所有介面中的方法都實現。

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()  }  

類型斷言

當我們不確定某個介面變數里存儲的是什麼類型的變數時,我們可以利用類型斷言來判斷變數類型。

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([]string, 0)      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。  // i、j、k 不能超出 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) //設置map的key和value,前提必須是初始化以後,存在覆蓋、不存在添加    func (v Value) Set(x Value) // 將v的持有值修改為x的持有值。如果v.CanSet()返回假,會panic。x的持有值必須能直接賦給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) bool) Value // 根據匹配函數 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 = %vn", field.Name, field.Type, value)      }        // 獲取方法      for i := 0; i < inputType.NumMethod(); i++ {          m := inputType.Method(i)          fmt.Printf("%s: %vn", m.Name, m.Type)      }  }

運行之後輸出:

Type of input is : Dog  Value of input is : {doggy 2}  Name: string = 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}  Name: string = 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 pointer, Indirect returns a zero Value.  // If v is not a pointer, Indirect 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 詳解】