Go struct拷貝
在用Go做orm相關操作的時候,經常會有struct之間的拷貝。比如下面兩個struct之間要拷貝共同成員B,C。這個在struct不是很大的時候從來都不是問題,直接成員拷貝即可。但是當struct的大小達到三四十個成員的時候,就要另闢蹊徑了。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
type A struct { A int B int C string E string } type B struct { B int C string D int E string } |
做法一. 反射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func CopyStruct(src, dst interface{}) { sval := reflect.ValueOf(src).Elem() dval := reflect.ValueOf(dst).Elem() for i := 0; i < sval.NumField(); i++ { value := sval.Field(i) name := sval.Type().Field(i).Name dvalue := dval.FieldByName(name) if dvalue.IsValid() == false { continue } dvalue.Set(value) } } |
做法二. json
先encode成json,再decode,其實golang的json包內部實現也是使用的反射,所以再大型專案中可以考慮使用ffjson來作為替代方案。
1 2 3 4 5 6 7 8 9 10 |
func main() { a := &A{1, "a", 1} // b := &B{"b",2,2} aj, _ := json.Marshal(a) b := new(B) _ = json.Unmarshal(aj, b) fmt.Printf("%+v", b) } |
關於golang中的反射機制一直是大家詬病挺多的。因為反射中使用了型別的列舉,所以效率比較低,在高效能場景中應該儘量規避,但是,對於大部分應用場景來說,犧牲一點效能來極大減少程式碼行數,或者說提高開發效率都是值得的。
Reflect
關於Reflect的介面可以參考golang的文件,也可以直接看go的原始碼。reflect的核心是兩個,一個是Type,一個是Value。reflect的使用一般都是以下面語句開始。
Value
Value的定義很簡單,如下。
1 2 3 4 5 |
type Value struct { typ *rtype ptr unsafe.Pointer //pointer-valued data or pointer to data flag //metedata } |
下面針對object的型別不同來看一下reflect提供的方法。
built-in type
reflect針對基本型別提供瞭如下方法,以Float()為例展開。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
//讀 func (v Value) Float() float64 { k := v.kind() switch k { case Float32: return float64(*(*float32)(v.ptr)) case Float64: return *(*float64)(v.ptr) } panic(&ValueError{"reflect.Value.Float", v.kind()}) } func (v Value) Bool() bool func (v Value) Bytes() []byte func (v Value) Int() int64 func (v Value) Uint() uint64 func (v Value) String() string func (v Value) Complex() complex128 //寫 func (v Value) Set(x Value) func (v Value) SetBool(x bool) func (v Value) SetBytes(x []byte) func (v Value) SetCap(n int) func (v Value) SetComplex(x complex128) func (v Value) SetFloat(x float64) func (v Value) SetInt(x int64) { v.mustBeAssignable() switch k := v.kind(); k { default: panic(&ValueError{"reflect.Value.SetInt", v.kind()}) case Int: *(*int)(v.ptr) = int(x) case Int8: *(*int8)(v.ptr) = int8(x) case Int16: *(*int16)(v.ptr) = int16(x) case Int32: *(*int32)(v.ptr) = int32(x) case Int64: *(*int64)(v.ptr) = x } } func (v Value) SetLen(n int) func (v Value) SetMapIndex(key, val Value) func (v Value) SetPointer(x unsafe.Pointer) func (v Value) SetString(x string) func (v Value) SetUint(x uint64) |
可以看到內部Float內部實現先通過kind()判斷value的型別,然後通過指標取資料並做型別轉換。kind()的定義如下
1 2 3 |
func (f flag) kind() Kind { return Kind(f & flagKindMask) } |
flag是Value的內部成員,所以方法也被繼承過來。通過實現不難猜到reflect對不同的型別是通過一個整數來實現的。我們來驗證一下,在type.go檔案中找到Kind定義,注意這個地方Kind()只是一個型別轉換。
1 2 3 4 5 6 7 8 9 10 11 |
type Kind uint const ( Invalid Kind = iota Bool Int Int8 Int16 Int32 ... ) |
緊挨著Kind定義的下面就是各種型別的表示:Bool=1, Int=2,…再來看一下f & flagKindMask
的意思。再value.go
檔案中找到flagKindMask
的定義:
1 2 3 4 5 |
const ( flagKindWidth = 5 // there are 27 kinds flagKindMask flag = 1<<flagKindWidth - 1 ... } |
所以這句f & flagKindMask
的意思就是最f的低5位,也就是對應了上面的各種型別。
上面說完了資料的讀介面,其實寫介面也很類似。唯一的區別在於reflect還提供了一個Set()方法,就是說我們自己去保證資料的型別正確性。
Struct
其實使用反射的很多場景都是struct。reflect針對struct提供的函式方法如下:
1 2 3 4 5 |
func (v Value) Elem() Value //返回指標或者interface包含的值 func (v Value) Field(i int) Value //返回struct的第i個field func (v Value) FieldByIndex(index []int) Value //返回巢狀struct的成員 func (v Value) FieldByName(name string) Value //通過成員名稱返回對應的成員 func (v Value) FieldByNameFunc(match func(string) bool) Value //只返回滿足函式match的第一個field |
通過上面的方法不出意外就可以取得對應是struct field了。
其他型別:Array, Slice, String
對於其他型別,reflect也提供了獲得其內部成員的方法。
1 2 3 4 5 6 |
func (v Value) Len() int //Array, Chan, Map, Slice, or String func (v Value) Index(i int) Value //Array, Slice, String func (v Value) Cap() int //Array, Chan, Slice func (v Value) Close() //Chan func (v Value) MapIndex(key Value) Value //Map func (v Value) MapKeys() []Value //Map |
函式呼叫
reflect當然也可以實現函式呼叫,下面是一個簡單的例子。
1 2 3 4 5 6 7 8 9 10 |
func main() { var f = func() { fmt.Println("hello world") } fun := reflect.ValueOf(f) fun.Call(nil) } //Output hello world |
當然我們還可以通過struct來呼叫其方法,需要注意的一點是通過反射呼叫的函式必須是外部可見的(首字母大寫)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
type S struct { A int } func (s S) Method1() { fmt.Println("method 1") } func (s S) Method2() { fmt.Println("method 2") } func main() { var a S S := reflect.ValueOf(a) fmt.Println(S.NumMethod()) m1 :=S.Method(0) m1.Call(nil) m2 := S.MethodByName("Method2") m2.Call(nil) } |
總結一下reflect提供了函式呼叫的相關介面如下:
1 2 3 4 5 |
func (v Value) Call(in []Value) []Value func (v Value) CallSlice(in []Value) []Value func (v Value) Method(i int) Value //v's ith function func (v Value) NumMethod() int func (v Value) MethodByName(name string) Value |
Type
Type是反射中另外重要的一部分。Type是一個介面,裡面包含很多方法,通常我們可以通過reflect.TypeOf(obj)
取得obj的型別對應的Type介面。介面中很多方法都是對應特定型別的,所以呼叫的時候需要注意。
1 2 3 4 5 6 7 |
type Type interface { Align() int FieldAlign() int Method(int) Method MethodByName(string) (Method, bool) NumMethod() int ... |
另外reflect包還為我們提供了幾個生成特定型別的Type介面的方法。這裡就不一一列舉了。
小結
關於reflect提供的介面需要注意的一點就是,一定要保證型別是匹配的,如果不匹配將導致panic。關於Value的主要介面都在這,本來還想寫一下Type以及內部的實現機制的,只能放到下篇再寫了。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式