Go语言备忘录,2:反射的原理与使用详解

本文内容是本人对Go语言的反射原理与使用的备忘录,记录了关键的相关知识点,以供翻查。 文中如有错误的地方请大家指出,以免误导!转摘本文也请注明出处:Go语言备忘录(2):反射的原理与使用详解,多谢! 参考书籍《The Go Programming Language》、《Go In Action》、《Go语言学习笔记》等

目录:

  1. 预备知识
  2. reflect.Typeof、reflect.ValueOf
  3. Value、Type
  4. 动态调用
  5. 通过反射可以修改原对象
  6. 实现类似“泛型”的功能

1.预备知识:

  • Go的变量都是静态类型(声明时指定的类型),它也有底层类型(定义类型时指定的基础类型,即:它是以什么形式存储的);
  • 一个接口变量存储了一对(value, type):赋值给这个接口变量的具体值value、以及这个值的类型描述符type;
  • Go的接口变量都是静态类型化的:一个接口类型变量总是保持同一个静态类型(即声明时指定的接口类型),即使在运行时它保存的值的类型发生变化,这些值总是满足这个接口。
  • 接口的静态类型决定了能用接口变量调用哪些方法(接口中定义的方法,它们是保存的值的方法集的子集);
  • 反射是一种检查存储在接口变量中的(value, type)对的机制,反射操作所需的全部信息都源自接口变量(通过把变量转换为空接口变量,从而获得了该变量的value、type,这样就可以进行一系列的“反射操作”);
  • reflect包中的两个类型:Type和Value,这两种类型提供了访问一个接口变量中所包含的(value, type)对的途径;

2.反射由reflect包提供支持,主要方法:

  • func TypeOf ( i interface{} ) Type:

    如 reflect.Typeof(x) ,形参x被保存为一个接口值并作为参数传递(复制),方法内部会把该接口值拆包恢复出x的类型信息保存为reflect.Type并返回;

  • func ValueOf ( i interface{} ) Value:

    如 reflect.ValueOf(x) ,形参被保存为一个接口值并作为参数传递(复制), 方法内部把该接口值的值恢复出来保存为reflect.Value并返回;

3.reflect包的两个主要类型Value、Type:这两种类型都提供了大量的方法让我们可以检查和操作这两种类型

  • Type 接口:可以表示一个Go类型
    • Kind() 将返回一个常量,表示具体类型的底层类型
    • Elem()方法返回指针、数组、切片、map、通道的基类型;
    • 可用反射提取struct tag,还能自动分解,常用于ORM映射、数据验证等;
    • 辅助判断方法Implements()、ConvertibleTo()、AssignableTo()
  • Value 结构体:可以持有一个任意类型的值
    • 调用 Value 的 Type() 将返回具体类型所对应的 reflect.Type(静态类型)
    • 调用 Value 的 Kind() 将返回一个常量,表示具体类型的底层类型
    • Interface方法是ValueOf方法的逆,它把一个reflect.Value恢复成一个接口值:把Value中保存的类型和值的信息打包成一个接口表示并返回;如:

      y,ok := v.Interface().(float64) // y 的类型被断言为 float64

      fmt.Println(y)

      以上可简写为这样:

      fmt.Println(v.Interface()) //fmt.Println会把它恢复出来

    • 通道类型的反射对象:有TrySend()、TryRecv()方法;
    • IsNil()方法判断反射对象保存的值是否为nil;

4.通过反射可以动态调用原对象的导出方法:

1

2

3

4

5

6

7

v := reflect.ValueOf(&x)

m := v.MethodByName("Show")

in := []reflect.Value{

reflect.ValueOf(23),

reflect.ValueOf(323),

}

out := m.Call(in)//对于变参可用CallSlice方法

5.通过反射可以修改原对象:

  • 原理:
    • 因为给Go的函数、方法传递的都是形参的副本,同样的,反射一个对象时,形参被保存为一个接口对象并作为参数传递(复制),该接口变量是non-settable的,返回的Value也是non-settable的,对它调用Set方法会出现错误;
    • Value的CanSet方法用于测试一个Value的Settablity性质,它有点像unaddressability,但是更加严格,描述的是一个反射对象能够修改创造它的那个实际存储的值的能力。settability由反射对象是否保存原始项而决定。
    • 所以,如果想通过反射来修改对象,必须先把该对象的指针传给reflect.ValueOf(&x),这样得到的Value对象内部就保存了原对象指针的副本,只有找到该指针指向的值才能修改原始对象,通过Elem()方法就可以获得一个保存了原对象的Value对象,此时的Value对象就是settable的;

