Golang Reflect反射3

序言

第一次接触反射技术是在很多年前学习设计模式的时候,那时在优化Java版简单工厂的实现,当读取配置信息中的的类型字符串后利用反射来创建对象实例,替代了switch case语句的分支判断。
第二次接触反射技术是在几年前微服务架构开始大范围流行的时候,那时在考虑异构微服务的多版本集成问题,支持反射的语言(Java等)的序列化就容易很多,而不支持反射的语言(C++等)的序列化就麻烦一些,不过最后统一选择了序列化工具protobuf,支持跨语言。
第三次接触反射技术是在听某个团队Session的时候,那时在简单分享Golang的反射机制,发现很多同学都没什么感觉,主要原因是大家在产品代码中几乎不会使用到反射技术。

在生活中,反射像一面镜子,通过镜子你可以清楚的看见自己的一切。

在计算机中,反射表示程序能够检查自身结构的一种能力,尤其是类型。通过反射,你可以获取对象类型的详细信息,并可动态操作对象。

众所周知,大多数语言都支持反射,但每种语言的反射模型却不尽相同。笔者在这一年多的Golang实践中,学习或二次开发了很多个框架,其中大部分都涉及反射技术,比如GoConvey,GoMock,GoStub,Monkey和gRPC等,所以本文的主角是Golang反射模型

一、反射应用的一个简单案例

我们在讨论反射模型之前,先看看反射应用的一个简单案例:

// Stubs represents a set of stubbed variables that can be reset.
type Stubs struct {
    // stubs is a map from the variable pointer (being stubbed) to the original value.
    stubs   map[reflect.Value]reflect.Value
    origEnv map[string]envVal
}

// Stub replaces the value stored at varToStub with stubVal.
// varToStub must be a pointer to the variable. stubVal should have a type
// that is assignable to the variable.
func (s *Stubs) Stub(varToStub interface{}, stubVal interface{}) *Stubs {
    v := reflect.ValueOf(varToStub)
    stub := reflect.ValueOf(stubVal)

    // Ensure varToStub is a pointer to the variable.
    if v.Type().Kind() != reflect.Ptr {
        panic("variable to stub is expected to be a pointer")
    }

    if _, ok := s.stubs[v]; !ok {
        // Store the original value if this is the first time varPtr is being stubbed.
        s.stubs[v] = reflect.ValueOf(v.Elem().Interface())
    }

    // *varToStub = stubVal
    v.Elem().Set(stub)
    return s
}

// Reset resets all stubbed variables back to their original values.
func (s *Stubs) Reset() {
    for v, originalVal := range s.stubs {
        v.Elem().Set(originalVal)
    }
    s.resetEnv()
}

上面的代码来自GoStub框架,Stub方法可以连续对多个全局变量或函数变量打桩,Reset方法对所有全局变量或函数变量的桩进行回滚。
Stub方法有两个参数,第一个参数是变量的地址,第二个参数是变量的桩。这两个参数的类型都是空接口,在方法处理的开始都转换成了reflect.Value类型。然而,无论怎么转换,第一个参数必须是指针,因为最终目标是要改变指针指向的值,所以要进行“址传递”。当指针校验不通过时(v.Type().Kind() != reflect.Ptr),就会触发panic。Stub方法先在map中保存变量指针和变量初始值,然后修改变量指针指向的值为桩。Reset方法将map中保存的所有变量指针指向的值修改为初始值。

这就是反射应用的一个简单案例。大家先了解大体意思就行,不用太纠结技术细节,等理解反射模型后,就会发现it is a piece of cake

因为反射建立在类型系统之上,所以我们先从基础知识开始:)

二、类型系统

顾名思义,类型系统是指一个语言的类型体系结构,是一门编程语言的地基。一个典型的类型系统统筹包括如下几本内容:

  • 基本类型,如int,bool,float,string等
  • 复合类型,如数组,结构体,指针等
  • Any类型
  • 值语义和引用语义
  • 接口

Golang与Python不同,变量类型是静态的,即变量在创建的时候类型就已经确定。
我们对变量做如下声明:

type MyInt int

var i int
var j MyInt

变量i的类型是int,而变量j的类型是MyInt,虽然底层类型都是int,但是它们的静态类型并不一样,当不经过类型转换直接相互赋值时,编译器会报错。

