【Go反射】读取对象
前言
最近在写一个自动配置的库cfgm,其中序列化和反序列化的过程用到了大量反射,主要部分写完之后,我在这里回顾总结一下反射的基本操作。
今天就先总结一下读取操作,即对简单类型(int、uint、float、bool、string)、指针、切片、数组、map、结构体的读取操作。
先声明一下后续会用到的打印的函数和需要引入的包:
import (
"fmt"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func Print(t *testing.T, obj interface{}) {
t.Logf("%T(%#v)", obj, obj)
}
参考
目录
基础知识
Value结构体
Value结构体是Go反射中最重要的结构体。它的本质是空接口interface{}
的拆箱,它包含三个字段:
- 指向描述类型的结构体的指针;
- 指向对象本身的指针;
- 包含Kind以及各种标志位的flag;
我们通常通过reflect.ValueOf()
方法来获得Value结构体,这个方法的实现很简单:
func ValueOf(i interface{}) Value {
if i == nil {
return Value{}
}
escapes(i)
return unpackEface(i)
}
先判空,然后将其进行逃逸,最后对这个空接口interface{}进行拆箱得到Value结构体。
到这里,我们就大致明白Go的反射的工作原理了。当我们将一个对象赋值给一个interface时,编译器会负责将该对象的运行时信息绑定到interface中,然后我们通过reflect.ValueOf()
对其进行拆箱,从而进行各类操作。
Value结构体能够给我们带来3方面的信息:
- 对象的具体类型信息;
- 对象的值信息;
- 反射对象的其它信息;
Kind 和 Type
我们进行反射的时候,每一个对象都有两个类型:Kind和Type,分别通过Value结构体的Kind()
方法和Type()
方法获取。简单来说,Kind表示的是一个对象的原始类型,Type表示的是一个对象的具体类型。
func TestNamedType(t *testing.T) {
type MyInteger int // 定义新类型,具体类型改变,原始类型依旧为int
type AliaInteger = int // 定义别名,具体类型和原始类型都是int
// Kind
assert.Equal(t, reflect.ValueOf(MyInteger(1)).Kind(), reflect.ValueOf(int(1)).Kind())
assert.Equal(t, reflect.ValueOf(AliaInteger(1)).Kind(), reflect.ValueOf(int(1)).Kind())
// Type
assert.NotEqual(t, reflect.ValueOf(MyInteger(1)).Type(), reflect.ValueOf(int(1)).Type())
assert.Equal(t, reflect.ValueOf(AliaInteger(1)).Type(), reflect.ValueOf(int(1)).Type())
}
reflect/type.go中定义了全部的Kind:
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
)
Array以前的加上String,我称之为“简单类型”,因为这些类型的对象只需要保存自己的值,只具有自己的类型属性。而不需要考虑子对象的值和类型。
反射读取简单对象
为什么要用反射读取简单对象?
按照我下面的示例中的方式来读取简单对象简直就是拖了裤子放屁,实际应用肯定不会是这样的。多数情况是,我们在读取一个复杂对象(尤其是struct),我们要对其每一个子对象进行处理,而这个时候我们并不知道子对象的原始对象,只能拿到描述子对象的Value结构体,这个时候就需要读取简单对象的方法了。
读取方法
这里有两种方法可以反射读取简单对象的值:
func TestReadInt(t *testing.T) {
var integer int = 12
value := reflect.ValueOf(integer)
Print(t, value.Int())
Print(t, value.Interface().(int))
}
/*
=== RUN TestReadInt
readonly_test.go:12: int64(12)
readonly_test.go:12: int(12)
--- PASS: TestReadInt (0.00s)
*/
.Int()
:对于Kind为reflect.Int
、reflect.Int8
、reflect.Int16
等对象,都可以通过Int()
方法获取值,注意获取的值永远是int64
类型的;.Interface()
:其实这个方法的本质是将拆箱后的Value结构体在重新装箱为interface{},因此还需要进行空接口的断言才能获得其值,不过这里进行接口断言就必须要使用具体类型,而不像.Int()
那样只需要知道Kind就可以获取值;
从这个例子就可以看出.Interface()
+断言的劣势:
func TestReadNamedInt(t *testing.T) {
type MyInteger int
var integer MyInteger = 12
value := reflect.ValueOf(integer)
Print(t, value.Int())
i, ok := value.Interface().(int)
if ok {
Print(t, i)
} else {
t.Log("value.Interface() cannot cast to int")
}
}
/*
=== RUN TestReadNamedInt
readonly_test.go:12: int64(12)
readonly_test.go:60: value.Interface() cannot cast to int
--- PASS: TestReadNamedInt (0.00s)
*/
总的来说还是推荐通过.Int()
方法获取值,类似的方法还有:
Kind | 方法 |
---|---|
Int、Intxx | Int() int64 |
Uint、Uintxx | Uint() uint64 |
String | String() string |
Float32、Float64 | Float() float64 |
Bool | Bool() bool |
当然,如果你获取值的目的仅仅是传给另一个接受interface{}的函数(fmt.Println()等),那么直接.Interface()
即可。
反射读取指针
解引用
func TestReadPtr(t *testing.T) {
var integer int = 12
for _, ptr := range []*int{&integer, nil} {
t.Run(fmt.Sprintf("%#v", ptr), func(t *testing.T) {
value := reflect.ValueOf(ptr)
elem := value.Elem()
if elem.IsValid() {
Print(t, elem.Interface())
} else {
t.Log("nil")
}
})
}
}
/*
=== RUN TestReadPtr
=== RUN TestReadPtr/(*int)(0xc00009f1f0)
readonly_test.go:12: int(12)
=== RUN TestReadPtr/(*int)(nil)
readonly_test.go:73: nil
--- PASS: TestReadPtr (0.00s)
--- PASS: TestReadPtr/(*int)(0xc00009f1f0) (0.00s)
--- PASS: TestReadPtr/(*int)(nil) (0.00s)
*/
.Elem()
方法获取指针指向的对象的Value结构体。注意当指针为nil时,Elem()
获取的Value结构体是invalid
的,即默认初始化的空Value结构体,可以通过.IsValid()
或者Kind() != Invalid
来进行判断。
获取指针对象的类型
func TestReadPtr_Kind(t *testing.T) {
var ptr *int = nil
value := reflect.ValueOf(ptr)
// 指针的value的kind
assert.Equal(t, reflect.Ptr, value.Kind())
// 指针指向对象的type
assert.Equal(t, reflect.TypeOf(int(0)), value.Type().Elem())
assert.Equal(t, reflect.Int, value.Type().Elem().Kind())
// Not Good
assert.NotEqual(t, reflect.Int, value.Elem().Kind())
assert.Equal(t, reflect.Invalid, value.Elem().Kind())
// BOOM! panic: reflect: call of reflect.Value.Type on zero Value
// assert.NotEqual(t, reflect.TypeOf(int(0)), value.Elem().Type())
}
这里展示了两种方法,显然后一种方法(标了NotGood)无法应付指针为nil的情况。
反射读取切片和数组
切片和数组在写操作时有区别,在读操作基本没有区别。
遍历
func TestReadSlice_Traversal(t *testing.T) {
slice := []int{1, 2, 3}
sliceValue := reflect.ValueOf(slice)
length := sliceValue.Len()
for i := 0; i < length; i++ {
elem := sliceValue.Index(i)
Print(t, elem.Interface())
}
}
/*
=== RUN TestReadSlice_Traversal
readonly_test.go:12: int(1)
readonly_test.go:12: int(2)
readonly_test.go:12: int(3)
--- PASS: TestReadSlice_Traversal (0.00s)
*/
func TestReadArray_Traversal(t *testing.T) {
array := [...]int{1, 2, 3}
arrayValue := reflect.ValueOf(array)
length := arrayValue.Len()
for i := 0; i < length; i++ {
elem := arrayValue.Index(i)
Print(t, elem.Interface())
}
}
/*
func TestReadArray_Traversal(t *testing.T) {
array := [...]int{1, 2, 3}
arrayValue := reflect.ValueOf(array)
length := arrayValue.Len()
for i := 0; i < length; i++ {
elem := arrayValue.Index(i)
Print(t, elem.Interface())
}
}
*/
通过.Len()
来获取长度,然后通过.Index(i)
来获取指定下标的元素的Value结构体,然后进行接下来的操作。
获取元素的类型
func TestReadSlice_Type(t *testing.T) {
slice := []int{1, 2, 3}
sliceValue := reflect.ValueOf(slice)
assert.Equal(t, reflect.TypeOf(int(1)), sliceValue.Type().Elem())
}
数组同理。
虽然可以取出第一个元素然后获取其类型达到类似的效果,但是当切片长度为0时,这种方法就不奏效了。所以还是推荐通过.Type().Elem()
来获取内部元素的类型。
反射读取map
遍历
func TestReadMap_Traversal(t *testing.T) {
dict := map[string]int{"A": 1, "B": 2}
dictValue := reflect.ValueOf(dict)
keys := dictValue.MapKeys()
for _, key := range keys {
value := dictValue.MapIndex(key)
Print(t, key.Interface())
Print(t, value.Interface())
}
}
/*
=== RUN TestReadMap_Traversal
readonly_test.go:12: string("A")
readonly_test.go:12: int(1)
readonly_test.go:12: string("B")
readonly_test.go:12: int(2)
--- PASS: TestReadMap_Traversal (0.00s)
*/
通过.MapKeys()
获取一个描述所有key的[]Value
切片,然后通过.MapIndex(key)
来获取value的Value结构体。当然,还有另一种可能更高效的方式:
func TestReadMap_Traversal2(t *testing.T) {
dict := map[string]int{"A": 1, "B": 2}
dictValue := reflect.ValueOf(dict)
iter := dictValue.MapRange()
for iter.Next() {
key := iter.Key()
value := iter.Value()
Print(t, key.Interface())
Print(t, value.Interface())
}
}
/*
=== RUN TestReadMap_Traversal2
readonly_test.go:13: string("A")
readonly_test.go:13: int(1)
readonly_test.go:13: string("B")
readonly_test.go:13: int(2)
--- PASS: TestReadMap_Traversal2 (0.00s)
*/
这种方式利用迭代器进行遍历,因为省去了每次通过key的Value结构体查询value的Value结构体的过程,所以理论上应该更快(但是应该是常数级优化)。
查找
func TestReadMap_Find(t *testing.T) {
dict := map[string]int{"A": 1, "B": 2}
dictValue := reflect.ValueOf(dict)
for _, key := range []string{"A", "C"} {
t.Run(fmt.Sprintf("find key=%s", key), func(t *testing.T) {
value := dictValue.MapIndex(reflect.ValueOf(key))
if value.IsValid() {
Print(t, value.Interface())
} else {
t.Log("not found")
}
})
}
}
/*
=== RUN TestReadMap_Find
=== RUN TestReadMap_Find/find_key=A
readonly_test.go:12: int(1)
=== RUN TestReadMap_Find/find_key=C
readonly_test.go:120: not found
--- PASS: TestReadMap_Find (0.00s)
--- PASS: TestReadMap_Find/find_key=A (0.00s)
--- PASS: TestReadMap_Find/find_key=C (0.00s)
*/
直接通过reflect.ValueOf(key)
将string类型的key转换为其对应的Value结构体,然后通过.MapIndex()
来查找value。这里需要注意,如果没有在map中找到这个key,返回的value就会是invalid
的,类似于前文介绍过的空指针.Elem()
。
获取key/value的类型
func TestReadMap_Type(t *testing.T) {
dict := map[string]int{"A": 1, "B": 1}
dictValue := reflect.ValueOf(dict)
// key 的类型
assert.Equal(t, reflect.TypeOf(string("")), dictValue.Type().Key())
// value 的类型
assert.Equal(t, reflect.TypeOf(int(1)), dictValue.Type().Elem())
}
和切片/数组类似,通过.Type().Key()
以及.Type().Elem()
来分别获取key、value的类型,而不要通过取出其中的元素然后通过元素类型来判断,因为后者无法应付map长度为0的情况。
反射读取结构体
遍历
func TestReadStruct_Traversal(t *testing.T) {
type SubStruct struct {
Name string
}
type MyStruct struct {
SubStruct
Age int
nickName string
}
var myStruct = MyStruct{SubStruct: SubStruct{Name: "name"}, Age: 12, nickName: "nick"}
structValue := reflect.ValueOf(myStruct)
fieldCount := structValue.NumField()
for i := 0; i < fieldCount; i++ {
fieldName := structValue.Type().Field(i).Name
fieldValue := structValue.Field(i)
if 'A' <= fieldName[0] && fieldName[0] <= 'Z' {
fieldInterface := fieldValue.Interface()
t.Logf("field %s is %T(%#v)", fieldName, fieldInterface, fieldInterface)
} else {
t.Logf("field %s not exported", fieldName)
switch fieldValue.Kind() {
case reflect.String:
// BOOM! panic: reflect.Value.Interface: cannot return value obtained from unexported field or method
// fieldValue.Interface()
Print(t, fieldValue.String())
}
}
}
}
/*
=== RUN TestReadStruct_Traversal
readonly_test.go:177: field SubStruct is experiment.SubStruct(experiment.SubStruct{Name:"name"})
readonly_test.go:177: field Age is int(12)
readonly_test.go:179: field nickName not exported
readonly_test.go:12: string("nick")
--- PASS: TestReadStruct_Traversal (0.00s)
*/
通过.NumField()
获取结构体的字段总数,然后通过.Field(i)
获取该字段的Value结构体。
需要注意的有两方面:
- 字段的名字、Tag等属于类型信息,而不是值信息,需要通过
.Type().Field(i).Name
来获取; - 如果字段名并非大写字母开头,则说明该字段未导出,此时对该Value调用
.Interface()
会导致panic,不过可以通过.String()
等方法来获取值;
查询字段
func TestReadStruct_Find(t *testing.T) {
type SubStruct struct {
Name string
}
type MyStruct struct {
SubStruct
Age int
NickName SubStruct
}
var myStruct = MyStruct{SubStruct: SubStruct{Name: "name"}, Age: 12, NickName: SubStruct{Name: "nick"}}
structValue := reflect.ValueOf(myStruct)
for _, name := range []string{"Name", "SubStruct", "NickName"} {
t.Run(fmt.Sprintf("find %s", name), func(t *testing.T) {
fieldValue := structValue.FieldByName(name)
fieldStruct, _ := structValue.Type().FieldByName(name)
fieldName := fieldStruct.Name
fieldInterface := fieldValue.Interface()
t.Logf("field %s is %T(%#v)", fieldName, fieldInterface, fieldInterface)
})
}
}
/*
=== RUN TestReadStruct_Find
=== RUN TestReadStruct_Find/find_Name
readonly_test.go:208: field Name is string("name")
=== RUN TestReadStruct_Find/find_SubStruct
readonly_test.go:208: field SubStruct is experiment.SubStruct(experiment.SubStruct{Name:"name"})
=== RUN TestReadStruct_Find/find_NickName
readonly_test.go:208: field NickName is experiment.SubStruct(experiment.SubStruct{Name:"nick"})
--- PASS: TestReadStruct_Find (0.00s)
--- PASS: TestReadStruct_Find/find_Name (0.00s)
--- PASS: TestReadStruct_Find/find_SubStruct (0.00s)
--- PASS: TestReadStruct_Find/find_NickName (0.00s)
*/
这里通过.FieldByName(name)
的方式来找到符合名字的字段,其实还可以通过.FieldByNameFunc(func)
来进行自定义查找。
值得注意的是伪继承的字段,即未提供名字的字段,其有两个特点:
- 该字段的类型名即为该字段的名字;
.FieldByName(name)
能够直接找到该字段内部的字段,而通过简单的组合的方式是无法找到的;- 遍历的时候并不会直接遍历到其子字段;
读取未导出字段
func TestReadUnexportedField(t *testing.T) {
type SubStruct struct {
a int
b int
c int
}
type MyStruct struct {
sub SubStruct
}
myStruct := MyStruct{SubStruct{1,2,3}}
structValue := reflect.ValueOf(&myStruct).Elem()
fieldCount := structValue.NumField()
for i := 0; i < fieldCount; i++ {
fieldInfo := structValue.Type().Field(i)
ptr := structValue.UnsafeAddr() + fieldInfo.Offset
t.Logf("%s is %#v", fieldInfo.Name, *(*SubStruct)(unsafe.Pointer(ptr)))
}
}
利用.UnsafeAddr()
和.Type().Field(i).Offset
来手动计算该字段的地址,然后进行读取。
总结
本文介绍了利用反射进行读取的操作,即对简单类型(int、uint、float、bool、string)和复杂类型(指针、切片、数组、map、结构体)的读取操作,对于简单类型,我们只需要知道如何获取值,对于复杂类型,我们需要知道如何获取子对象的值,以及子对象的类型,对于有多个子对象的情况,我们需要知道如何进行查询,以及如何进行遍历。
转载请注明原文地址:https://www.cnblogs.com/SnowPhoenix/p/15689027.html