在之前的两篇文章 《深入理解
go reflect - 反射基本原理》 、《深入理解
go reflect - 要不要传指针》 中, 我们讲解了关于 go
反射的一些基本原理,以及通过反射对象修改变量的一些注意事项。
本篇文章将介绍一些常见的反射用法,涵盖了常见的数据类型的反射操作。
根据类型做不同处理
使用反射很常见的一个场景就是根据类型做不同处理,比如下面这个方法,根据不同的
Kind 返回不同的字符串表示:
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 47 48 func getType (i interface {}) string { v := reflect.ValueOf(i) switch v.Kind() { case reflect.Bool: b := "false" if v.Bool() { b = "true" } return fmt.Sprintf("bool: %s" , b) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return fmt.Sprintf("int: %d" , v.Int()) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return fmt.Sprintf("uint: %d" , v.Uint()) case reflect.Float32, reflect.Float64: return fmt.Sprintf("float: %.1f" , v.Float()) case reflect.String: return fmt.Sprintf("string: %s" , v.String()) case reflect.Interface: return fmt.Sprintf("interface: %v" , v.Interface()) case reflect.Struct: return fmt.Sprintf("struct: %v" , v.Interface()) case reflect.Map: return fmt.Sprintf("map: %v" , v.Interface()) case reflect.Slice: return fmt.Sprintf("slice: %v" , v.Interface()) case reflect.Array: return fmt.Sprintf("array: %v" , v.Interface()) case reflect.Pointer: return fmt.Sprintf("pointer: %v" , v.Interface()) case reflect.Chan: return fmt.Sprintf("chan: %v" , v.Interface()) default : return "unknown" } } func TestKind (t *testing.T) { assert.Equal(t, "int: 1" , getType(1 )) assert.Equal(t, "string: 1" , getType("1" )) assert.Equal(t, "bool: true" , getType(true )) assert.Equal(t, "float: 1.0" , getType(1.0 )) arr := [3 ]int {1 , 2 , 3 } sli := []int {1 , 2 , 3 } assert.Equal(t, "array: [1 2 3]" , getType(arr)) assert.Equal(t, "slice: [1 2 3]" , getType(sli)) }
标准库 json 中的示例
在标准库 encoding/json
中,也有类似的场景,比如下面这个方法,根据不同的 Kind
做不同的处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func newTypeEncoder (t reflect.Type, allowAddr bool ) encoderFunc { switch t.Kind() { case reflect.Bool: return boolEncoder case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return intEncoder case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return uintEncoder default : return unsupportedTypeEncoder } }
在进行 json
编码的时候,因为不知道传入的参数是什么类型,所以需要根据类型做不同的处理,这里就是使用反射来做的。
通过判断不同的类型,然后返回不同的 encoder。
基本类型的反射
这里说的基本类型是:int*、uint*、float*、complex*、bool
这种类型。
通过反射修改基本类型的值,需要注意的是,传入的参数必须是指针类型,否则会
panic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func TestBaseKind (t *testing.T) { a := 1 v := reflect.ValueOf(&a) v.Elem().SetInt(10 ) assert.Equal(t, 10 , a) b := uint16 (10 ) v1 := reflect.ValueOf(&b) v1.Elem().SetUint(20 ) assert.Equal(t, uint16 (20 ), b) f := float32 (10.0 ) v2 := reflect.ValueOf(&f) v2.Elem().SetFloat(20.0 ) assert.Equal(t, float32 (20.0 ), f) }
通过反射修改值的时候,需要通过 Elem()
方法的返回值来修改。
数组类型的反射
通过反射修改数组中元素的值,可以使用 Index
方法取得对应下标的元素,然后再使用 Set 方法修改值:
1 2 3 4 5 6 7 8 func TestArray (t *testing.T) { arr := [3 ]int {1 , 2 , 3 } v := reflect.ValueOf(&arr) v.Elem().Index(0 ).SetInt(10 ) assert.Equal(t, [3 ]int {10 , 2 , 3 }, arr) }
chan 反射
我们可以通过反射对象来向 chan 中发送数据,也可以从
chan 中接收数据:
1 2 3 4 5 6 7 8 9 func TestChan (t *testing.T) { ch := make (chan int , 1 ) v := reflect.ValueOf(&ch) v.Elem().Send(reflect.ValueOf(2 )) assert.Equal(t, 2 , <-ch) }
map 反射
通过反射修改 map 中的值,可以使用
SetMapIndex 方法修改 map 中对应的
key:
1 2 3 4 5 6 7 8 9 func TestMap (t *testing.T) { m := map [string ]int {"a" : 1 } v := reflect.ValueOf(&m) v.Elem().SetMapIndex(reflect.ValueOf("a" ), reflect.ValueOf(2 )) assert.Equal(t, 2 , m["a" ]) }
迭代反射 map 对象
我们可以通过反射对象的 MapRange 方法来迭代
map 对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func TestIterateMap (t *testing.T) { m := map [string ]int {"a" : 1 , "b" : 2 } v := reflect.ValueOf(m) iter := v.MapRange() for iter.Next() { fmt.Println(iter.Key(), iter.Value()) } }
slice 反射
通过反射修改 slice 中的值,可以使用 Index
方法取得对应下标的元素,然后再使用 Set*
方法修改值,跟数组类似:
1 2 3 4 5 6 7 func TestSlice (t *testing.T) { sli := []int {1 , 2 , 3 } v := reflect.ValueOf(&sli) v.Elem().Index(0 ).SetInt(10 ) assert.Equal(t, []int {10 , 2 , 3 }, sli) }
string 反射
对于 string 类型,我们可以通过其反射对象的
String 方法来修改其内容:
1 2 3 4 5 6 7 func TestString (t *testing.T) { s := "hello" v := reflect.ValueOf(&s) v.Elem().SetString("world" ) assert.Equal(t, "world" , s) }
interface/Pointer 反射
对于 interface 或 Pointer
类型,我们可以通过其反射对象的 Elem 方法来修改其内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func TestPointer (t *testing.T) { a := 1 var i interface {} = &a v1 := reflect.ValueOf(i) v1.Elem().SetInt(10 ) assert.Equal(t, 10 , a) var p = &a v2 := reflect.ValueOf(p) v2.Elem().SetInt(20 ) assert.Equal(t, 20 , a) }
这两种类型,我们都需要通过 Elem
方法来先获取其实际保存的值,然后再修改其值。
结构体的反射
对于 go
中的结构体,反射系统中为我们提供了很多操作结构体的方法,比如获取结构体的字段、方法、标签、通过反射对象调用其方法等。
先假设我们有如下结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Person struct { Name string Age int sex uint8 } func (p Person) M1() string { return "person m1" } func (p *Person) M2() string { return "person m2" }
遍历结构体字段
我们可以通过 NumField
方法来获取结构体的字段数量,然后通过 Field
方法来获取结构体的字段:
1 2 3 4 5 6 7 8 9 10 11 func TestStruct1 (t *testing.T) { var p = Person{Name: "Tom" , Age: 18 , sex: 1 } v := reflect.ValueOf(p) for i := 0 ; i < v.NumField(); i++ { fmt.Println(v.Field(i).Type(), v.Field(i)) } }
根据名称或索引获取结构体字段
我们可以根据结构体字段的名称或索引来获取结构体的字段:
1 2 3 4 5 6 7 8 func TestStruct2 (t *testing.T) { var p = Person{Name: "Tom" , Age: 18 , sex: 1 } v := reflect.ValueOf(p) assert.Equal(t, 18 , v.Field(1 ).Interface()) assert.Equal(t, 18 , v.FieldByName("Age" ).Interface()) assert.Equal(t, 18 , v.FieldByIndex([]int {1 }).Interface()) }
修改结构体字段
我们可以通过 Field 方法来获取结构体的字段,然后再使用
Set* 方法来修改其值:
1 2 3 4 5 6 7 8 func TestStruct2 (t *testing.T) { var p = Person{Name: "Tom" , Age: 18 , sex: 1 } v := reflect.ValueOf(&p) v.Elem().FieldByName("Name" ).SetString("Jack" ) assert.Equal(t, "Jack" , p.Name) }
上面因为 Name 是 string 类型,所以我们使用
SetString 方法来修改其值,如果是 int
类型,我们可以使用 SetInt 方法来修改其值,依此类推。
结构体方法调用
通过反射对象来调用结构体的方法时,需要注意的是,如果我们需要调用指针接收者的方法,则需要传递地址 :
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 func TestStruct3 (t *testing.T) { var p = Person{Name: "Tom" , Age: 18 , sex: 1 } v1 := reflect.ValueOf(p) assert.Equal(t, 1 , v1.NumMethod()) assert.False(t, v1.MethodByName("M2" ).IsValid()) results := v1.MethodByName("M1" ).Call(nil ) assert.Len(t, results, 1 ) assert.Equal(t, "person m1" , results[0 ].Interface()) v2 := reflect.ValueOf(&p) assert.Equal(t, 2 , v2.NumMethod()) results = v2.MethodByName("M1" ).Call(nil ) assert.Len(t, results, 1 ) assert.Equal(t, "person m1" , results[0 ].Interface()) results = v2.MethodByName("M2" ).Call(nil ) assert.Len(t, results, 1 ) assert.Equal(t, "person m2" , results[0 ].Interface()) }
说明:
结构体参数是值的时候,reflect.ValueOf
返回的反射对象只能调用值接收者的方法,不能调用指针接收者的方法。
结构体参数是指针的时候,reflect.ValueOf
返回的反射对象可以调用值接收者和指针接收者的方法。
调用 MethodByName
方法时,如果方法不存在,则返回的反射对象的 IsValid 方法返回
false。
调用 Call 方法时,如果没有参数,传 nil
参数即可。如果方法没有返回值,则返回的结果切片为空。
调用 Call 方法的参数是 reflect.Value
类型的切片,返回值也是 reflect.Value 类型的切片。
是否实现接口
对于这个,其实有一个更简单的方法,那就是利用接口断言:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func TestStrunct4_0 (t *testing.T) { type TestInterface interface { M1() string } var p = Person{Name: "Tom" , Age: 18 , sex: 1 } v := reflect.ValueOf(p) v1, ok := v.Interface().(TestInterface) assert.True(t, ok) assert.Equal(t, "person m1" , v1.M1()) }
另外一个方法是,通过反射对象的 Type
方法获取类型对象,然后调用 Implements
方法来判断是否实现了某个接口:
1 2 3 4 5 6 7 8 9 10 11 func TestStruct4 (t *testing.T) { type TestInterface interface { M1() string } var p = Person{Name: "Tom" , Age: 18 , sex: 1 } typ := reflect.TypeOf(p) typ1 := reflect.TypeOf((*TestInterface)(nil )).Elem() assert.True(t, typ.Implements(typ1)) }
结构体的 tag
这在序列化、反序列化、ORM 库中用得非常多,常见的
validator 库也是通过 tag 来实现的。
下面的例子中,通过获取变量的 Type 就可以获取其
tag 了:
1 2 3 4 5 6 7 8 9 10 11 type Person1 struct { Name string `json:"name"` } func TestStruct5 (t *testing.T) { var p = Person1{Name: "Tom" } typ := reflect.TypeOf(p) tag := typ.Field(0 ).Tag assert.Equal(t, "name" , tag.Get("json" )) }
修改结构体未导字段
我们知道,结构体的字段如果首字母小写,则是未导出的,不能被外部包访问。但是我们可以通过反射修改它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func TestStruct6 (t *testing.T) { var p = Person{Name: "Tom" , Age: 18 , sex: 1 } v := reflect.ValueOf(&p) ft := v.Elem().FieldByName("sex" ) sexV := reflect.NewAt(ft.Type(), unsafe.Pointer(ft.UnsafeAddr())).Elem() assert.Equal(t, 1 , p.sex) sexV.Set(reflect.ValueOf(uint8 (0 ))) assert.Equal(t, 0 , p.sex) }
这里通过 NewAt 方法针对 sex
这个未导出的字段创建了一个指针,然后我们就可以通过这个指针来修改
sex 字段了。
方法的反射
这里说的方法包括函数和结构体的方法。
入参和返回值
reflect 包中提供了 In 和 Out
方法来获取方法的入参和返回值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func (p Person) Test(a int , b string ) int { return a } func TestMethod (t *testing.T) { var p = Person{Name: "Tom" , Age: 18 , sex: 1 } v := reflect.ValueOf(p) m := v.MethodByName("Test" ) assert.Equal(t, 2 , m.Type().NumIn()) assert.Equal(t, 1 , m.Type().NumOut()) arg1 := m.Type().In(0 ) assert.Equal(t, "int" , arg1.Name()) arg2 := m.Type().In(1 ) assert.Equal(t, "string" , arg2.Name()) ret0 := m.Type().Out(0 ) assert.Equal(t, "int" , ret0.Name()) }
说明:
In 和 Out 方法返回的是
reflect.Type 类型,可以通过 Name
方法获取类型名称。
NumIn 和 NumOut
方法返回的是参数和返回值的个数。
reflect.Value 类型的 MethodByName
方法可以获取结构体的方法。
通过反射调用方法
reflect.Value 中对于方法类型的反射对象,有一个
Call 方法,可以通过它来调用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func TestMethod2 (t *testing.T) { var p = Person{Name: "Tom" , Age: 18 , sex: 1 } v := reflect.ValueOf(p) m := v.MethodByName("Test" ) arg1 := reflect.ValueOf(1 ) arg2 := reflect.ValueOf("hello" ) args := []reflect.Value{arg1, arg2} rets := m.Call(args) assert.Len(t, rets, 1 ) assert.Equal(t, 1 , rets[0 ].Interface()) }
说明:
Call 方法的参数是 []reflect.Value
类型,需要将参数转换为 reflect.Value 类型。
Call 方法的返回值也是 []reflect.Value
类型。
reflect.Value 类型的 MethodByName
方法可以获取结构体的方法的反射对象。
通过方法的反射对象的 Call 方法可以实现调用方法。
总结
通过 reflect.Kind
可以判断反射对象的类型,Kind 涵盖了 go
中所有的基本类型,所以反射的时候判断 Kind 就足够了。
如果要获取反射对象的值,需要传递指针给
reflect.Value。
可以往 chan 的反射对象中发送数据,也可以从
chan 的反射对象中接收数据。
SetMapIndex 方法可以修改 map
中的元素。MapRange 方法可以获取 map
的迭代器。
可以通过 Index 方法获取 slice
的元素,也可以通过 SetIndex 方法修改 slice
的元素。
可以通过 SetString 方法修改 string
的值。
对于 interface 和 Pointer
类型的反射对象,可以通过 Elem
方法获取它们的值,同时也只有通过 Elem
获取到的反射对象能调用 Set* 方法来修改其指向的对象。
reflect
包中提供了很多操作结构体的功能:如获取结构体的字段、获取结构体的方法、调用结构体的方法等。我们使用一些类库的时候,会需要通过结构体的
tag 来设置一些元信息,这些信息只有通过反射才能获取。
我们可以通过 NewAt
来创建一个指向结构体未导出字段的反射对象,这样就可以修改结构体的未导出字段了。
对于函数和方法,go
的反射系统也提供了很多功能,如获取参数和返回值信息、使用
Call 来调用函数和方法等。