golang反射总结

简介

反射是建立在类型系统之上的,首先要清楚的是Go是一种静态语言,准确的说变量在编译器会有一种固定的明确的类型。

type MyInt int

var i int
var j MyInt

上面例子中,i是int类型,j是MyInt类型,二者底层虽然都是int类型,但是若不进行转换是无法直接赋值的。

另一个需要事先了解的接口,接口代表一组固定的方法。一个接口可以存储任何确定的非接口类型的值,只要这种类型的值实现了接口对应的方法,如下

var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)

最后一个重要的点是空接口interface{}。因为他没有方法,所以他可以存储任何具体的值。

关于go语言的反射非常重要的一点是他和接口是密切相关的。每一个接口变量都存储着一个二元组(value, type),其中value表示赋给变量的具体值,type表示该值的类型描述。举例如下:

var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    return nil, err
}
r = tty

在上面代码中,r的二元组实际是(tty, *os.File).需要注意的是虽然r定义的是Reader类型,但是type要求的是对值的完整类型描述,也就是具体类型,而不是接口类型。

从接口值获取反射对象

简单来说,反射是一种检查存储在接口变量中的类型和值的机制。所以在go语言的reflect包中提供了这样两种类型:TypeValue。也就分别代表接口变量中存储的那个二元组。分别使用reflect.TypeOfreflect.ValueOf去获取。如下

func main() {
	var x float64 = 3.4
	fmt.Println("type:", reflect.TypeOf(x))
	fmt.Println("value:", reflect.ValueOf(x))
}

输出:
    type: float64
    value: 3.4

Type()

Type与Value都有大量方法,首先Value由一个Type()方法可以获取Type对象

var x float64 = 3.4
xValue := reflect.ValueOf(x)
fmt.Println(xValue.Type())

Kind()

Type与Value都还有一个Kind方法,他返回与各常量,用来表示存储项的类型,如Uint, Float64, Slice等

func main() {
	var x float64 = 3.4
	xValue := reflect.ValueOf(x)
	xType := xValue.Type()
	fmt.Println(xValue.Kind())
	fmt.Println(xType.Kind())
}

都输出float64

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
)

Kind表示的实际上就是底层类型,虽然我们可以定义的类型由无数种,但都是基于某种底层类型,如下

type myInt int

func main() {
	var x myInt = 5
	xValue := reflect.ValueOf(x)
	fmt.Println(xValue.Kind())
}


输出:int

Xxxx

此外Value还有一组以类型命名的方法,如Bool(),Int()等,可以让我们取Value表示的具体的值。这里有一点需要注意的是,虽然我们直接将Value类型传递给fmt也可以直接获得值,那是因为fmt会帮我们做特别处理,若要获得真正的值还要借助这一组方法,这些方法返回的都是基本的类型。另外,如果用错类型会报错,如原本是float,我们却用int取值,就会报错

func main() {
	var x float64 = 3.4
	v := reflect.ValueOf(x)
	fmt.Println("value:", v.Int())
}

SetXxx

Value还有一组Set方法,如SetInt,SetFloat等,但是使用起来有许多需要注意的,见后文详细分析。

从反射对象获取接口值

前面说的是给定一个接口值获得反射对象(Type、Value),这里我们还可以反过来

func main() {
	var x float64 = 3.4
	v := reflect.ValueOf(x)
	y:=v.Interface().(float64)
	fmt.Println(y)
}

进一步的,由于println方法接受interface参数,我们甚至可以这样

fmt.Println(v.Interface())

修改反射对象

首先通过反射对象修改一个值,最重要的前提是该值必须是可设置的

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)

上面一段代码必然会可报错,回顾刚才那句话,能修改的前提是该值是可设置的。这里用CanSet()方法检测,结果是false。

关于可设置性实际上类似于可寻址,通过看报错信息就可以知道panic: reflect: reflect.Value.SetFloat using unaddressable value。回顾刚才那段代码,我们通过调用ValueOf方法获得一个Value对象,ValueOf传入的是x的一个副本,这就类似c语言中的一个经典问题:值传递和引用传递。当是值传递是,给方法的仅仅是一个副本,所以不能修改对象本身,既然这样我们试试传入地址呢?

var x float64 = 3.4
p := reflect.ValueOf(&x) 
fmt.Println(p.Type())
fmt.Println(p.CanSet())

输出:*float64false

很遗憾还是不可设置,但是注意这里的不可设置是不可以修改*p(指针)本身。我们当然不要修改指针本身,我们要修改指针指向的值,这是就需要Elem方法

func main() {
	var x float64 = 3.4
	p := reflect.ValueOf(&x)
	fmt.Println(p.Type())
	fmt.Println(p.CanSet())
	v := p.Elem()
	fmt.Println(v.Type())
	fmt.Println(v.CanSet())
}

输出:
    *float64
    false
    float64
    true

通过调用Elem方法我们又获得了一个Value对象,但是这个对象就不是指针了,而且是可修改的,这时就可以调用SetXxx方法

func main() {
	var x float64 = 3.4
	p := reflect.ValueOf(&x)
	v := p.Elem()
	v.SetFloat(5.1)
	fmt.Println(v.Interface())
	fmt.Println(x)
}

输出:5.1 5.1

总结一下,虽然是到了反射的层面,也依旧是指针才能修改对象。

结构体

前面讲的都是基本类型,关于结构体也是一样的原理,下面给一个反射结构体的例子

获取结构体成员

type T struct {
	A int
	B string
}

