大家好,我是考100分的小小码 ,祝大家学习进步,加薪顺利呀。今天说一说go 反射的常见用法[亲测有效],希望您对编程的造诣更进一步.
在之前的两篇文章 《深入理解 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))
}
标准库 json 中的示例
在标准库 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
中发送数据,也可以从 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 反射
通过反射修改 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"])
}
迭代反射 map 对象
我们可以通过反射对象的 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 反射
通过反射修改 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
类型,我们可以通过其反射对象的 String
方法来修改其内容:
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
方法来修改其内容:
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))
}
结构体的 tag
这在序列化、反序列化、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
来创建一个指向结构体未导出字段的反射对象,这样就可以修改结构体的未导出字段了。 - 对于函数和方法,go 的反射系统也提供了很多功能,如获取参数和返回值信息、使用
Call
来调用函数和方法等。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/13606.html