Go反射 實現任意型別屬性拷貝
開發中會頻繁的使用各種物件,在Java中稱為Javabean,在Go中用結構體。使用ORM框架時,經常會用實體類來對映資料表,但實際上很少會直接使用對映資料表的實體類物件在各層傳輸,更多的會使用其他物件(如DTO,VO等),對讀出的實體類物件的屬性進行過濾或增加。
用Java的朋友都知道,有個便利的工具叫BeanUtils,呼叫一下copy()
方法即可去除大量的setter操作。Go自帶很多package,但並沒有任意型別的拷貝方法,內建的copy()
也只能拷貝切片。
不過Go自帶反射包,利用反射,我們可以手動實現一個任意型別屬性拷貝的函式或方法。
實現起來也很簡單,喜歡琢磨的朋友可以直接閱讀反射的檔案自己實現。
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
常量,表示待處理型別的型別。聽起來的很繞,其實很好理解,除了基本型別外,當我們自定義結構體時,Kind
為Strcut
,當處理的型別為指標時,Kind
為Ptr
,還有其他的諸如Slice
,Map
,Arrray
,Chan
等。
在Value
和Type
的一些方法只能給特定的型別使用,比如說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
。
如果Kind
為Array
,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)
獲取單一屬性,取出StuctField
的Name
,再用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…