初识golang的反射

定义类型、声明变量、使用变量是一门编程语言的基本功能,咱们能够这样来定义一个结构体类型:golang

type Foo struct {
    X string `foo:"x"`
    Y int    `foo:"y"`
}
复制代码

像这样来使用这个类型声明一个变量:编程

var bar Foo
复制代码

使用变量也很方便,像这样就能够在终端打印出结构体的字段了:json

fmt.Printf("%s, %s", bar.X, bar.Y)
复制代码

以上这些操做都是在明确了变量的类型的时候进行的,不过在编程过程当中,咱们可能会遇到一种状况:在编写代码时没法明确变量的类型,变量的信息只有在程序运行时在会获取,好比这样的函数:bash

func theFunc(val interface{}) 复制代码

它的函数签名只有一个参数类型为 interface 的参数 val,这意味着能够将任意类型的变量做为实参传入函数。这样的状况下该如何操做变量 val 呢?golang 标准库中有一个 reflect包--即反射--它提供了一系列的方法能帮助咱们在运行时获取变量的信息,或者修改变量的值。在反射中有三个比较重要的概念:Type、Kind 和 Value。下面就一块儿来看看反射的奇妙之处吧。编程语言

若是咱们把 bar 变量传入了 theFunc 函数,在函数中咱们须要知道些什么信息呢?可能会须要知道传入的变量是什么类型的,若是是一个结构体,可能还会须要知道结构体中有那些字段,这些字段又是什么类型的。咱们可能还须要根据结构体字段特定的 tag 来执行特定的操做,好比 encoding/json 包就会根据 tag 来给序列化的 json 字段取名。若是传入的变量是咱们所指望的,咱们可能还须要修改它的值,或者用它来建立一个新的变量。咱们就一个个来谈谈如何用反射来实现这些功能把。函数

获取变量类型

TypeOf()

reflect 提供了 TypeOf 函数来获取指定变量的类型,它的返回值类型为 reflect.Type,这是一个接口类型,它提供了一系列的方法来获取变量相关信息的方法。ui

Name()、Kind() 和 Elem()

Name() 方法获取变量的类型名称,不过它有一个限制,即只能获取基本类型或者自定义的结构体的类型名称,其余的类型会返回一个空的字符串。spa

Kind() 方法会返回变量的内置类型名称,好比 ptr、slice、array、map、func、struct 等等。它一般能够和 switch 配合来作类型判断。指针

