欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Golang reflect使用指南

程序员文章站 2022-03-20 15:39:27
Go提供了各种变量、切片、结构体等等特性,我们可以非常方便的定义与使用它们。例如,当你想定义一个结构体的类型,只需要简单地定义: 然而,当需要处理处理动态数据结构时,我们无法在编译阶段就知道未知数据的结构,其中一个非常经典的使用情景就是对Json串的Marshal。此时,就该 包出场了,它提供了在运 ......

go提供了各种变量、切片、结构体等等特性,我们可以非常方便的定义与使用它们。例如,当你想定义一个结构体的类型,只需要简单地定义:

type a struct {
  name string
}

然而,当需要处理处理动态数据结构时,我们无法在编译阶段就知道未知数据的结构,其中一个非常经典的使用情景就是对json串的marshal。此时,就该reflect包出场了,它提供了在运行时创建、更新某种类型以及获取该类型的各种信息的能力,有了它,我们不仅能有效处理动态数据类型,还可以大大提高代码的复用性、可读性。

type

在reflect包中,是用type来描述go中某个对象的类型,并提供了一系列方法,来获取类型的相关信息,一般通过调用typeof来获取一个任意变量的类型type

例如,name()返回的就是该类型的具体名称,string()返回类型的字符串表示。

值得注意的是kind()方法,它返回的是该类型的类别,这似乎有点拗口,但其实十分好理解,举个例子,type a struct{} ,它的类型是a而类别是struct。通常,在开始阶段,我们会先判断传入的interface的类别,从而避免panic。因为有些方法只适用于某种类别,随意使用的话代码很容易panic,例如numfield()方法,只能用以获取kind为结构体的字段数量。

还有一个方法elem(),返回type的子元素的type。举个例子,若type为指针,那么elem()返回指针所指向的type,若为切片,则elem()返回切片元素的类型type。例如*[]int,它的elem()方法返回[]int的type。而[]intelem()方法返回int的type。

import (
	"fmt"
	"reflect"
)

type a []int

func printinfo(t reflect.type) {
	fmt.printf("kind = %s\tname = %s\n", t.kind(), t.name())
}

func main() {
	a := &a{}
	printinfo(reflect.typeof(a))
	printinfo(reflect.typeof(a).elem())
	printinfo(reflect.typeof(a).elem().elem())
}

输出如下:

kind = ptr      name = 
kind = slice    name = a
kind = int      name = int

value

value描述了在go运行时某个对象的值,我们可以针对它进行增删改查之类的操作,一般通过valueof方法来获取对象的value

通常情况下,我们可以通过set()方法来修改变量的值。例如下述代码

	var a = 1
	val := reflect.valueof(&a)
	val.elem().set(reflect.valueof(2))
	fmt.printf("a = %d", a)

输出:

a = 2

可以看到,变量a的值由1被修改为2了。

使用举例

动态初始化结构体

实际工作中,struct通常用来表示某种数据结构(或对象),是十分简洁易懂的。然而,缺点也很明显,即其表达能力很有限,比如,你想指定某个字段的默认值,你不得不在构造函数中手动指定。这种方式虽然可行,但是不够优雅,可读性也很差。

type ds struct {
	fieldone string
}

func newds() *ds {
	return &ds{
		fieldone: "something",
	}
}

那么该如何优化呢?很简单,即利用字段的tag信息。例如,下述代码,我在tag中设置了默认值。

type ds struct {
	fieldone string `default:"something"`
}

然后,我使用一个初始化函数initstruct()来读取tag并设置字段默认值。

func newds() *ds {
	ds := &ds{}
	initstruct(ds)
	fmt.printf("fieldone = %s", ds.fieldone)
	return ds
}

func initstruct(v interface{}) error {
   e := reflect.indirect(reflect.valueof(v))
   if e.kind() != reflect.struct {
      return errors.new("v must be struct")
   }
   et, ev := e.type(), e
   for i := 0; i < et.numfield(); i++ {
      field, val := et.field(i), ev.field(i)
      defaultvalue, ok := field.tag.lookup("default")
      if !ok {
         continue
      }
      switch field.type.kind() {
      case reflect.string:
         val.setstring(defaultvalue)
      case reflect.int:
         if x, err := strconv.parseint(defaultvalue, 10, 64); err != nil {
            val.setint(x)
         }
      // 针对不同kind,将defaultvalue转换为对应类型并赋值
      ...
      }
   }
   return nil
}

至此,我们就可以既方便又优雅地给结构体设置默认值了,当然,你还可以在tag中设置其他动态属性来动态更改结构体。

动态创建map

通常情况下,我们是通过make来创建一个map,而有了reflect包后,我们也可以通过reflet包来动态地创建一个map。

这里,我们有个需求,需要将一个代表长方形的结构体转换为一个map,并且存在额外要求,例如浮点字段只保留两位小数且转换为字符串。

首先,定义一个名为rectangle的结构体来代表一个长方形

type rectangle struct {
	name   string
	unit   string
	length float64
	width  float64
}

然后,使用一个convert函数,将其转换为map。

func convert(rectangle *rectangle) (res map[string]string, err error) {
	e := reflect.indirect(reflect.valueof(rectangle))
	if e.kind() != reflect.struct {
		return nil, errors.new("v must be struct")
	}
	et, ev := e.type(), e

	var mapstringtype = reflect.typeof(make(map[string]string))
	mapreflect := reflect.makemap(mapstringtype)
	for i := 0; i < et.numfield(); i++ {
		field, val := et.field(i), ev.field(i)
		switch field.type.kind() {
		case reflect.string:
			mapreflect.setmapindex(reflect.valueof(field.name), reflect.valueof(val.string()))
		case reflect.float64:
			s := strconv.formatfloat(val.float(), 'f', 2, 64)
			mapreflect.setmapindex(reflect.valueof(field.name), reflect.valueof(s))
		// other cases
		...
		}
	}
	return mapreflect.interface().(map[string]string), nil
}

最后,我们可以打印出转换后的map。

func main() {
	res, _ := convert(&rectangle{
		name: "rec-1",
		unit: "cm",
		length: 12.121764,
		width: 5.989681,
	})
	fmt.printf("res = %+v", res)
}

输出:

res = map[length:12.12 name:rec-1 unit:cm width:5.99]

总结

至此,对于reflect的简单介绍已完毕,相信你已经有了一个大概的认知了。是不是觉得这个包很强大,想跃跃欲试呢?但是,在此之前,还是要提醒你要铭记以下注意点。

  1. reflect大多只能适用于动态数据类型的场景,且较为危险,因此能使用原生类型尽量使用原生类型。
  2. 书写要小心,错误使用reflect很容易panic,你需要确保你的类型使用了正确的相关方法,并提前返回错误。
  3. 编程界没有银弹,因此reflect也不是万能,例如你无法动态创建结构体的方法。

本人才疏学浅,文章难免有些不足之处,非常欢迎大家评论指出。

参考