初識golang的反射

定義類型、聲明變量、使用變量是一門編程語言的基本功能,咱們能夠這樣來定義一個結構體類型:golang

type Foo struct {
    X string `foo:"x"`
    Y int    `foo:"y"`
}
複製代碼

像這樣來使用這個類型聲明一個變量:編程

var bar Foo
複製代碼

使用變量也很方便,像這樣就能夠在終端打印出結構體的字段了:json

fmt.Printf("%s, %s", bar.X, bar.Y)
複製代碼

以上這些操做都是在明確了變量的類型的時候進行的,不過在編程過程當中,咱們可能會遇到一種狀況:在編寫代碼時沒法明確變量的類型,變量的信息只有在程序運行時在會獲取,好比這樣的函數:bash

func theFunc(val interface{}) 複製代碼

它的函數簽名只有一個參數類型爲 interface 的參數 val,這意味着能夠將任意類型的變量做爲實參傳入函數。這樣的狀況下該如何操做變量 val 呢?golang 標準庫中有一個 reflect包--即反射--它提供了一系列的方法能幫助咱們在運行時獲取變量的信息,或者修改變量的值。在反射中有三個比較重要的概念:Type、Kind 和 Value。下面就一塊兒來看看反射的奇妙之處吧。編程語言

若是咱們把 bar 變量傳入了 theFunc 函數,在函數中咱們須要知道些什麼信息呢?可能會須要知道傳入的變量是什麼類型的,若是是一個結構體,可能還會須要知道結構體中有那些字段,這些字段又是什麼類型的。咱們可能還須要根據結構體字段特定的 tag 來執行特定的操做,好比 encoding/json 包就會根據 tag 來給序列化的 json 字段取名。若是傳入的變量是咱們所指望的,咱們可能還須要修改它的值,或者用它來建立一個新的變量。咱們就一個個來談談如何用反射來實現這些功能把。函數

獲取變量類型

TypeOf()

reflect 提供了 TypeOf 函數來獲取指定變量的類型,它的返回值類型爲 reflect.Type,這是一個接口類型,它提供了一系列的方法來獲取變量相關信息的方法。ui

Name()、Kind() 和 Elem()

Name() 方法獲取變量的類型名稱,不過它有一個限制,即只能獲取基本類型或者自定義的結構體的類型名稱,其餘的類型會返回一個空的字符串。spa

Kind() 方法會返回變量的內置類型名稱,好比 ptr、slice、array、map、func、struct 等等。它一般能夠和 switch 配合來作類型判斷。指針