对于一个settable的Value反射对象,如 d := reflect.ValueOf(&x).Elem():

  • d.CanAddr()方法:判断它是否可被取地址
  • d.CanSet()方法:判断它是否可被取地址并可被修改

通过一个settable的Value反射对象来访问、修改其对应的变量的方式:

  • 方式1:通过把反射对象转换回原对象类型的指针,然后直接修改该指针
    • px := d.Addr().Interface().(*int)
    • 第一步是调用Addr()方法,它返回一个Value,里面保存了指向变量的指针。
    • 然后是在Value上调用Interface()方法,也就是返回一个interface{},里面通用包含指向变量的指针。
    • 最后,如果我们知道变量的类型,我们可以使用类型的断言机制将得到的interface{}类型的接口强制环为普通的类型指针。这样我们就可以通过这个普通指针来更新变量了
  • 方式2:可直接通过Set()方法来修改
    • d.Set(reflect.ValueOf(4))
    • SetInt、SetUint、SetString和SetFloat等方法:d.SetInt(3),注意:虽然如SetInt()等方法只要参数变量的底层数据类型是有符号整数就可以工作,但不能是一个引用interface{}类型的reflect.Value
  • 小结:Value反射对象为了修改它们所表示的东西必须要有这些东西的地址
  • 例子:

1

2

3

4

5

6

7

8

9

varx float64 = 3.4

p := reflect.ValueOf(&x)// 注意这里:把x地址传进去了!

fmt.Println(p.Type())//*float64

fmt.Println(p.CanSet())//false 这里的p只是指针,仍然是non-settable的

v := p.Elem()//此时的v保存了x

fmt.Println( v.CanSet())//true

v.SetFloat(7.1)

fmt.Println(v.Interface())//7.1

fmt.Println(x)//7.1

虽然反射可以越过Go语言的导出规则的限制读取结构体中未导出的成员,但不能修改这些未导出的成员。因为一个struct中只有被导出的字段才是settable的。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

typeTstruct{

A int

B string

}

t := T{23,"skidoo"}

s := reflect.ValueOf(&t).Elem()

typeOfT := s.Type()//把s.Type()返回的Type对象复制给typeofT,typeofT也是一个反射。

fori := 0; i < s.NumField(); i++ {

f := s.Field(i)//迭代s的各个域,注意每个域仍然是反射。

fmt.Printf("%d: %s %s = %v\n", i,

typeOfT.Field(i).Name, f.Type(), f.Interface())//提取了每个域的名字

}

//0: A int = 23

//1: B string = skidoo

s.Field(0).SetInt(77)//s.Field(0).Set(reflect.ValueOf(77))

s.Field(1).SetString("Sunset Strip")

fmt.Println("t is now", t)//t is now {77 Sunset Strip}

6.反射库提供了内置函数make和new的对应操作,如reflect.MakeFunc()方法,通过它可以实现类似“泛型”的功能:

  • 定义一个可适应不同数据类型的通用模板算法函数,然后用reflect.MakeFunc()方法,可以把任意函数类型变量绑定到通用模板算法函数(为一系列函数对象指定同一个函数体);

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

packagemain

import(

"reflect"

"strings"

"fmt"

)

//通用算法函数体模板

funcadd(args []reflect.Value) (results []reflect.Value) {

iflen(args) == 0 {

returnnil

}

varr reflect.Value

switchargs[0].Kind() {

casereflect.Int:

n:=0

for_,a:=rangeargs{

n+=int(a.Int())

}

r = reflect.ValueOf(n)

casereflect.String:

ss := make([]string,0,len(args))

for_,s:=rangeargs{

ss = append(ss,s.String())

}

r=reflect.ValueOf(strings.Join(ss,""))

}

results = append(results,r)

return

}

funcmakeAdd(Tinterface{}) {

fn:=reflect.ValueOf(T).Elem()

v:=reflect.MakeFunc(fn.Type(),add)//把原始函数变量的类型和通用算法函数存到同一个Value中

fn.Set(v)//把原始函数指针变量指向v,这样它就获得了函数体

}

funcmain() {

//定义函数变量,未定义函数体

varintAddfunc(x,y int) int

varstrAddfunc(a,b string) string

makeAdd(&intAdd)

makeAdd(&strAdd)

fmt.Println(intAdd(12,23))//35

fmt.Println(strAdd("hello, ","world!"))//hello, world!

}

最后,反射对性能有一定的影响,如对性能要求较高,须谨慎使用反射!

