定义类型、声明变量、使用变量是一门编程语言的基本功能,咱们能够这样来定义一个结构体类型: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 字段取名。若是传入的变量是咱们所指望的,咱们可能还须要修改它的值,或者用它来建立一个新的变量。咱们就一个个来谈谈如何用反射来实现这些功能把。函数
reflect 提供了 TypeOf 函数来获取指定变量的类型,它的返回值类型为 reflect.Type,这是一个接口类型,它提供了一系列的方法来获取变量相关信息的方法。ui
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() 函数获取变量中实际存储的值。例如:
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 的基本操做都讲了一遍了,原本是想简单写写的,结果越写越多。可能有些方面没有将的十分清楚,请各位看官多多斧正。