目錄
前言
書上對go反射的講解太淺顯,網上的博客也太零散,想着自己整理一篇博客吧,爲解決當前的一個問題,也爲以後複習參考。
思考題
在讀這篇博客之前可以先想象這樣一個情景:自己設計一個類似 json.Marshal(v interface{})的函數,按順序遞歸打印出入參的每一個字段的名稱和值。
這也是我當前遇到的需求的一個環節。
反射的三大法則及go中對應的實現
- 能根據接口數據獲得反射對象。對應go中reflect.TypeOf(i interface{})和reflect.ValueOf(i interface{})兩個方法。
- 能根據反射對象獲得接口數據。對應go中Value.Interface()、Value.[基本數據類型]()等方法。
- 如果數據可修改,則能通過反射對象修改。對應go中Value.CanSet()和Value的Set系列方法。
go反射概述
- go不支持解析string然後執行。golang的反射機制只能存在於已經存在的對象上面。
- go的反射主要由Type和Value組成。可分別由reflect.TypeOf(i interface{})和reflect.ValueOf(i interface{})獲得。Type是一個接口,有若干函數,reflect.TypeOf(i interface{})返回的是實現了這個接口的具體實現。Value是一個結構體,有若干字段和函數。
- Type也可由Value.Type()獲得,Value不可由Type獲得。
- Type存儲接口的類型信息,例如方法數量、方法名、字段數量、字段名等。當使用Type時應當只讀取信息,不能試圖通過Type進行修改、調用之類的操作。
- Value存儲接口的數據信息,並且包含對應的Type,可以通過Value.Type()獲得對應的Type。
Type詳解
通過go中Type的定義,可以看出Type及其具體實現有哪些方法可以用。下面給幾個常用方法做了註釋
type Type interface {
Name() string //返回類型名
Kind() Kind //返回該類型的種類,Array、Slice、Struct等
Elem() Type //當type當kind是Array, Chan, Map, Ptr, 或 Slice使用,返回類型的元素類型。
Method(int) Method // 按下標返回Method,超出範圍Panic
MethodByName(string) (Method, bool) // 按方法名稱返回Method和表示方法是否存在的bool
NumMethod() int // 返回方法數量
NumIn() int //當kind是Func時,返回入參數量
In(i int) Type //當Kind是Func時,返回第i個入參類型
NumOut() int //當kind是Func時,返回出參數量
Out(i int) Type //當kind是Func時,返回第i個出參類型
NumField() int //當Kind是Struct時,返回字段數量,內嵌結構體算一個。
Field(i int) StructField //當Kind是Struct時,通過下標返回字段
FieldByIndex(index []int) StructField //當Kind是Struct時,返回內嵌結構體字段
FieldByName(name string) (StructField, bool) //當Kind是Struct時,通過名稱返回字段
FieldByNameFunc(match func(string) bool) (StructField, bool) //當Kind是Struct時,按自定義字段名匹配函數返回字段
Key() Type //當Kind時map時,返回鍵的類型,值的類型可由Elem()返回
Len() int //當類型是Array時,返回長度
PkgPath() string //返回定義類型的包的路徑
Size() uintptr //返回存儲一個該類型的值所需要的字節數
Align() int
FieldAlign() int
String() string
Implements(u Type) bool
AssignableTo(u Type) bool
ConvertibleTo(u Type) bool
Comparable() bool
Bits() int
ChanDir() ChanDir
IsVariadic() bool
common() *rtype
uncommon() *uncommonType
}
Method
Type中Method(int) Method、MethodByName(string) (Method, bool)、NumMethod() int三個方法與Method有關,Method是一個結構體,存儲了方法名、方法在Type方法集中的下標等信息。
type Method struct {
Name string //方法名
PkgPath string
Type Type
Func Value
Index int // 在Type方法集中的下標
}
示例代碼:
func (user User) GetId() int {
log.Println("[User] GetId begin")
log.Printf("[User] age:%d", user.age)
log.Println("[User] GetId end")
return user.age
}
func main() {
user := User{
Name: "張三",
age: 18,
}
userType := reflect.TypeOf(user)
log.Printf("userType.NumMethod():%d", userType.NumMethod()) //1
method, exist := userType.MethodByName("GetId")
if exist {
log.Printf("method.Name:%+v", method.Name) //GetId
log.Printf("method.PkgPath:%+v", method.PkgPath) //
log.Printf("method.Type:%+v", method.Type) //func(main.User) int
log.Printf("method.Func:%+v", method.Func) //0x10bd9f0
log.Printf("method.Index:%+v", method.Index) //0
}
for i := 0; i < userType.NumMethod(); i++ {
method := userType.Method(i)
log.Printf("method.Name:%+v", method.Name) //GetId
log.Printf("method.PkgPath:%+v", method.PkgPath) //
log.Printf("method.Type:%+v", method.Type) //func(main.User) int
log.Printf("method.Func:%+v", method.Func) //0x10bd9f0
log.Printf("method.Index:%+v", method.Index) //i
}
}
Kind
Type的Kind()方法返回該類型的種類,可根據不同的種類進行不同的操作。reflect中枚舉的種類有以下幾種:
const (
Invalid Kind = iota
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr
Float32
Float64
Complex64
Complex128
Array
Chan
Func
Interface
Map
Ptr
Slice
String
Struct
UnsafePointer
)
StructField
先通過一個例子說明一下FieldByIndex(index []int) StructField函數
type Point struct {
X int
Y int
Z int
}
type User struct {
Name string
Age int
Point
}
func main() {
user := User{
Name: "張三",
Age: 18,
}
userType := reflect.TypeOf(user)
index := []int{2, 1} //代表下標爲2的字段是一個內嵌結構體,取出這個內嵌結構體中下標爲1的字段
field := userType.FieldByIndex(index)
log.Printf("userType.FieldByIndex():%+v", field)
}
StructField記錄結構體的字段信息,包括字段名稱、字段標籤、字段是否爲內嵌結構體、字段在結構體中的偏移量等。
type StructField struct {
Name string //名稱
PkgPath string
Type Type //類型
Tag StructTag //標籤
Offset uintptr //如果是內嵌結構體裏的字段,則從內嵌結構體裏面重新計算偏移量
Index []int //如果是內嵌結構體裏的字段,則從內嵌結構體裏面重新計數下標
Anonymous bool //是否是內嵌結構體
}
Value詳解
先看一下Value的定義:
type Value struct {
typ *rtype
ptr unsafe.Pointer
flag
}
可以看到Value只有三個非導出字段,這表面我們對Value的一切操作都要通過調用函數。值得注意的是typ *rtype字段,這是Value對應的Type實例。我們可以通過Value.Type()獲得這個實例。
Value有61個可用函數。下面分類看看這些函數
比較通用的函數
func (v Value) CanAddr() bool //判斷v是否可尋址。如果v是切片元素、可尋址數組元素、可尋址結構體字段、或ptr執行Elem()後返回的結果,那麼v是可尋址的。
func (v Value) CanSet() bool //判斷v是否可修改。可當v尋址且是導出字段時可修改。
func (v Value) Elem() Value //當v的kind是Interface 或 Ptr,返回v指向的實例。
func (v Value) Addr() Value //當v可尋址是返回v當指針Value,和Elem()互爲逆操作。
func (v Value) Kind() Kind //返回v的Kind。
func (v Value) IsNil() bool //當v的kind是Chan, Func, Map, Ptr, UnsafePointer時,判斷v是否是nil
func (v Value) IsValid() bool //判斷v是否有效
func (v Value) IsZero() bool //判斷v是否是它所屬類型的零值
func (v Value) Pointer() uintptr //當v的 Kind is not Chan, Func, Map, Ptr, Slice, or UnsafePointer時使用。
func (v Value) UnsafeAddr() uintptr //返回指向v的數據的指針
函數
func (v Value) NumMethod() int //返回v的函數數量。
func (v Value) Method(i int) Value //按下標返回函數。
func (v Value) MethodByName(name string) Value //按名字返回函數。
func (v Value) Call(in []Value) []Value //當v的Kind是Func時,執行函數v。in是入參,返回是出參。
func (v Value) CallSlice(in []Value) []Value //當vKind是Func且是變參函數時,執行函數。in是入參,返回是出參。
數組、切片、字符串
func (v Value) Cap() int //當v的Kind是Array、Slice、Chan時返回v的容量。
func (v Value) Len() int //當v的Kind是Array、Slice、String、Chan、Map時返回v的長度。
func (v Value) Index(i int) Value //當v的Kind是Array、Slice、String時按下標返回元素。
func (v Value) Slice(i, j int) Value //當v的Kind是Array、Slice、String時,返回v[i:j]
func (v Value) Slice3(i, j, k int) Value //當v的Kind是Array、Slice時,返回v[i:j:k]
通道
func (v Value) Len() int
func (v Value) Cap() int
func (v Value) Recv() (x Value, ok bool)
func (v Value) Send(x Value)
func (v Value) TryRecv() (x Value, ok bool)
func (v Value) TrySend(x Value) bool
func (v Value) Close() //當v的kind是Chan時,關閉v。
映射
func (v Value) Len() int
func (v Value) MapKeys() []Value //當v的Kind是Map時,返回鍵數組
func (v Value) MapIndex(key Value) Value //當v的Kind是Map時,輸入鍵返回值。
func (v Value) MapRange() *MapIter //當v的Kind是Map時,返回MapIter實例。
MapIter是遍歷Map的迭代器,它的字段都是非導出字段,我們只需要關心MapIter的方法:
func (it *MapIter) Key() Value
func (it *MapIter) Value() Value
func (it *MapIter) Next() bool
結構體
func (v Value) Field(i int) Value //當v的kind是Struct時,按下標返回字段
func (v Value) FieldByIndex(index []int) Value //當v的kind是Struct時,返回內嵌字段
func (v Value) FieldByName(name string) Value //當v的kind是Struct時,按名字返回字段
func (v Value) FieldByNameFunc(match func(string) bool) Value //當v的kind是Struct時,按自定義字段名匹配規則返回字段。
從反射類型得到接口數據
從反射類型轉化接口數據的方法,對應反射的第二法則。結構體可先轉化爲接口再進行斷言。基本數據類型和[]byte、[]rune有一步到位的方法。
func (v Value) Interface() (i interface{})
func (v Value) Bool() bool
func (v Value) Int() int64
func (v Value) Uint() uint64
func (v Value) String()
func (v Value) Float() float64
func (v Value) Complex() complex128
func (v Value) Bytes() []byte
func (v Value) runes() []rune
Setter函數
func (v Value) Set(x Value)
func (v Value) SetBool(x bool)
func (v Value) SetBytes(x []byte)
func (v Value) setRunes(x []rune)
func (v Value) SetComplex(x complex128)
func (v Value) SetFloat(x float64)
func (v Value) SetInt(x int64)
func (v Value) SetLen(n int)
func (v Value) SetCap(n int)
func (v Value) SetMapIndex(key, elem Value)
func (v Value) SetUint(x uint64)
func (v Value) SetPointer(x unsafe.Pointer)
func (v Value) SetString(x string)
Overflow一類的函數
func (v Value) OverflowComplex(x complex128) bool
func (v Value) OverflowFloat(x float64) bool
func (v Value) OverflowInt(x int64) bool
func (v Value) OverflowUint(x uint64) bool
其他函數
// CanInterface reports whether Interface can be used without panicking.
func (v Value) CanInterface() bool
// InterfaceData returns the interface v's value as a uintptr pair.
// It panics if v's Kind is not Interface.
func (v Value) InterfaceData() [2]uintptr
// Convert returns the value v converted to type t.
// If the usual Go conversion rules do not allow conversion
// of the value v to type t, Convert panics.
func (v Value) Convert(t Type) Value
go反射最佳實踐
從reflect.ValueOf開始
從上文中可以看出,Type能做的事Value基本都能做,Value不能做的事可以通過Value.Type()獲得對應的Type再做,所以一般反射可以從reflect.ValueOf開始。如果只需要讀取結構信息,可以從reflect.TypeOf開始。
如果想修改數據
在go語言中可以被修改的數據必須滿足以下兩個條件:
- 可以被尋址
- 被導出
第一個條件很好理解,畢竟要修改數據的值,就得找到數據所在的地址。那什麼樣的變量才叫可尋址呢?分兩步走。
第一步:reflect.ValueOf(i interface{})函數要傳入指針類型變量。
第二步:對reflect.ValueOf獲得的對象執行Elem()函數。Elem()函數返回的Value就是可尋址的。示例代碼:
func main() {
i := 1
v1 := reflect.ValueOf(&i)
v2 := v1.Elem()
//可用Value.CanAddr()判斷是不是可以被尋址。
fmt.Println(v1.CanAddr()) // false
fmt.Println(v2.CanAddr()) // true
}
所以如果想通過反射修改數據,就向reflect.ValueOf傳入一個指針。
如果只想通過反射讀取一些信息,爲了避免誤改,就向reflect.ValueOf傳入一份拷貝。當傳入非指針實例時go會自動拷貝數據。
Elem()的替代函數
func (v Value) Elem() Value函數不是任何Value都能調用的,如果v的 Kind 不是 Interface 或 Ptr就會導致Panic。我們遇到的大多數需要執行Elem()的時候都是v的 Kind是Ptr的時候。爲了避免每次都手動判斷v的Kind是不是Ptr,可以藉助reflect包裏的func Indirect(v Value) Value函數。這個函數會自行判斷v的是不是Kind是不是Ptr,如果是則返回v.Elem(),否則返回v本身。
最初的思考
現在再看文章剛開始的思考題,似乎已經沒那麼難了。下面貼出來代碼,有興趣的可以看看。
package main
import (
"log"
"reflect"
)
type People struct {
Name string
Age int
}
type User struct {
Id int64
People
friends []*People
ext map[string]interface{}
}
func main() {
user := User{
Id: 1,
People: People{
Name: "張三",
Age: 18,
},
friends: []*People{
{
Name: "李四",
Age: 18,
},
{
Name: "朱五",
Age: 18,
},
},
ext: map[string]interface{}{
"teacher": People{
Name: "楊六",
Age: 36,
},
},
}
Handle(user)
}
func Handle(i interface{}) {
val := reflect.ValueOf(i)
route(val)
}
func route(val reflect.Value) {
if !val.IsValid() {
return
}
switch val.Kind() {
case reflect.Struct:
handleStruct(val)
case reflect.Slice:
handleSlice(val)
case reflect.Map:
handleMap(val)
case reflect.Ptr, reflect.Interface:
route(val.Elem())
default:
log.Printf("處理能力之外的Kind,Kind:%+v", val.Kind())
}
}
func handleStruct(obj reflect.Value) {
objType := obj.Type()
for i := 0; i < obj.NumField(); i++ {
field := obj.Field(i)
switch field.Kind() {
case reflect.Array, reflect.Slice, reflect.Chan, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Struct:
route(field)
default:
log.Printf("%s:%v", objType.Field(i).Name, field)
}
}
}
func handleSlice(list reflect.Value) {
for i := 0; i < list.Len(); i++ {
element := list.Index(i)
route(element)
}
}
func handleMap(m reflect.Value) {
for iter := m.MapRange(); iter.Next(); {
route(iter.Value())
}
}
參考文章
go源碼
https://www.jb51.net/article/180540.htm
http://c.biancheng.net/view/116.html