读完觉得学到点什么,就( 顶一个!)

本文内容是本人对Go语言的反射原理与使用的备忘录,记录了关键的相关知识点,以供翻查。 文中如有错误的地方请大家指出,以免误导!转摘本文也请注明出处:Go语言备忘录(2):反射的原理与使用详解,多谢! 参考书籍《The Go Programming Language》、《Go In Action》、《Go语言学习笔记》等

目录:

  1. 预备知识
  2. reflect.Typeof、reflect.ValueOf
  3. Value、Type
  4. 动态调用
  5. 通过反射可以修改原对象
  6. 实现类似“泛型”的功能

1.预备知识:

  • Go的变量都是静态类型(声明时指定的类型),它也有底层类型(定义类型时指定的基础类型,即:它是以什么形式存储的);
  • 一个接口变量存储了一对(value, type):赋值给这个接口变量的具体值value、以及这个值的类型描述符type;
  • Go的接口变量都是静态类型化的:一个接口类型变量总是保持同一个静态类型(即声明时指定的接口类型),即使在运行时它保存的值的类型发生变化,这些值总是满足这个接口。
  • 接口的静态类型决定了能用接口变量调用哪些方法(接口中定义的方法,它们是保存的值的方法集的子集);
  • 反射是一种检查存储在接口变量中的(value, type)对的机制,反射操作所需的全部信息都源自接口变量(通过把变量转换为空接口变量,从而获得了该变量的value、type,这样就可以进行一系列的“反射操作”);
  • reflect包中的两个类型:Type和Value,这两种类型提供了访问一个接口变量中所包含的(value, type)对的途径;

2.反射由reflect包提供支持,主要方法:

  • func TypeOf ( i interface{} ) Type:

    如 reflect.Typeof(x) ,形参x被保存为一个接口值并作为参数传递(复制),方法内部会把该接口值拆包恢复出x的类型信息保存为reflect.Type并返回;

  • func ValueOf ( i interface{} ) Value:

    如 reflect.ValueOf(x) ,形参被保存为一个接口值并作为参数传递(复制), 方法内部把该接口值的值恢复出来保存为reflect.Value并返回;

3.reflect包的两个主要类型Value、Type:这两种类型都提供了大量的方法让我们可以检查和操作这两种类型

  • Type 接口:可以表示一个Go类型
    • Kind() 将返回一个常量,表示具体类型的底层类型
    • Elem()方法返回指针、数组、切片、map、通道的基类型;
    • 可用反射提取struct tag,还能自动分解,常用于ORM映射、数据验证等;
    • 辅助判断方法Implements()、ConvertibleTo()、AssignableTo()
  • Value 结构体:可以持有一个任意类型的值
    • 调用 Value 的 Type() 将返回具体类型所对应的 reflect.Type(静态类型)
    • 调用 Value 的 Kind() 将返回一个常量,表示具体类型的底层类型
    • Interface方法是ValueOf方法的逆,它把一个reflect.Value恢复成一个接口值:把Value中保存的类型和值的信息打包成一个接口表示并返回;如:

      y,ok := v.Interface().(float64) // y 的类型被断言为 float64

      fmt.Println(y)

      以上可简写为这样:

      fmt.Println(v.Interface()) //fmt.Println会把它恢复出来

    • 通道类型的反射对象:有TrySend()、TryRecv()方法;
    • IsNil()方法判断反射对象保存的值是否为nil;

4.通过反射可以动态调用原对象的导出方法:

1

2

3

4

5

6

7

v := reflect.ValueOf(&x)

m := v.MethodByName("Show")

in := []reflect.Value{

reflect.ValueOf(23),

reflect.ValueOf(323),

}

out := m.Call(in)//对于变参可用CallSlice方法

5.通过反射可以修改原对象:

  • 原理:
    • 因为给Go的函数、方法传递的都是形参的副本,同样的,反射一个对象时,形参被保存为一个接口对象并作为参数传递(复制),该接口变量是non-settable的,返回的Value也是non-settable的,对它调用Set方法会出现错误;
    • Value的CanSet方法用于测试一个Value的Settablity性质,它有点像unaddressability,但是更加严格,描述的是一个反射对象能够修改创造它的那个实际存储的值的能力。settability由反射对象是否保存原始项而决定。
    • 所以,如果想通过反射来修改对象,必须先把该对象的指针传给reflect.ValueOf(&x),这样得到的Value对象内部就保存了原对象指针的副本,只有找到该指针指向的值才能修改原始对象,通过Elem()方法就可以获得一个保存了原对象的Value对象,此时的Value对象就是settable的;

