1. 程式人生 > 程式設計 >學習使用Go反射的用法示例

學習使用Go反射的用法示例

什麼是反射

大多數時候,Go中的變數,型別和函式非常簡單直接。當需要一個型別、變數或者是函式時,可以直接定義它們:

type Foo struct {
 A int
 B string
}

var x Foo

func DoSomething(f Foo) {
 fmt.Println(f.A,f.B)
}

但是有時你希望在執行時使用變數的在編寫程式時還不存在的資訊。比如你正在嘗試將檔案或網路請求中的資料對映到變數中。或者你想構建一個適用於不同型別的工具。在這種情況下,你需要使用反射。反射使您能夠在執行時檢查型別。它還允許您在執行時檢查,修改和建立變數,函式和結構體。

Go中的反射是基於三個概念構建的:型別,種類和值(Types Kinds Values)。標準庫中的reflect包提供了 Go 反射的實現。

反射變數型別

首先讓我們看一下型別。你可以使用反射來呼叫函式varType := reflect.TypeOf(var)來獲取變數var的型別。這將返回型別為reflect.Type的變數,該變數具有獲取定義時變數的型別的各種資訊的方法集。下面我們來看一下常用的獲取型別資訊的方法。

我們要看的第一個方法是Name()。這將返回變數型別的名稱。某些型別(例如切片或指標)沒有名稱,此方法會返回空字串。

下一個方法,也是我認為第一個真正非常有用的方法是Kind()。Type是由Kind組成的---Kind 是切片,對映,指標,結構,介面,字串,陣列,函式,int或其他某種原始型別的抽象表示。要理解Type和Kind之間的差異可能有些棘手,但是請你以這種方式來思考。如果定義一個名為Foo的結構體,則Kind為struct,型別為Foo。

使用反射時要注意的一件事:反射包中的所有內容都假定你知道自己在做什麼,並且如果使用不正確,許多函式和方法呼叫都會引起 panic。例如,如果你在reflect.Type上呼叫與當前型別不同的型別關聯的方法,您的程式碼將會panic。

如果變數是指標,對映,切片,通道或陣列變數,則可以使用varType.Elem()找出指向或包含的值的型別。

如果變數是結構體,則可以使用反射來獲取結構體中的欄位數,並從每個欄位上獲取reflect.StructField結構體。 reflection.StructField為您提供了欄位的名稱,標號,型別和結構體標籤。其中標籤資訊對應reflect.StructTag型別的字串,並且它提供了Get方法用於解析和根據特定key提取標籤資訊中的子串。

下面是一個簡單的示例,用於輸出各種變數的型別資訊:

type Foo struct {
  A int `tag1:"First Tag" tag2:"Second Tag"`
  B string
}

func main() {
  sl := []int{1,2,3}
  greeting := "hello"
  greetingPtr := &greeting
  f := Foo{A: 10,B: "Salutations"}
  fp := &f

  slType := reflect.TypeOf(sl)
  gType := reflect.TypeOf(greeting)
  grpType := reflect.TypeOf(greetingPtr)
  fType := reflect.TypeOf(f)
  fpType := reflect.TypeOf(fp)

  examiner(slType,0)
  examiner(gType,0)
  examiner(grpType,0)
  examiner(fType,0)
  examiner(fpType,0)
}

