Golang reflect使用指南

Go提供了各類變量、切片、結構體等等特性,咱們能夠很是方便的定義與使用它們。例如,當你想定義一個結構體的類型,只須要簡單地定義:golang

type A struct {
  Name string
}

然而,當須要處理處理動態數據結構時,咱們沒法在編譯階段就知道未知數據的結構,其中一個很是經典的使用情景就是對Json串的Marshal。此時,就該reflect包出場了,它提供了在運行時建立、更新某種類型以及獲取該類型的各類信息的能力,有了它,咱們不只能有效處理動態數據類型,還能夠大大提升代碼的複用性、可讀性。shell

Type

在reflect包中,是用Type來描述Go中某個對象的類型,並提供了一系列方法,來獲取類型的相關信息,通常經過調用TypeOf來獲取一個任意變量的類型Type編程

例如,Name()返回的就是該類型的具體名稱,String()返回類型的字符串表示。api

值得注意的是Kind()方法,它返回的是該類型的類別,這彷佛有點拗口,但其實十分好理解,舉個例子,type A struct{} ,它的類型是A而類別是struct。一般,在開始階段,咱們會先判斷傳入的interface的類別,從而避免panic。由於有些方法只適用於某種類別,隨意使用的話代碼很容易panic,例如NumField()方法,只能用以獲取Kind爲結構體的字段數量。數據結構

還有一個方法Elem(),返回Type的子元素的Type。舉個例子,若Type爲指針,那麼Elem()返回指針所指向的Type,若爲切片,則Elem()返回切片元素的類型Type。例如*[]int,它的Elem()方法返回[]int的Type。而[]intElem()方法返回int的Type。函數

import (
	"fmt"
	"reflect"
)

type A []int

func printInfo(t reflect.Type) {
	fmt.Printf("Kind = %s\tName = %s\n", t.Kind(), t.Name())
}

func main() {
	a := &A{}
	printInfo(reflect.TypeOf(a))
	printInfo(reflect.TypeOf(a).Elem())
	printInfo(reflect.TypeOf(a).Elem().Elem())
}

輸出以下:優化

Kind = ptr      Name = 
Kind = slice    Name = A
Kind = int      Name = int

Value

Value描述了在Go運行時某個對象的值,咱們能夠針對它進行增刪改查之類的操做,通常經過ValueOf方法來獲取對象的Value設計

一般狀況下,咱們能夠經過Set()方法來修改變量的值。例以下述代碼指針

var a = 1
	val := reflect.ValueOf(&a)
	val.Elem().Set(reflect.ValueOf(2))
	fmt.Printf("a = %d", a)

輸出:code

a = 2

能夠看到,變量a的值由1被修改成2了。

使用舉例

動態初始化結構體

實際工做中,struct一般用來表示某種數據結構(或對象),是十分簡潔易懂的。然而,缺點也很明顯,即其表達能力頗有限,好比,你想指定某個字段的默認值,你不得不在構造函數中手動指定。這種方式雖然可行,可是不夠優雅,可讀性也不好。

type DS struct {
	FieldOne string
}

func NewDS() *DS {
	return &DS{
		FieldOne: "something",
	}
}

那麼該如何優化呢?很簡單,即利用字段的tag信息。例如,下述代碼,我在tag中設置了默認值。

type DS struct {
	FieldOne string `default:"something"`
}

而後,我使用一個初始化函數initStruct()來讀取tag並設置字段默認值。

func NewDS() *DS {
	ds := &DS{}
	initStruct(ds)
	fmt.Printf("FieldOne = %s", ds.FieldOne)
	return ds
}

func initStruct(v interface{}) error {
   e := reflect.Indirect(reflect.ValueOf(v))
   if e.Kind() != reflect.Struct {
      return errors.New("v must be struct")
   }
   et, ev := e.Type(), e
   for i := 0; i < et.NumField(); i++ {
      field, val := et.Field(i), ev.Field(i)
      defaultValue, ok := field.Tag.Lookup("default")
      if !ok {
         continue
      }
      switch field.Type.Kind() {
      case reflect.String:
         val.SetString(defaultValue)
      case reflect.Int:
         if x, err := strconv.ParseInt(defaultValue, 10, 64); err != nil {
            val.SetInt(x)
         }
      // 針對不一樣Kind,將defaultValue轉換爲對應類型並賦值
      ...
      }
   }
   return nil
}

至此,咱們就能夠既方便又優雅地給結構體設置默認值了,固然,你還能夠在tag中設置其餘動態屬性來動態更改結構體。

動態建立Map

一般狀況下,咱們是經過make來建立一個map,而有了reflect包後,咱們也能夠經過reflet包來動態地建立一個map。

這裏,咱們有個需求,須要將一個表明長方形的結構體轉換爲一個map,而且存在額外要求,例如浮點字段只保留兩位小數且轉換爲字符串。

首先,定義一個名爲Rectangle的結構體來表明一個長方形

type Rectangle struct {
	Name   string
	Unit   string
	Length float64
	Width  float64
}

而後,使用一個convert函數,將其轉換爲map。

func convert(rectangle *Rectangle) (res map[string]string, err error) {
	e := reflect.Indirect(reflect.ValueOf(rectangle))
	if e.Kind() != reflect.Struct {
		return nil, errors.New("v must be struct")
	}
	et, ev := e.Type(), e

	var mapStringType = reflect.TypeOf(make(map[string]string))
	mapReflect := reflect.MakeMap(mapStringType)
	for i := 0; i < et.NumField(); i++ {
		field, val := et.Field(i), ev.Field(i)
		switch field.Type.Kind() {
		case reflect.String:
			mapReflect.SetMapIndex(reflect.ValueOf(field.Name), reflect.ValueOf(val.String()))
		case reflect.Float64:
			s := strconv.FormatFloat(val.Float(), 'f', 2, 64)
			mapReflect.SetMapIndex(reflect.ValueOf(field.Name), reflect.ValueOf(s))
		// other cases
		...
		}
	}
	return mapReflect.Interface().(map[string]string), nil
}

最後,咱們能夠打印出轉換後的map。

func main() {
	res, _ := convert(&Rectangle{
		Name: "rec-1",
		Unit: "cm",
		Length: 12.121764,
		Width: 5.989681,
	})
	fmt.Printf("res = %+v", res)
}

輸出:

res = map[Length:12.12 Name:rec-1 Unit:cm Width:5.99]

總結

至此,對於reflect的簡單介紹已完畢,相信你已經有了一個大概的認知了。是否是以爲這個包很強大,想躍躍欲試呢?可是,在此以前,仍是要提醒你要銘記如下注意點。

  1. reflect大多隻能適用於動態數據類型的場景,且較爲危險,所以能使用原生類型儘可能使用原生類型。
  2. 書寫要當心,錯誤使用reflect很容易panic,你須要確保你的類型使用了正確的相關方法,並提早返回錯誤。
  3. 編程界沒有銀彈,所以reflect也不是萬能,例如你沒法動態建立結構體的方法。

本人才疏學淺,文章不免有些不足之處,很是歡迎你們評論指出。

參考

相關文章
相關標籤/搜索