对于一个settable的Value反射对象,如 d := reflect.ValueOf(&x).Elem():

  • d.CanAddr()方法:判断它是否可被取地址
  • d.CanSet()方法:判断它是否可被取地址并可被修改

通过一个settable的Value反射对象来访问、修改其对应的变量的方式:

  • 方式1:通过把反射对象转换回原对象类型的指针,然后直接修改该指针
    • px := d.Addr().Interface().(*int)
    • 第一步是调用Addr()方法,它返回一个Value,里面保存了指向变量的指针。
    • 然后是在Value上调用Interface()方法,也就是返回一个interface{},里面通用包含指向变量的指针。
    • 最后,如果我们知道变量的类型,我们可以使用类型的断言机制将得到的interface{}类型的接口强制环为普通的类型指针。这样我们就可以通过这个普通指针来更新变量了
  • 方式2:可直接通过Set()方法来修改
    • d.Set(reflect.ValueOf(4))
    • SetInt、SetUint、SetString和SetFloat等方法:d.SetInt(3),注意:虽然如SetInt()等方法只要参数变量的底层数据类型是有符号整数就可以工作,但不能是一个引用interface{}类型的reflect.Value
  • 小结:Value反射对象为了修改它们所表示的东西必须要有这些东西的地址
  • 例子:

1

2

3

4

5

6

7

8

9

varx float64 = 3.4

p := reflect.ValueOf(&x)// 注意这里:把x地址传进去了!

fmt.Println(p.Type())//*float64

fmt.Println(p.CanSet())//false 这里的p只是指针,仍然是non-settable的

v := p.Elem()//此时的v保存了x

fmt.Println( v.CanSet())//true

v.SetFloat(7.1)

fmt.Println(v.Interface())//7.1

fmt.Println(x)//7.1

虽然反射可以越过Go语言的导出规则的限制读取结构体中未导出的成员,但不能修改这些未导出的成员。因为一个struct中只有被导出的字段才是settable的。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

typeTstruct{

A int

B string

}

t := T{23,"skidoo"}

s := reflect.ValueOf(&t).Elem()

typeOfT := s.Type()//把s.Type()返回的Type对象复制给typeofT,typeofT也是一个反射。

fori := 0; i < s.NumField(); i++ {

f := s.Field(i)//迭代s的各个域,注意每个域仍然是反射。

fmt.Printf("%d: %s %s = %v\n", i,

typeOfT.Field(i).Name, f.Type(), f.Interface())//提取了每个域的名字

}

//0: A int = 23

//1: B string = skidoo

s.Field(0).SetInt(77)//s.Field(0).Set(reflect.ValueOf(77))

s.Field(1).SetString("Sunset Strip")

fmt.Println("t is now", t)//t is now {77 Sunset Strip}

6.反射库提供了内置函数make和new的对应操作,如reflect.MakeFunc()方法,通过它可以实现类似“泛型”的功能:

  • 定义一个可适应不同数据类型的通用模板算法函数,然后用reflect.MakeFunc()方法,可以把任意函数类型变量绑定到通用模板算法函数(为一系列函数对象指定同一个函数体);

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

packagemain

import(

"reflect"

"strings"

"fmt"

)

//通用算法函数体模板

funcadd(args []reflect.Value) (results []reflect.Value) {

iflen(args) == 0 {

returnnil

}

varr reflect.Value

switchargs[0].Kind() {

casereflect.Int:

n:=0

for_,a:=rangeargs{

n+=int(a.Int())

}

r = reflect.ValueOf(n)

casereflect.String:

ss := make([]string,0,len(args))

for_,s:=rangeargs{

ss = append(ss,s.String())

}

r=reflect.ValueOf(strings.Join(ss,""))

}

results = append(results,r)

return

}

funcmakeAdd(Tinterface{}) {

fn:=reflect.ValueOf(T).Elem()

v:=reflect.MakeFunc(fn.Type(),add)//把原始函数变量的类型和通用算法函数存到同一个Value中

fn.Set(v)//把原始函数指针变量指向v,这样它就获得了函数体

}

funcmain() {

//定义函数变量,未定义函数体

varintAddfunc(x,y int) int

varstrAddfunc(a,b string) string

makeAdd(&intAdd)

makeAdd(&strAdd)

fmt.Println(intAdd(12,23))//35

fmt.Println(strAdd("hello, ","world!"))//hello, world!

}

最后,反射对性能有一定的影响,如对性能要求较高,须谨慎使用反射!