Golang中的指针相对C/C++语言进行了大量的简化,没有”强大“的指针计算功能,仅仅代表变量的地址,属于复合类型。当指针类型作为函数参数类型时,为“址传递”。
Golang中有四种类型比较特殊,为引用类型,分别为slice,map,channel和interface。当引用类型作为函数参数类型时,为“引用传递”,只能使modify操作生效,而add操作不能生效。要想使add操作生效,必须使用“址传递”,所以json.Unmarshal时引用类型的变量也要进行“址传递”。反过来说,如果没有add操作,则就没有必要进行“址传递”,“引用传递”的效率也很高。
其他类型作为函数参数类型时均为“值传递”。

在Golang中,可以给任意自定义类型添加相应的方法,指针类型除外。

type Integer int
func (a Integer) Less(b Integer) bool { 
    return a < b
 }

Integer和int属于不同的静态类型,更重要的是Integer可以像普通的类一样使用:

func main() {
    var a Integer = 1 
    if a.Less(2) {
        fmt.Println(a, "Less 2")
    }
}

Golang中的struct和其它语言中的class有同等重要的地位,但是Golang放弃了包括继承在内的大量面向对象特性,通过组合来解决所有问题(通过匿名组合来模拟继承的一部分功能)。

Golang中的interface是整个类型系统的基石:

  • 非侵入式:一个类只要实现了接口要求的所有函数,就说这个类实现了该接口
  • 接口赋值:既可以将对象实例赋值给接口,也可以将一个接口赋值给另一个接口
  • 接口查询:可以直接了当地询问接口指向的对象实例是否实现了另一个接口
  • 类型查询:可以直接了当地询问接口指向的对象实例的类型
  • 接口组合:可以认为接口组合式类型匿名组合的一个特殊场景
  • Any类型:任意类型都实现了零个或多个方法,那就是说任意类型都实现了空接口interface{},即interface{}是可以指向任意对象的Any类型

三、反射模型

在Golang的实现中,每个interface变量都有一个对应的pair,pair中记录了实际变量的值和类型,即(value, type)。在基本的层面上,反射只是一个检查存储在interface变量中的value和type的算法,value和type用类型reflect.Value和reflect.Type描述。
尽管从reflect.Value也很容易得到reflect.Type(reflect.Value.Type()),但为了让value和type在概念上进行分离,我们更习惯用pair进行表达。

经过学习和实践,我们将Golang中的反射模型形式化表达如下:

interface --> (value, type)
(value, type) --> (value, kind)
(value, type) --> interface
(value, type) --> (value1, type)

1、interface --> (value, type)

定义类File:

type File struct { 
    ...
}
func (f *File) Read(buf []byte) (n int, err error) {
    ...
}

func (f *File) Write(buf []byte) (n int, err error) {
    ...
}

func (f *File) Seek(off int64, whence int) (pos int64, err error) {
    ...
}

func (f *File) Close() error {
    ...
}

定义接口Reader,Writer和Closer:

type Reader interface {
    Read(buf []byte) (n int, err error)
}

