在之前的两篇文章 《深入理解 go reflect - 反射基本原理》、《深入理解 go reflect - 要不要传指针》 中,
我们讲解了关于 go 反射的一些基本原理,以及通过反射对象修改变量的一些注意事项。
本篇文章将介绍一些常见的反射用法,涵盖了常见的数据类型的反射操作。
使用反射很常见的一个场景就是根据类型做不同处理,比如下面这个方法,根据不同的 Kind
返回不同的字符串表示:
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))
}
在标准库 encoding/json
中,也有类似的场景,比如下面这个方法,根据不同的 Kind
做不同的处理:
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
// ...省略其他 case...
default:
return unsupportedTypeEncoder
}
}
在进行 json
编码的时候,因为不知道传入的参数是什么类型,所以需要根据类型做不同的处理,这里就是使用反射来做的。
通过判断不同的类型,然后返回不同的 encoder
。
这里说的基本类型是:int*
、uint*
、float*
、complex*
、bool
这种类型。
通过反射修改基本类型的值,需要注意的是,传入的参数必须是指针类型,否则会 panic
:
func TestBaseKind(t *testing.T) {
// 通过反射修改 int 类型变量的值
a := 1
v := reflect.ValueOf(&a)
v.Elem().SetInt(10)
assert.Equal(t, 10, a)
// 通过反射修改 uint16 类型变量的值
b := uint16(10)
v1 := reflect.ValueOf(&b)
v1.Elem().SetUint(20)
assert.Equal(t, uint16(20), b)
// 通过反射修改 float32 类型变量的值
f := float32(10.0)
v2 := reflect.ValueOf(&f)
v2.Elem().SetFloat(20.0)
assert.Equal(t, float32(20.0), f)
}
通过反射修改值的时候,需要通过
Elem()
方法的返回值来修改。
通过反射修改数组中元素的值,可以使用 Index
方法取得对应下标的元素,然后再使用 Set
方法修改值:
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
中接收数据:
func TestChan(t *testing.T) {
// 通过反射修改 chan
ch := make(chan int, 1)
v := reflect.ValueOf(&ch)
// 通过反射对象向 chan 发送数据
v.Elem().Send(reflect.ValueOf(2))
// 在反射对象外部从 chan 接收数据
assert.Equal(t, 2, <-ch)
}
通过反射修改 map
中的值,可以使用 SetMapIndex
方法修改 map
中对应的 key
:
func TestMap(t *testing.T) {
// 通过反射修改 map 元素的值
m := map[string]int{"a": 1}
v := reflect.ValueOf(&m)
// 修改 a 的 key,修改其值为 2
v.Elem().SetMapIndex(reflect.ValueOf("a"), reflect.ValueOf(2))
// 外部的 m 可以看到反射对象的修改
assert.Equal(t, 2, m["a"])
}
我们可以通过反射对象的 MapRange
方法来迭代 map
对象:
func TestIterateMap(t *testing.T) {
// 遍历 map
m := map[string]int{"a": 1, "b": 2}
v := reflect.ValueOf(m)
// 创建 map 迭代器
iter := v.MapRange()
// 迭代 map 的元素
for iter.Next() {
// a 1
// b 2
fmt.Println(iter.Key(), iter.Value())
}
}
通过反射修改 slice
中的值,可以使用 Index
方法取得对应下标的元素,然后再使用 Set*
方法修改值,跟数组类似:
func TestSlice(t *testing.T) {
// 通过反射修改 slice 元素的值
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
方法来修改其内容:
func TestString(t *testing.T) {
// 通过反射修改字符串的值
s := "hello"
v := reflect.ValueOf(&s)
v.Elem().SetString("world")
assert.Equal(t, "world", s)
}
对于 interface
或 Pointer
类型,我们可以通过其反射对象的 Elem
方法来修改其内容:
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 中的结构体,反射系统中为我们提供了很多操作结构体的方法,比如获取结构体的字段、方法、标签、通过反射对象调用其方法等。
先假设我们有如下结构体:
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
方法来获取结构体的字段:
func TestStruct1(t *testing.T) {
var p = Person{Name: "Tom", Age: 18, sex: 1}
v := reflect.ValueOf(p)
// string Tom
// int 18
// uint8 1
for i := 0; i < v.NumField(); i++ {
fmt.Println(v.Field(i).Type(), v.Field(i))
}
}
我们可以根据结构体字段的名称或索引来获取结构体的字段:
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*
方法来修改其值:
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
方法来修改其值,依此类推。
通过反射对象来调用结构体的方法时,需要注意的是,如果我们需要调用指针接收者的方法,则需要传递地址:
func TestStruct3(t *testing.T) {
var p = Person{Name: "Tom", Age: 18, sex: 1}
// 值接收者(receiver)
v1 := reflect.ValueOf(p)
assert.Equal(t, 1, v1.NumMethod())
// 注意:值接收者没有 M2 方法
assert.False(t, v1.MethodByName("M2").IsValid())
// 通过值接收者调用 M1 方法
results := v1.MethodByName("M1").Call(nil)
assert.Len(t, results, 1)
assert.Equal(t, "person m1", results[0].Interface())
// 指针接收者(pointer receiver)
v2 := reflect.ValueOf(&p)
assert.Equal(t, 2, v2.NumMethod())
// 通过指针接收者调用 M1 和 M2 方法
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
类型的切片。对于这个,其实有一个更简单的方法,那就是利用接口断言:
func TestStrunct4_0(t *testing.T) {
type TestInterface interface {
M1() string
}
var p = Person{Name: "Tom", Age: 18, sex: 1}
v := reflect.ValueOf(p)
// v.Interface() 返回的是 interface{} 类型
// v.Interface().(TestInterface) 将 interface{} 类型转换为 TestInterface 类型
v1, ok := v.Interface().(TestInterface)
assert.True(t, ok)
assert.Equal(t, "person m1", v1.M1())
}
另外一个方法是,通过反射对象的 Type
方法获取类型对象,然后调用 Implements
方法来判断是否实现了某个接口:
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))
}
这在序列化、反序列化、ORM 库中用得非常多,常见的 validator
库也是通过 tag 来实现的。
下面的例子中,通过获取变量的 Type
就可以获取其 tag
了:
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"))
}
我们知道,结构体的字段如果首字母小写,则是未导出的,不能被外部包访问。但是我们可以通过反射修改它:
func TestStruct6(t *testing.T) {
var p = Person{Name: "Tom", Age: 18, sex: 1}
v := reflect.ValueOf(&p)
// 下面这样写会报错:
// panic: reflect: reflect.Value.SetInt using value obtained using unexported field
// v.Elem().FieldByName("sex").SetInt(0)
ft := v.Elem().FieldByName("sex")
sexV := reflect.NewAt(ft.Type(), unsafe.Pointer(ft.UnsafeAddr())).Elem()
assert.Equal(t, 1, p.sex) // 修改前是 1
sexV.Set(reflect.ValueOf(uint8(0))) // 将 sex 字段修改为 0
assert.Equal(t, 0, p.sex) // 修改后是 0
}
这里通过 NewAt
方法针对 sex
这个未导出的字段创建了一个指针,然后我们就可以通过这个指针来修改 sex
字段了。
这里说的方法包括函数和结构体的方法。
reflect
包中提供了 In
和 Out
方法来获取方法的入参和返回值:
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")
// 参数个数为 2
assert.Equal(t, 2, m.Type().NumIn())
// 返回值个数为 1
assert.Equal(t, 1, m.Type().NumOut())
// In(0) 是第一个参数,In(1) 是第二个参数
arg1 := m.Type().In(0)
assert.Equal(t, "int", arg1.Name())
arg2 := m.Type().In(1)
assert.Equal(t, "string", arg2.Name())
// Out(0) 是第一个返回值
ret0 := m.Type().Out(0)
assert.Equal(t, "int", ret0.Name())
}
说明:
In
和 Out
方法返回的是 reflect.Type
类型,可以通过 Name
方法获取类型名称。NumIn
和 NumOut
方法返回的是参数和返回值的个数。reflect.Value
类型的 MethodByName
方法可以获取结构体的方法。reflect.Value
中对于方法类型的反射对象,有一个 Call
方法,可以通过它来调用方法:
func TestMethod2(t *testing.T) {
var p = Person{Name: "Tom", Age: 18, sex: 1}
v := reflect.ValueOf(p)
// 通过反射调用 Test 方法
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
来创建一个指向结构体未导出字段的反射对象,这样就可以修改结构体的未导出字段了。Call
来调用函数和方法等。