Elem() 方法用于判断类型的元素类型(type's element type)。它是对 Name 方法的补充,它能够返回 array, chan, map, ptr, 或 slice 类型中元素的类型。好比,针对一个指针 &bar, Elem 方法会返回 Foo 这个类型名称;针对 []string 这样一个字符串 slice,Elem 会返回 string 这个类型名称。若是不在容许的类型上调用 Elem 方法,会致使 panic ,其实 reflect 包中不少方法和函数都是这样的,它要求使用者知道本身在作什么。code

下面来举一个完整的示例吧:

import (
	"fmt"
	"reflect"
)

type Foo struct {
	X string  `foo:"x"`
	Y string `foo:"y"`
}

func main() {
	bar := Foo{
		X: "hello",
		Y: "world",
	}
	sli := make([]string, 0)
	ch := make(chan bool)
	m := make(map[int]int)
	arr := [10]int{}
	i := 0
	f := 1.1
	b := true

	theFunc(bar)
	theFunc(&bar)
	theFunc(sli)
	theFunc(ch)
	theFunc(m)
	theFunc(arr)
	theFunc(theFunc)
	theFunc(i)
	theFunc(f)
	theFunc(b)
}

func theFunc(val interface{}) {
	valType := reflect.TypeOf(val)
	fmt.Printf("name of value : %s, kind of value : %s, ", valType.Name(), valType.Kind())

	switch valType.Kind() {
	case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Slice, reflect.Map:
		fmt.Printf("elem of value : %s", valType.Elem())
	}
	fmt.Println()
}
复制代码

输出为:

获取变量值

ValueOf()

ValueOf() 函数获取变量中实际存储的值。例如:

bar := Foo {
    X: "hello",
    Y: "world",
}

fmt.Println(reflect.ValueOf(bar))
复制代码

输出的结果为:

{"hello", "world"}
复制代码

类型断言

若是使用者知道传入的值是什么类型的,或者经过类型判断的方式获取了值的类型信息,那么可使用 val.(type) 这种类型断言的方式将传入的 interface 类型的数据强制转换成咱们所须要的类型,这时候就能够正常使用类型的字段了。须要注意的是,若是类型断言出错了,那么将会引起 panic,因此,在使用的时候能够先确认变量的类型再进行类型断言,或者利用类型断言的第二个布尔类型的返回值来判断断言是否成功。

func main() {
	bar := Foo {
	    X: "hello",
	    Y: "world",
	}
    
    theFunc(bar)
}

func theFunc(val interface{}) {
    // v1 := val.(int) // 引起 panic
    //在使用断言前先判断类型是否正确
    if reflect.TypeOf(val) == reflect.TypeOf(Foo{}) {
        v2 := val.(Foo)
        fmt.Println(v2)
    }
    //使用 ok 来判断断言是否成功
    if v2, ok := val.(Foo); ok {
        fmt.Println(v2)
    }
}
复制代码

输出为

{"hello" "world"}
{"hello" "world"}
复制代码

遍历类型的方法

NumMethod()、Method(int)、 MethodByName(string) 这几个方法能够获取变量所对应的类型已导出的方法:

type Foo struct {
	X string  `foo:"x"`
	Y string `foo:"y"`
}

func (Foo) unExported() {
	fmt.Println("unExported")
}

func (Foo) Exported()  {
	fmt.Println("Exported")
}

func main() {
	bar := Foo{
		X: "hello",
		Y: "world",
	}

	valTheFun(bar)
}

func valTheFun(val interface{}) {
	valType := reflect.TypeOf(val)
	fmt.Println(valType.NumMethod())
	for i := 0; i < valType.NumMethod(); i++ {
		fmt.Println(valType.Method(i).Name)
	}
}
复制代码

输出结果:

1
Exported
复制代码

遍历函数的入参和出参

对于函数类型,Type 接口也提供了相应的方法来遍历入参和出参:NumIn(),In(i int), NumOut,Out(i int)。这几个方法只能用与类型为函数的变量,不然将会引起 panic 。

func main() {
    valTheFun(valTheFun)
	valTheFun(param)
}

func param(i int, s string) (int, string) {
	return i, s
}

func valTheFun(val interface{}) {
	valType := reflect.TypeOf(val)
	
    fmt.Printf("number of in args %d:\n", valType.NumIn())
	for i := 0; i < valType.NumIn(); i++ {
		fmt.Printf("\t %s\n", valType.In(i))
	}
	fmt.Printf("number of out args %d:\n", valType.NumOut())
	for i := 0; i < valType.NumOut(); i++ {
		fmt.Printf("\t %s\n", valType.Out(i))
	}
}
复制代码

输出:

number of in args 1:
	 interface {}
number of out args 0:
number of in args 2:
	 int
	 string
number of out args 2:
	 int
	 string
复制代码

遍历结构体的字段

遍历结构体的字段是十分经常使用的,当咱们须要统一处理 kind 为结构体的入参,但又不知道结构体的具体类型,也不须要知道结构体的具体类型的时候,就能够用上遍历结构体字段的一系列方法了。结构体中还有一个 tag 属性,用它能够给结构体中的字段添加额外的属性,golang 也提供了方法用于遍历 tag 的数据。我的以为这一部分的功能仍是须要好好研究一下的,熟悉这部分的操做能够写出更好的代码,golang 标准库中比较经常使用的场景有: encoding/json 将结构体序列化为 json 字符串;encoding/xml 将结构体序列化为 xml 数据等等。下面给一个简单的例子:

func main() {
	bar := Foo{
		X: "hello",
		Y: "world",
	}

	structFunc(bar)
}

func structFunc(val interface{}) {
	valType := reflect.TypeOf(val)
	fmt.Println("number of fields in val :", valType.NumField())
	for i := 0; i < valType.NumField(); i++ {
		fmt.Printf("field : %s ", valType.Field(i).Name)
		fmt.Println("tags", valType.Field(i).Tag)
	}
}
复制代码

输出:

number of fields in val : 2
field : X tags foo:"x"
field : Y tags foo:"y"
复制代码

建立新的变量

reflect 中有两种函数能够建立新的变量,分别是 New(Type) 和 Make* 。用两种是由于 Make* 是一系列的函数,它们和内建函数 make 同样,只能为 slice, map, chan 来建立新的变量,不一样的是 reflect 为这几个类型都分别声明了一个 Make 函数,而且还为 func 类型也声明了一个 Make 函数。而 New 和内建的 new 函数同样,返回的是建立的变量的指针,这个很重要。由于给新建的变量设置值的时候,须要使用 Field() 方法来指定结构体中的字段,而这个方法的接收器必须为 struct,因此,新建的变量必需要先调用 Elem() 方法来获取对应的结构体类型,而后再调用 Field() 方法来设置新的值。

func main() {
	bar := Foo{
		X: "hello",
		Y: "world",
	}

	valType := reflect.TypeOf(bar)
	valFields := reflect.ValueOf(bar)

	val := reflect.New(valType)
	//由于 val 是一个指针,因此须要使用 Elem 来获取元素的实际类型
	val.Elem().Field(0).SetString(valFields.Field(0).String())
	val.Elem().Field(1).SetString("golang")

	//val 是一个 reflect.Value 类型的变量,
    //须要经过 Interface() 来获取它所维护的数据,
    //而后再经过类型断言强制转换为指定的类型
	if v, ok := val.Interface().(*Foo); !ok {
		panic("wrong type")
	} else {
		fmt.Println(*v)
	}
}
复制代码

输出:

{hello golang}
复制代码

小结

到这里,基本是把 reflect 的基本操做都讲了一遍了,原本是想简单写写的,结果越写越多。可能有些方面没有将的十分清楚,请各位看官多多斧正。

相关文章
相关标签/搜索