zoukankan      html  css  js  c++  java
  • 【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方面的信息:

    1. 对象的具体类型信息;
    2. 对象的值信息;
    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.Intreflect.Int8reflect.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结构体。

    需要注意的有两方面:

    1. 字段的名字、Tag等属于类型信息,而不是值信息,需要通过.Type().Field(i).Name来获取;
    2. 如果字段名并非大写字母开头,则说明该字段未导出,此时对该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)来进行自定义查找。

    值得注意的是伪继承的字段,即未提供名字的字段,其有两个特点:

    1. 该字段的类型名即为该字段的名字;
    2. .FieldByName(name)能够直接找到该字段内部的字段,而通过简单的组合的方式是无法找到的;
    3. 遍历的时候并不会直接遍历到其子字段;

    读取未导出字段

    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

  • 相关阅读:
    Makefile的常用技术总结
    NPOI 插入行[转]
    LINQ语句中的.AsEnumerable() 和 .AsQueryable()的区别 [转]
    Using Google Public DNS[Google公共DNS服务器]
    软件开发知识[TDD]
    MySQL函数之STRCMP()
    MySQL知识[INSERT语法]
    软件开发知识[ORM]
    软件开发知识[ADO.NET Entity Framework]
    mysql workbench 在模板与数据库间同步
  • 原文地址:https://www.cnblogs.com/SnowPhoenix/p/15689027.html
Copyright © 2011-2022 走看看