Elem() 方法用於判斷類型的元素類型(type's element type)。它是對 Name 方法的補充,它能夠返回 array, chan, map, ptr, 或 slice 類型中元素的類型。好比,針對一個指針 &bar, Elem 方法會返回 Foo 這個類型名稱;針對 []string 這樣一個字符串 slice,Elem 會返回 string 這個類型名稱。若是不在容許的類型上調用 Elem 方法,會致使 panic ,其實 reflect 包中不少方法和函數都是這樣的,它要求使用者知道本身在作什麼。code

下面來舉一個完整的示例吧:

import (
	"fmt"
	"reflect"
)

type Foo struct {
	X string  `foo:"x"`
	Y string `foo:"y"`
}

func main() {
	bar := Foo{
		X: "hello",
		Y: "world",
	}
	sli := make([]string, 0)
	ch := make(chan bool)
	m := make(map[int]int)
	arr := [10]int{}
	i := 0
	f := 1.1
	b := true

	theFunc(bar)
	theFunc(&bar)
	theFunc(sli)
	theFunc(ch)
	theFunc(m)
	theFunc(arr)
	theFunc(theFunc)
	theFunc(i)
	theFunc(f)
	theFunc(b)
}

func theFunc(val interface{}) {
	valType := reflect.TypeOf(val)
	fmt.Printf("name of value : %s, kind of value : %s, ", valType.Name(), valType.Kind())

	switch valType.Kind() {
	case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Slice, reflect.Map:
		fmt.Printf("elem of value : %s", valType.Elem())
	}
	fmt.Println()
}
複製代碼

輸出爲:

獲取變量值

ValueOf()

ValueOf() 函數獲取變量中實際存儲的值。例如:

bar := Foo {
    X: "hello",
    Y: "world",
}

fmt.Println(reflect.ValueOf(bar))
複製代碼

輸出的結果爲:

{"hello", "world"}
複製代碼

類型斷言

若是使用者知道傳入的值是什麼類型的,或者經過類型判斷的方式獲取了值的類型信息,那麼可使用 val.(type) 這種類型斷言的方式將傳入的 interface 類型的數據強制轉換成咱們所須要的類型,這時候就能夠正常使用類型的字段了。須要注意的是,若是類型斷言出錯了,那麼將會引起 panic,因此,在使用的時候能夠先確認變量的類型再進行類型斷言,或者利用類型斷言的第二個布爾類型的返回值來判斷斷言是否成功。

func main() {
	bar := Foo {
	    X: "hello",
	    Y: "world",
	}
    
    theFunc(bar)
}

func theFunc(val interface{}) {
    // v1 := val.(int) // 引起 panic
    //在使用斷言前先判斷類型是否正確
    if reflect.TypeOf(val) == reflect.TypeOf(Foo{}) {
        v2 := val.(Foo)
        fmt.Println(v2)
    }
    //使用 ok 來判斷斷言是否成功
    if v2, ok := val.(Foo); ok {
        fmt.Println(v2)
    }
}
複製代碼

輸出爲

{"hello" "world"}
{"hello" "world"}
複製代碼

遍歷類型的方法

NumMethod()、Method(int)、 MethodByName(string) 這幾個方法能夠獲取變量所對應的類型已導出的方法:

type Foo struct {
	X string  `foo:"x"`
	Y string `foo:"y"`
}

func (Foo) unExported() {
	fmt.Println("unExported")
}

func (Foo) Exported()  {
	fmt.Println("Exported")
}

func main() {
	bar := Foo{
		X: "hello",
		Y: "world",
	}

	valTheFun(bar)
}

func valTheFun(val interface{}) {
	valType := reflect.TypeOf(val)
	fmt.Println(valType.NumMethod())
	for i := 0; i < valType.NumMethod(); i++ {
		fmt.Println(valType.Method(i).Name)
	}
}
複製代碼

輸出結果:

1
Exported
複製代碼

遍歷函數的入參和出參

對於函數類型,Type 接口也提供了相應的方法來遍歷入參和出參:NumIn(),In(i int), NumOut,Out(i int)。這幾個方法只能用與類型爲函數的變量,不然將會引起 panic 。

func main() {
    valTheFun(valTheFun)
	valTheFun(param)
}

func param(i int, s string) (int, string) {
	return i, s
}

func valTheFun(val interface{}) {
	valType := reflect.TypeOf(val)
	
    fmt.Printf("number of in args %d:\n", valType.NumIn())
	for i := 0; i < valType.NumIn(); i++ {
		fmt.Printf("\t %s\n", valType.In(i))
	}
	fmt.Printf("number of out args %d:\n", valType.NumOut())
	for i := 0; i < valType.NumOut(); i++ {
		fmt.Printf("\t %s\n", valType.Out(i))
	}
}
複製代碼

輸出:

number of in args 1:
	 interface {}
number of out args 0:
number of in args 2:
	 int
	 string
number of out args 2:
	 int
	 string
複製代碼

遍歷結構體的字段

遍歷結構體的字段是十分經常使用的,當咱們須要統一處理 kind 爲結構體的入參,但又不知道結構體的具體類型,也不須要知道結構體的具體類型的時候,就能夠用上遍歷結構體字段的一系列方法了。結構體中還有一個 tag 屬性,用它能夠給結構體中的字段添加額外的屬性,golang 也提供了方法用於遍歷 tag 的數據。我的以爲這一部分的功能仍是須要好好研究一下的,熟悉這部分的操做能夠寫出更好的代碼,golang 標準庫中比較經常使用的場景有: encoding/json 將結構體序列化爲 json 字符串;encoding/xml 將結構體序列化爲 xml 數據等等。下面給一個簡單的例子:

func main() {
	bar := Foo{
		X: "hello",
		Y: "world",
	}

	structFunc(bar)
}

func structFunc(val interface{}) {
	valType := reflect.TypeOf(val)
	fmt.Println("number of fields in val :", valType.NumField())
	for i := 0; i < valType.NumField(); i++ {
		fmt.Printf("field : %s ", valType.Field(i).Name)
		fmt.Println("tags", valType.Field(i).Tag)
	}
}
複製代碼

輸出:

number of fields in val : 2
field : X tags foo:"x"
field : Y tags foo:"y"
複製代碼

建立新的變量

reflect 中有兩種函數能夠建立新的變量,分別是 New(Type) 和 Make* 。用兩種是由於 Make* 是一系列的函數,它們和內建函數 make 同樣,只能爲 slice, map, chan 來建立新的變量,不一樣的是 reflect 爲這幾個類型都分別聲明瞭一個 Make 函數,而且還爲 func 類型也聲明瞭一個 Make 函數。而 New 和內建的 new 函數同樣,返回的是建立的變量的指針,這個很重要。由於給新建的變量設置值的時候,須要使用 Field() 方法來指定結構體中的字段,而這個方法的接收器必須爲 struct,因此,新建的變量必需要先調用 Elem() 方法來獲取對應的結構體類型,而後再調用 Field() 方法來設置新的值。

func main() {
	bar := Foo{
		X: "hello",
		Y: "world",
	}

	valType := reflect.TypeOf(bar)
	valFields := reflect.ValueOf(bar)

	val := reflect.New(valType)
	//由於 val 是一個指針,因此須要使用 Elem 來獲取元素的實際類型
	val.Elem().Field(0).SetString(valFields.Field(0).String())
	val.Elem().Field(1).SetString("golang")

	//val 是一個 reflect.Value 類型的變量,
    //須要經過 Interface() 來獲取它所維護的數據,
    //而後再經過類型斷言強制轉換爲指定的類型
	if v, ok := val.Interface().(*Foo); !ok {
		panic("wrong type")
	} else {
		fmt.Println(*v)
	}
}
複製代碼

輸出:

{hello golang}
複製代碼

小結

到這裏,基本是把 reflect 的基本操做都講了一遍了,原本是想簡單寫寫的,結果越寫越多。可能有些方面沒有將的十分清楚,請各位看官多多斧正。

相關文章
相關標籤/搜索