关于 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 // ...省略其他 case... 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) { // 通过反射修改 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
方法修改值:
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) { // 通过反射修改 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
:
1 2 3 4 5 6 7 8 9 | 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
对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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*
方法修改值,跟数组类似:
1 2 3 4 5 6 7 | 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
方法来修改其内容:
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) // string Tom // int 18 // uint8 1 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 } // 值接收者(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
类型的切片。
是否实现接口
对于这个,其实有一个更简单的方法,那就是利用接口断言:
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) // 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
方法来判断是否实现了某个接口:
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) // 下面这样写会报错: // 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
方法来获取方法的入参和返回值:
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" ) // 参数个数为 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
方法,可以通过它来调用方法:
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) // 通过反射调用 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
来调用函数和方法等。
到此这篇关于Golang中反射的常见用法分享的文章就介绍到这了,希望可以帮到你
转自:微点阅读 https://www.weidianyuedu.com