func examiner(t reflect.Type,depth int) {
  fmt.Println(strings.Repeat("\t",depth),"Type is",t.Name(),"and kind is",t.Kind())
  switch t.Kind() {
  case reflect.Array,reflect.Chan,reflect.Map,reflect.Ptr,reflect.Slice:
    fmt.Println(strings.Repeat("\t",depth+1),"Contained type:")
    examiner(t.Elem(),depth+1)
  case reflect.Struct:
    for i := 0; i < t.NumField(); i++ {
      f := t.Field(i)
      fmt.Println(strings.Repeat("\t","Field",i+1,"name is",f.Name,"type is",f.Type.Name(),f.Type.Kind())
      if f.Tag != "" {
        fmt.Println(strings.Repeat("\t",depth+2),"Tag is",f.Tag)
        fmt.Println(strings.Repeat("\t","tag1 is",f.Tag.Get("tag1"),"tag2 is",f.Tag.Get("tag2"))
      }
    }
  }
}

變數的型別輸出如下:

Type is and kind is slice
   Contained type:
   Type is int and kind is int
 Type is string and kind is string
 Type is and kind is ptr
   Contained type:
   Type is string and kind is string
 Type is Foo and kind is struct
   Field 1 name is A type is int and kind is int
     Tag is tag1:"First Tag" tag2:"Second Tag"
     tag1 is First Tag tag2 is Second Tag
   Field 2 name is B type is string and kind is string
 Type is and kind is ptr
   Contained type:
   Type is Foo and kind is struct
     Field 1 name is A type is int and kind is int
       Tag is tag1:"First Tag" tag2:"Second Tag"
       tag1 is First Tag tag2 is Second Tag
     Field 2 name is B type is string and kind is string

Run in go playground: https://play.golang.org/p/lZ97yAUHxX

使用反射建立新例項

除了檢查變數的型別外,還可以使用反射來讀取,設定或建立值。首先,需要使用refVal := reflect.ValueOf(var) 為變數建立一個reflect.Value例項。如果希望能夠使用反射來修改值,則必須使用refPtrVal := reflect.ValueOf(&var);獲得指向變數的指標。如果不這樣做,則可以使用反射來讀取該值,但不能對其進行修改。

一旦有了reflect.Value例項就可以使用Type()方法獲取變數的reflect.Type。

如果要修改值,請記住它必須是一個指標,並且必須首先對其進行解引用。使用refPtrVal.Elem().Set(newRefVal)來修改值,並且傳遞給Set()的值也必須是reflect.Value。

如果要建立一個新值,可以使用函式newPtrVal := reflect.New(varType)來實現,並傳入一個reflect.Type。這將返回一個指標值,然後可以像上面那樣使用Elem().Set()對其進行修改。

最後,你可以通過呼叫Interface()方法從reflect.Value回到普通變數值。由於Go沒有泛型,因此變數的原始型別會丟失;該方法返回型別為interface{}的值。如果建立了一個指標以便可以修改該值,則需要使用Elem().Interface()解引用反射的指標。在這兩種情況下,都需要將空介面轉換為實際型別才能使用它。

下面的程式碼來演示這些概念:

type Foo struct {
  A int `tag1:"First Tag" tag2:"Second Tag"`
  B string
}

func main() {
  greeting := "hello"
  f := Foo{A: 10,B: "Salutations"}

  gVal := reflect.ValueOf(greeting)
  // not a pointer so all we can do is read it
  fmt.Println(gVal.Interface())

  gpVal := reflect.ValueOf(&greeting)
  // it's a pointer,so we can change it,and it changes the underlying variable
  gpVal.Elem().SetString("goodbye")
  fmt.Println(greeting)

  fType := reflect.TypeOf(f)
  fVal := reflect.New(fType)
  fVal.Elem().Field(0).SetInt(20)
  fVal.Elem().Field(1).SetString("Greetings")
  f2 := fVal.Elem().Interface().(Foo)
  fmt.Printf("%+v,%d,%s\n",f2,f2.A,f2.B)
}

他們的輸出如下:

hello
goodbye
{A:20 B:Greetings},20,Greetings

Run in go playground https://play.golang.org/p/PFcEYfZqZ8

反射建立引用型別的例項

除了生成內建型別和使用者定義型別的例項之外,還可以使用反射來生成通常需要make函式的例項。可以使用reflect.MakeSlice,reflect.MakeMap和reflect.MakeChan函式製作切片,Map或通道。在所有情況下,都提供一個reflect.Type,然後獲取一個reflect.Value,可以使用反射對其進行操作,或者可以將其分配回一個標準變數。

func main() {
 // 定義變數
  intSlice := make([]int,0)
  mapStringInt := make(map[string]int)

 // 獲取變數的 reflect.Type
  sliceType := reflect.TypeOf(intSlice)
  mapType := reflect.TypeOf(mapStringInt)

  // 使用反射建立型別的新例項
  intSliceReflect := reflect.MakeSlice(sliceType,0)
  mapReflect := reflect.MakeMap(mapType)

  // 將建立的新例項分配回一個標準變數
  v := 10
  rv := reflect.ValueOf(v)
  intSliceReflect = reflect.Append(intSliceReflect,rv)
  intSlice2 := intSliceReflect.Interface().([]int)
  fmt.Println(intSlice2)

  k := "hello"
  rk := reflect.ValueOf(k)
  mapReflect.SetMapIndex(rk,rv)
  mapStringInt2 := mapReflect.Interface().(map[string]int)
  fmt.Println(mapStringInt2)
}

使用反射建立函式

反射不僅僅可以為儲存資料創造新的地方。還可以使用reflect.MakeFunc函式使用reflect來建立新函式。該函式期望我們要建立的函式的reflect.Type,以及一個閉包,其輸入引數為[]reflect.Value型別,其返回型別也為[] reflect.Value型別。下面是一個簡單的示例,它為傳遞給它的任何函式建立一個定時包裝器:

func MakeTimedFunction(f interface{}) interface{} {
  rf := reflect.TypeOf(f)
  if rf.Kind() != reflect.Func {
    panic("expects a function")
  }
  vf := reflect.ValueOf(f)
  wrapperF := reflect.MakeFunc(rf,func(in []reflect.Value) []reflect.Value {
    start := time.Now()
    out := vf.Call(in)
    end := time.Now()
    fmt.Printf("calling %s took %v\n",runtime.FuncForPC(vf.Pointer()).Name(),end.Sub(start))
    return out
  })
  return wrapperF.Interface()
}

func timeMe() {
  fmt.Println("starting")
  time.Sleep(1 * time.Second)
  fmt.Println("ending")
}

func timeMeToo(a int) int {
  fmt.Println("starting")
  time.Sleep(time.Duration(a) * time.Second)
  result := a * 2
  fmt.Println("ending")
  return result
}

func main() {
  timed := MakeTimedFunction(timeMe).(func())
  timed()
  timedToo := MakeTimedFunction(timeMeToo).(func(int) int)
  fmt.Println(timedToo(2))
}

你可以在goplayground執行程式碼https://play.golang.org/p/QZ8ttFZzGx並看到輸出如下:

starting
ending
calling main.timeMe took 1s
starting
ending
calling main.timeMeToo took 2s
4

反射是每個Go開發人員都應瞭解並學會的強大工具。但是使用他們可以用來做什麼呢?在下一篇部落格文章中,我將探討現有庫中反射的一些用法,並使用反射來建立一些新的東西。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。