golang结构体标签与反射

本文对golang反射做了一些尝试,整理了一些关键理解。

结构体标签(structure tag)

用过golang json的同学应该对下面的结构体定义很熟悉:

type Request struct {
	Id int	`json:"id"`
	Name string	`json:"name"`
	inner string
	Age *int 	`json:"age"`
	Xi interface{}
}

`json:”xxx”`就是一段结构体标签,它本身并没有什么魔法,很像一段代码注释。

但是和注释的区别在于,通过反射机制是可以获取到标签的,从而可以实现一些神奇的事情。

反射与标签

我们平时用json marshal来序列化一个结构体,那么背后大概是什么原理呢?

我来实现一段演示用的代码,主要展示反射与标签的关系,并不是真的json序列化,这个函数叫做MyJsonEncode:

func main() {
	var (
		req *Request
	)

	req = &Request{
		Id: 1,
		Name: "owen",
	}

	MyJsonEncode(req)
}

我定义了一个Request对象,将指针传了进去,当然也可以传对象进去,因为我的函数定义是这样的:

func MyJsonEncode(obj interface{}) {
	var (
		i int

		objType reflect.Type
		objValue reflect.Value

		field reflect.StructField
		fieldValue reflect.Value

		fieldName string
	)

就像json.Marshal一样,interface{}可以容纳任意类型的变量,interface{}本质上内部维护了2个东西:

  • type:也就是变量的类型,比如是int类型,*int类型,都是不同的。

  • value:变量的值

再次理解interface{}

这里存在一个比较容易混淆的概念,大家一定要注意区分:

  • interface{}为nil:这是说interface{}没有容纳任何变量

  • interface{}装了空指针:比如把ptr *string = nil赋值给了obj interface{},这种情况下obj的type是*string,值是nil

所以,我们首先要判断interface{}为nil的情况,这种情况压根没法json编码:

	// 接口是空(没装任何东西的interface{})
	if obj == nil {
		fmt.Println("空接口")
		return
	}

反射类型与值

接下来,我们要判断一下interface{}这个万能容器中,放的到底是int、string、*int、*string还是什么其他类型的变量,因此需要用到反射机制。

	// 反射变量
	objType = reflect.TypeOf(obj) // 反射类型
	objValue = reflect.ValueOf(obj) // 反射值

通过TypeOf可以获取interface{}容纳的变量的类型,ValueOf就是取其中的变量值了。

我们使用json.Marshal的时候,一定会发现无论传入的是结构体对象还是结构体指针,都可以编码成json,这到底是为什么?

其实可以通过反射来得知变量的类型,如果是指针就取指针指向的值对象,所以总是相当于传入了一个对象:

	// 如果是指针, 需要取值
	if objType.Kind() == reflect.Ptr {
		if objValue.IsNil() {		// 空指针
		        fmt.Println("空指针")
			return
		}
		objType = objType.Elem() // 相当于类型为*ptr
		objValue = objValue.Elem() // 相当于值为*ptr
	}

这里Kind()返回一个枚举值,表示变量是什么类型,这里Ptr是指针的意思。

如果变量是指针,我们还需要进一步判断一下指针是否为空,我们之前说过interface{}为nil与interface{}装着空指针的区别了!

如果指针不空,那么通过Elem()可以取得指针的值类型与值对象,相当于*ptr,大家感受一下:

  • 原本objType是*int类型,那么objType.Elem()就是int类型

  • 原本objValue是一个*int变量,那么objValue.Elem()就是int变量

嵌套则递归

得到了值对象后,需要判断它是否为结构体,如果是结构体则需要递归为每个字段做json编码:

	// 如果不是结构体, 则不需要递归处理
	if objType.Kind() != reflect.Struct {
		fmt.Println("普通值", objValue.Interface())
		return
	}

如果是结构体则代码继续向下运行,开始编码结构体的各个字段:

	// 递归处理结构体中的字段
	for i = 0; i < objType.NumField(); i++ {
		field = objType.Field(i)	// 获取字段类型
		fieldValue = objValue.Field(i) // 获取字段的值

objType反射了结构体的定义,所以可以遍历它的每个字段(field),每个字段的类型赋值给field,值则通过objValue才能获得,我们已经说过Interface{}的类型与值!

json.Marshal只会导出首字母大写的字段,我们需要根据字段的首字母判断:

		// 小写字段不导出
		fieldName = field.Name
		if unicode.IsLower(rune(fieldName[0])) {
			continue
		}

接着,我们打印出这个结构体字段的信息:

		// 打印这个字段的信息
		fmt.Println("字段:", field.Name, "类型:", field.Type, "标签:", field.Tag)

通过对field取Tag就可以得到标签的内容:

字段: Id 类型: int 标签: json:"id"
字段: Name 类型: string 标签: json:"name"
字段: Age 类型: *int 标签: json:"age"
字段: Xi 类型: interface {} 标签:

我们应该判定一下,只有标签中包含json标识的字段,才会被导出。

这里我们知道了结构体每个字段的导出名,那么剩下的工作就是递归的编码字段value:

		// 递归编码这个字段
		MyJsonEncode(fieldValue.Interface()) 
    } // for循环结束

字段value的interface()方法可以把字段的值(无论是指针、对象)包装到一个interface{}容器中返回,因此我们可以再次进入递归,处理这个子value。

结束

上述演示代码在github观看:https://github.com/owenliang/go-structure-tag/tree/master。

一些基本的反射API和编程逻辑就是这样了~

点赞