1. 程式人生 > 程式設計 >Go反射 實現任意型別屬性拷貝

Go反射 實現任意型別屬性拷貝

開發中會頻繁的使用各種物件,在Java中稱為Javabean,在Go中用結構體。使用ORM框架時,經常會用實體類來對映資料表,但實際上很少會直接使用對映資料表的實體類物件在各層傳輸,更多的會使用其他物件(如DTO,VO等),對讀出的實體類物件的屬性進行過濾或增加。

用Java的朋友都知道,有個便利的工具叫BeanUtils,呼叫一下copy()方法即可去除大量的setter操作。Go自帶很多package,但並沒有任意型別的拷貝方法,內建的copy()也只能拷貝切片。

不過Go自帶反射包,利用反射,我們可以手動實現一個任意型別屬性拷貝的函式或方法。

實現起來也很簡單,喜歡琢磨的朋友可以直接閱讀反射的檔案自己實現。

golang.org/pkg/reflect…

Overview

以下摘自檔案

Package reflect implements run-time reflection,allowing a program to manipulate objects with arbitrary types. The typical use is to take a value with static type interface{} and extract its dynamic type information by calling TypeOf,which returns a Type.

A call to ValueOf returns a Value representing the run-time data. Zero takes a Type and returns a Value representing a zero value for that type.

大致意思就是說,通過利用反射,可以在程式執行時處理任意型別。通過TypeOf方法取得取得型別資訊,包裝在Type中。通過ValueOf取得執行時的資料,包裝在Value中。

下面介紹reflect包中的一些型別及方法。已經熟悉反射包的大佬可以直接跳到最後。

Kind

定義:type Kind uint

iota定義一系列 Kind常量,表示待處理型別的型別。聽起來的很繞,其實很好理解,除了基本型別外,當我們自定義結構體時,KindStrcut,當處理的型別為指標時,KindPtr,還有其他的諸如Slice,Map,Arrray,Chan等。

ValueType的一些方法只能給特定的型別使用,比如說Type

MapOf()方法,只能是Map使用,當使用的Type不是Map時會報panic。諸如此類的方法還有很多,因為型別不匹配時會直接panic(),為了安全,在使用特定方法時應該先對Kind進行判斷。

Type

定義的Type是介面型別,獲得例項後可以通過呼叫一系列方法獲得型別相關資訊,可以通過reflect.TypeOf(i interface{})獲得例項。

通過Name()方法可以獲得型別名稱。Kind()方法可以獲得型別的Kind

如果Kind為結構體型別Struct,通過NumField()可獲得結構體的屬性個數,可以通過Field(i int) StructField,FieldByName(name string) StructField獲得具體的屬性,返回值是另外一種定義的結構體型別StructField

如果KindArray,Chan,Ptr,Slice,可用通過Elem()獲得具體的元素的Type

一些特定方法只能給特定的型別使用,使用不當會直接panic()

兩種Type是可比較的,可以使用 == 或 != 。

StructField

用來描述結構體中單個屬性,定義如下

type StructField struct {
    Name string
    PkgPath string
    Type      Type      
    Tag       StructTag 
    Offset    uintptr   
    Index     []int     
    Anonymous bool     
}
複製程式碼

其中Name為屬性名稱,PkgPath為包路徑,Type為屬性的型別資訊,Tag為標籤(常用來處理編碼解碼問題,有興趣的朋友可以看一下相關庫和原始碼)。

Value

當使用Type時,我們只能獲取到型別的相關資訊,若需要操作具體值,我們就得使用Value,通過reflect.ValueOf(i interface{})。與Type不同,Value的型別為結構體。Value也有跟Type相似的方法,如NumField()Field(i),FieldByName(name string),Elem()等。

此外Value還有一系列set方法,如果值可以設定,那麼我們可以動態改變值。

Type不同,Value的比較是不可以用 == 或 != ,必須通過相應方法來進行比較。

同樣的,一些特定方法只能給特定的型別使用,使用不當會直接panic()

實踐

上文簡單瞭解了一下反射的基礎。相信很多人都知道怎麼實現了。

大致思路:因為需要改變值,所以目標引數傳遞時必須使用結構體指標,而來源引數可以傳指標或者例項。遍歷需拷貝型別的所有屬性值,用Field(i int)獲取單一屬性,取出StuctFieldName,再用Name通過FieldByName(name string)獲取被拷貝物件的值,如果獲取成功,則呼叫Set(v Value)動態設定值。

coding

func SimpleCopyProperties(dst,src interface{}) (err error) {
	// 防止意外panic
	defer func() {
		if e := recover(); e != nil {
			err = errors.New(fmt.Sprintf("%v",e))
		}
	}()

	dstType,dstValue := reflect.TypeOf(dst),reflect.ValueOf(dst)
	srcType,srcValue := reflect.TypeOf(src),reflect.ValueOf(src)

	// dst必須結構體指標型別
	if dstType.Kind() != reflect.Ptr || dstType.Elem().Kind() != reflect.Struct {
		return errors.New("dst type should be a struct pointer")
	}

	// src必須為結構體或者結構體指標
	if srcType.Kind() == reflect.Ptr {
		srcType,srcValue = srcType.Elem(),srcValue.Elem()
	}
	if srcType.Kind() != reflect.Struct {
		return errors.New("src type should be a struct or a struct pointer")
	}

	// 取具體內容
	dstType,dstValue = dstType.Elem(),dstValue.Elem()

	// 屬性個數
	propertyNums := dstType.NumField()

	for i := 0; i < propertyNums; i++ {
		// 屬性
		property := dstType.Field(i)
		// 待填充屬性值
		propertyValue := srcValue.FieldByName(property.Name)

		// 無效,說明src沒有這個屬性 || 屬性同名但型別不同
		if !propertyValue.IsValid() || property.Type != propertyValue.Type() {
			continue
		}

		if dstValue.Field(i).CanSet() {
			dstValue.Field(i).Set(propertyValue)
		}
	}

	return nil
}
複製程式碼

小結

至此,我們已經完成了同名屬性拷貝。因為使用reflect包時,到處都有panic,所以在最前面需要用延遲函式recover一下panic。引數傳遞時,第二個引數使用指標還是例項請自行斟酌。需要注意的是,該拷貝方法為淺拷貝,換句話說,如果說物件內巢狀有其他的引用型別如Slice,Map等,用此方法完成拷貝後,源物件中的引用型別屬性內容發生了改變,該物件對應的屬性中內容也會改變。

反射包中還有很多有意思的東西,感興趣的朋友可以參考檔案。
golang.org/pkg/reflect…