type Writer interface {
    Write(buf []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

尽管File类并没有从这些接口继承,甚至可以不知道这些接口的存在,但是File类却实现了这些接口:

f := &File{}
var r Reader
r = f
var w Writer
w = r.(Writer)
var c Closer
c = w.(Closer)

空接口可以直接赋值:

var e interface{}
e = c

上面代码中,r,w,c和e的pair均为(f, *File)。pair的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。

如何从接口变量中获取value和type信息?
通过reflect.ValueOf函数获取value,通过reflect.TypeOf函数获取type:

package main

import (
    "fmt"
    "reflect"
)

type MyInt int

func main() {
    var i MyInt = 10
    fmt.Println("value: ", reflect.ValueOf(i))
    fmt.Println("type: ", reflect.TypeOf(i))
}

运行时输出的结果是:

value:  10
type:  main.MyInt

可见type中描述的是静态类型。

2、(value, type) --> (value, kind)

(value, type)中的type描述的是静态类型,那如何才能知道底层类型?

reflect.Type有一个Kind方法,而通过Kind方法返回一个常量来表示底层类型:

package main

import (
    "fmt"
    "reflect"
)

type MyInt int

func main() {
    var i MyInt = 10
    fmt.Println("type: ", reflect.TypeOf(i))
    fmt.Println("type.Kind: ", reflect.TypeOf(i).Kind())
}

运行时输出的结果是:

type:  main.MyInt
type.Kind:  int

底层类型在reflect包中有详细的定义:

// A Kind represents the specific kind of type that a Type represents.
// The zero Kind is not a valid kind.
type Kind uint

const (
    Invalid Kind = iota
    Bool
    Int
    Int8
    Int16
    Int32
    Int64
    Uint
    Uint8
    Uint16
    Uint32
    Uint64
    Uintptr
    Float32
    Float64
    Complex64
    Complex128
    Array
    Chan
    Fun
    Interface
    Map
    Ptr
    Slice
    String
    Struct
    UnsafePointer
)

3、(value, type) --> interface

reflect.Value可以使用Interface方法还原接口值,该方法高效的打包类型和值信息到接口值中,并返回这个结果:

func (v Value) Interface() (i interface{}) {
    return valueInterface(v, true)
}

当得到一个类型为reflect.Value的变量,可以通过下面的方式转换变量的类型:

t := v.Interface().(T)

当类型查询成功后,t就可以使用T的成员和方法。

reflect.Value除过能直接转换成Interface{},还可以直接转换成基本类型,比如:

func (v Value) Bool() bool
func (v Value) Bytes() []byte
func (v Value) Int() int64
func (v Value) Float() float64
...

以Int方法为例,如果v.Kind()不是Int, Int8, Int16, Int32, 活着Int64,会触发panic。

4、(value, type) --> (value1, type)

reflect.Value变量是通过reflect.Value(n)获得的,n可能是值也可能是指针。

当n是值时,不能通过reflect.Value修改n的值:

n := 3.4
v := reflect.ValueOf(n)
fmt.Println("settability of v:", v.CanSet())
v.SetFloat(4.1)

运行上面的代码,结果如下:

settability of v: false
panic: reflect: reflect.Value.SetFloat using unaddressable value

当n是指针时,可以通过reflect.Value修改n指向的变量的值:

m := 3.4
n := &m
p := reflect.ValueOf(n)
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())
v.SetFloat(4.1)
fmt.Println("m: ", m)

运行上面的代码,结果如下:

settability of v: true
m:  4.1

四、进阶话题

1、对结构的反射操作

对于结构的反射操作并没有根本的不同,只是用了reflect.Value和reflect.Type的Field方法按索引获取对应的成员,比如:

type Student struct {
    Id   int
    Name string
}

s := Student{100032, "zxl"}
v := reflect.ValueOf(s)
t := v.Type()
for i := 0; i < v.NumField(); i++ {
    f := v.Field(i)
    fmt.Printf("%d: %s %s = %v\n", i,
        t.Field(i).Name, f.Type(), f.Interface())
}

运行上面的代码,结果如下:

0: Id int = 100032
1: Name string = zxl

如果想修改结构体中成员的值时,则

  • v.CanSet()必须为true
  • 该成员名必须大写(可导出)
type Student struct {
    Id   int
    Name string
}

s := Student{100032, "zxl"}
v := reflect.ValueOf(&s).Elem()
t := v.Type()
v.Field(0).SetInt(230001)
v.Field(1).SetString("lxz")
for i := 0; i < v.NumField(); i++ {
    f := v.Field(i)
    fmt.Printf("%d: %s %s = %v\n", i,
        t.Field(i).Name, f.Type(), f.Interface())
}

运行上面的代码,结果如下:

0: Id int = 230001
1: Name string = lxz

2、通过反射创建一个匿名函数

假设函数FuncReturing的功能是创建一个匿名函数:

func FuncReturning(funcType reflect.Type, results ...interface{}) reflect.Value {
    var resultValues []reflect.Value
    for i, r := range results {
        var retValue reflect.Value
        if r == nil {
            retValue = reflect.Zero(funcType.Out(i))
        } else {
            tempV := reflect.New(funcType.Out(i))
            tempV.Elem().Set(reflect.ValueOf(r))
            retValue = tempV.Elem()
        }
        resultValues = append(resultValues, retValue)
    }
    return reflect.MakeFunc(funcType, func(_ []reflect.Value) []reflect.Value {
        return resultValues
    })
}

函数FuncReturing的第一个参数funcType用于描述匿名函数的类型,其余参数results是一个变参(本质是数组切片的语法糖),用于匿名函数的返回值的输入:

  • 当results中的元素r等于nil时,我们不能直接使用reflect.ValueOf(nil),而是生成一个reflect.Value零值
  • 当results中的元素r不等于nil时,我们不能直接使用reflect.ValueOf(r),而是先使用reflect.New生成一个reflect.Value对象,然后设置这个对象的数据

最后通过reflect.MakeFunc生成一个funcType类型的匿名函数。

注:为了简单起见,例子中的匿名函数只关注返回值,而对参数不care。

如果以上描述的很难理解,先看以下示例:

package main

import (
	"fmt"
	"reflect"
)

type Temp struct {
	date string
	Source string
}
func main() {
	reflectFun()
}
func reflectFun()  {
	//创建类型示例
	fmt.Println("===========创建类型示例演示=========")
	var a  Temp
	typeOfA:=reflect.TypeOf(a)
	fmt.Println(typeOfA)
	aIns:=reflect.New(typeOfA)
	fmt.Println(aIns.Type(),aIns.Kind(),aIns)
	//fmt.Println(aIns.set)
	//修改值
	fmt.Println("===========修改值示例演示=========")
	valueOfDog:=reflect.ValueOf(&Temp{Source:"2018-05-20"})
	elem := valueOfDog.Elem()
	name := elem.FieldByName("Source")
	name.SetString("2019-05-20") //source : 结构体首字符要大写,不然执行此处会发生异常
	fmt.Println(name.String())
	//判断是否有效
	fmt.Println("===========判断是否有效示例演示=========")
	s:=Temp{}
	fmt.Println(reflect.ValueOf(s).FieldByName("Source").IsValid()) //找到(有效)
	fmt.Println(reflect.ValueOf(s).FieldByName("source01").IsValid())//没有找到(无效)
}
func main() {
	//定义一个函数,完成数据的交换,具体实现由makeSwap执行
	//由于在事先不知道具体的数据类型,因此采用reflect.Value
	swap:= func(in []reflect.Value)[]reflect.Value {
		return []reflect.Value{in[1],in[0]}
	}
	//执行数据交换的实现
	makeSwap:= func(fPtr interface{}) {
		//获取元素
		fn:=reflect.ValueOf(fPtr).Elem()
		//创建正确类型的函数
		//func MakeFunc(typ Type, fn func(args []Value) (results []Value)) Value
		//MakeFunc返回一个具有给定类型、包装函数fn的函数的Value封装。当被调用时,该函数会:
		//
		//- 将提供给它的参数转化为Value切片
		//- 执行results := fn(args)
		//- 将results中每一个result依次排列作为返回值
		v := reflect.MakeFunc(fn.Type(), swap)
		//赋值(1次即可完成所有值的赋值)
		fn.Set(v)
	}

	var intSwap func(int,int)(int,int)
	makeSwap(&intSwap)
	i1, i2 := intSwap(100, 200)
	fmt.Println(i1,i2)

	var strSwap func(string,string)(string,string)
	makeSwap(&strSwap)
	str1,str2:=strSwap("hello","jiangzhou")
	fmt.Println(str1,str2)

}

可以看出,代码在不知道具体类型的情况下动态完成了数据的交换。

接下来,再来看看上面提到的函数FuncReturing。

func FuncReturing(funcType reflect.Type,results ...interface{})reflect.Value  {
	var resultsValue []reflect.Value
	for i,r:=range results{
		var value reflect.Value
		if r==nil {
			//Zero返回一个持有类型typ的零值的Value。注意持有零值的Value和Value零值是两回事。Value零值表示不持有任何值。
			// 例如Zero(TypeOf(42))返回一个Kind为Int、值为0的Value。Zero的返回值不能设置也不会寻址。
			//Out(i int) 返回func类型的第i个返回值的类型,如非函数或者i不在[0, NumOut())内将会panic
			value=reflect.Zero(funcType.Out(i))//i 为函数对应的第i个参数
		}else {
			//New返回一个Value类型值,该值持有一个指向类型为typ的新申请的零值的指针,返回值的Type为PtrTo(typ)。
			tempV:=reflect.New(funcType.Out(i))
			tempV.Elem().Set(reflect.ValueOf(r))
			value=tempV.Elem()
		}
		resultsValue= append(resultsValue, value)
	}
	//MakeFunc返回一个具有给定类型、包装函数fn的函数的Value封装
	return reflect.MakeFunc(funcType, func(args []reflect.Value) (results []reflect.Value) {
		return resultsValue
	})
}
func main() {

	var temFun1 func(int,int)(int,int)
	fn:=reflect.ValueOf(&temFun1).Elem()
	funType:=fn.Type()
	v:=FuncReturing(funType,nil,200)
	fn.Set(v)
    fmt.Println(temFun1(333,666))
}

由此可以看出,通过FuncReturing实现了数据的设置,与函数具体输入的值没有什么关系(333,666)(匿名函数只关注返回值,而对参数不care

3、通过反射调用函数或方法

在反射中,函数或方法的底层类型都是reflect.Func。如果要调用函数或方法的话,可以使用reflect.Value的Call方法,参数和返回值类型都是[]reflect.Value。

通过反射调用函数

示例代码:

func prints(i int) string {
    fmt.Println("i =", i)
    return strconv.Itoa(i)
}

func main() {
    v := reflect.ValueOf(prints)
    params := make([]reflect.Value, 1)
    params[0] = reflect.ValueOf(20)
    fmt.Println("result:", v.Call(params)[0])
}

运行上面的代码,结果如下:

i = 20
result: 20

通过反射调用方法

方法和函数可以说在本质上是相同的,只不过方法与一个对象进行了绑定,方法是对象的一种行为,比如:

type Student struct {
    id   int
    name string
}

func (s *Student) SetId(id int) {
    s.id = id
}

func (s *Student) SetName(name string) {
    s.name = name
}

func (s *Student) String() string {
    return "id = " + strconv.Itoa(s.id) + ", " + "name = " + s.name
}

使用reflect.Value的MethodByName方法:

func main() {
    s := &Student{100032, "zxl"}
    v := reflect.ValueOf(s)
    fmt.Println("Before:", v.MethodByName("String").Call(nil)[0])

    params := make([]reflect.Value, 1)
    params[0] = reflect.ValueOf(230001)
    v.MethodByName("SetId").Call(params)

    params[0] = reflect.ValueOf("lxz")
    v.MethodByName("SetName").Call(params)

    fmt.Println("After:", v.MethodByName("String").Call(nil)[0])
}

运行上面的代码,结果如下:

Before: id = 100032, name = zxl
After: id = 230001, name = lxz

使用reflect.Value的Method方法:

func main() {
    s := &Student{100032, "zxl"}
    v := reflect.ValueOf(s)
    fmt.Println("Before:", v.Method(2).Call(nil)[0])

    params := make([]reflect.Value, 1)
    params[0] = reflect.ValueOf(230001)
    v.Method(0).Call(params)

    params[0] = reflect.ValueOf("lxz")
    v.Method(1).Call(params)

    fmt.Println("After:", v.Method(2).Call(nil)[0])
}

运行上面的代码,结果如下:

Before: id = 100032, name = zxl
After: id = 230001, name = lxz

小结

本文先通过一个简单案例来说明Golang反射技术的价值,接着简单回顾了类型系统,然后详细阐述了Golang反射模型:

interface --> (value, type)
(value, type) --> (value, kind)
(value, type) --> interface
(value, type) --> (value1, type)

最后又加了几道关于进阶话题的菜:

  • 对结构的反射操作
  • 通过反射创建一个匿名函数
  • 通过反射调用函数或方法

 

希望对读者理解Golang反射模型有一定的帮助。

文章出处:https://www.jianshu.com/p/c713d0507114(在此文章基础上进行了代码解释、示例的增加)

了解更多Go语言知识https://study.163.com/course/introduction/1210620804.htm

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言中的reflect包提供了一种对程序的静态类型进行操作的方法,即可以在程序运行时动态地调用、检查和更改变量、类型和方法。 首先,reflect包提供了两个重要的类型:Type和Value。Type表示变量的类型,Value则表示变量的值。可以通过reflect.TypeOf和reflect.ValueOf来获取一个变量的Type和Value。 使用reflect包,我们可以在运行时获取变量的类型和值的一些基本信息,例如判断一个变量是否是某个特定类型,或者获取一个变量的名称和值。这在某些情况下可能是非常有用的,比如在编写通用的函数时,需要对不同类型的变量做相同的处理。 此外,reflect包还提供了一些函数来获取、设置和调用变量、类型和方法的具体信息。可以使用reflect.Value的相关方法来获取和设置变量的值,也可以使用reflect.Type的相关方法来获取类型的信息。使用reflect包还可以动态地调用某个值的方法。 需要注意的是,使用reflect包可能会导致一些性能上的损失,因为在运行时需要通过反射来获取变量的信息。因此,在性能要求较高的场景下,尽量避免使用反射。 总结而言,reflect包为我们提供了一种在运行时对变量、类型和方法进行操作的方法,可以通过反射来获取、设置和调用它们的信息。但是,需要注意在性能要求较高的情况下,尽量避免使用反射

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值