【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

posted @ 2021-12-14 17:21  SnowPhoenix  阅读(394)  评论(0编辑  收藏  举报