func main() {
	t := T{23, "skidoo"}

	tType:=reflect.TypeOf(t)
	tValue:=reflect.ValueOf(t)

	for i := 0; i<tType.NumField();i++  {
		tf:=tType.Field(i)
		f:=tValue.Field(i)
		fmt.Printf("%d : %s(%s) %s=%v\n",i,tf.Name,f.Kind(),f.Type(),f.Interface())
	}
}

注意这里用了两个Field方法,都是通过编号取对应的成员。但是主要区别是,Type的Field方法返回的是一个StructField对象,其中包含着字段的名称,包路径,类型,标签(在序列化与反序列化操作中,如json中很重要)等内容。而Value的Field方法返回的就是成员的一个Value对象,可进一步做反射操作

获取结构体方法

type T struct {
	A int
	B string
}

func(t T)f1(){

}

func(t T)F2(){

}

func (t T)F3(i int)int  {
	return 0
}

func(t *T)F4(){

}


func main() {
	a:=&T{5,"hello"}
	b:=T{5,"hello"}

	tType1 := reflect.TypeOf(a)
	tType2 := reflect.TypeOf(b)

	for i:=0; i<tType1.NumMethod();i++ {
		m:=tType1.Method(i)
		fmt.Printf("该结构体第%d个方法名为%v,类型是%v\n",i,m.Name,m.Type)
		fnType:=m.Func.Type()
		for j := 0; j < fnType.NumIn(); j++ {
			fmt.Printf("\t 该方法第%d个参数类型为%v\n",j,fnType.In(j))
		}
		for j := 0; j < fnType.NumOut(); j++ {
			fmt.Printf("\t 该方法第%d个返回值类型为%v\n",j,fnType.Out(j))
		}
	}

	fmt.Println("-----------")

	for i:=0; i<tType2.NumMethod();i++ {
		m:=tType2.Method(i)
		fmt.Printf("该结构体第%d个方法名为%v,类型是%v\n",i,m.Name,m.Type)
		fnType:=m.Func.Type()
		for j := 0; j < fnType.NumIn(); j++ {
			fmt.Printf("\t 该方法第%d个参数类型为%v\n",j,fnType.In(j))
		}
		for j := 0; j < fnType.NumOut(); j++ {
			fmt.Printf("\t 该方法第%d个返回值类型为%v\n",j,fnType.Out(j))
		}
	}

}

输出:
    该结构体第0个方法名为F2,类型是func(*main.T)
    	 该方法第0个参数类型为*main.T
    该结构体第1个方法名为F3,类型是func(*main.T, int) int
    	 该方法第0个参数类型为*main.T
    	 该方法第1个参数类型为int
    	 该方法第0个返回值类型为int
    该结构体第2个方法名为F4,类型是func(*main.T)
    	 该方法第0个参数类型为*main.T
    -----------
    该结构体第0个方法名为F2,类型是func(main.T)
    	 该方法第0个参数类型为main.T
    该结构体第1个方法名为F3,类型是func(main.T, int) int
    	 该方法第0个参数类型为main.T
    	 该方法第1个参数类型为int
    	 该方法第0个返回值类型为int

基本流程还是先反射出Type对象,然后用NumMethod()获取结构体方法数(只包含可导出方法),接着用Method方法获取某个方法的Method对象,Method包含着方法的的基本描述,但是不含参数与返回值。要获得这两组内容需要先获取Method的Func成员,他是一个Value对象,他的NumIn()方法表示参数个数,In()表示某个参数的类型。同理,NumOut()表示返回值个数,Out()表示某个返回值的类型。暂时无法获取到参数名,因为在调用是参数名是无关紧要的,只要按照参数类型传值即可。

还有一点需要注意的是,对指针类型对象和对值类型对象反射获取到的方法数是不同的,指针类型可以获取所有可导出方法,但是值类型只能获取那些值方法。

另外所有方法的参数第一个参数都是结构体对象本身,这也就间接的解释了结构体方法的特点。

利用反射实例化

func main() {
	var a int
	t := reflect.TypeOf(a)
	n := reflect.New(t)
	n.Elem().SetInt(10)
	fmt.Println(a)
	fmt.Println(n.Elem().Interface())

}

首先要反射出一种Type对象,然后用new方法新建一个Value对象,注意这实际上是指针类型的,所以要用前面讨论过的方法进行赋值。再给一个实例化结构体的

func main() {
	var t T
	tType := reflect.TypeOf(t)
	n := reflect.New(tType)
	n.Elem().Field(0).SetInt(10)
	n.Elem().Field(1).SetString("hello")
	nt := n.Elem().Interface().(T)
	fmt.Println(nt.A,nt.B)
}

利用反射调用方法

func main() {
	v:=reflect.ValueOf(add)
	params:=[]reflect.Value{reflect.ValueOf(10),reflect.ValueOf(20)}
	r :=v.Call(params)
	fmt.Println(r[0].Int())
}

func add(a,b int)int  {
	return a+b;
}

首先反射出方法的value对象,然后构造参数数组,接着调用call方法调用,返回值是一个数组,表示方法的返回值数组。再给一个反射调用结构体方法的例子

type T struct {
	A int
	B string
}

func(t *T)Add(i int)int{
	return t.A+i;
}


func main() {
	t:=&T{10,"hello"}
	tType:=reflect.TypeOf(t)
	f:=tType.Method(0).Func
	fmt.Println(f.Call([]reflect.Value{reflect.ValueOf(t),reflect.ValueOf(10)})[0].Int())
}

注意,凡是属于结构体的方法,默认第一个参数都是结构体对象本身。

题图来自unsplash:https://unsplash.com/photos/wAn4RfmXtxU