Go语言学习七——反射初探

幸运草 2020年4月16日20:49:07前端框架评论阅读模式

定义

reflection is the ability of a computer program to examine, introspect, and modify its own structure and behavior at runtime.
——维基百科

从定义中我们可以看出,反射是计算机程序在运行时(注意是运行时,而不是编译时)拥有获取并修改自身结构的能力。

反射并不是某一种编程语言的专利,而是在众多语言中广泛应用,例如Java、Python、PHP以及我们今天要讲的Go语言。具体>到特定到语言,具体实现哪些反射特性,以及如何实现就各有千秋了。

典型的反射一般包括如下功能:

  • 获取和修改源代码的结构(例如类、方法、字段等)
  • 从字符串类型的类描述符构造出特定的类和对象
  • 检验并执行一段文本的源代码

发展历史

  • 远古时代,那时候编程还只能使用针对特殊硬件架构的指令,代码实际上是一堆机器指令,权限非常大,可以操作具体的硬件,也可以将一串指令作为变量,因此,代码实际上就具有了运行时的动态特性。
  • 中古时代,开始出现编译型的语言,例如Pascal,Fortron,以及后来的C语言,由于所有的代码逻辑都是预先编写好,在编译时已经确定了,因此代码又失去了运行时的动态性。
  • 近代,Brian Cantwell Smith于1982年在他的博士论文中提出了运行时反射的概念。
  • Java等高级编程语言提供了反射特性,在某些场景中提供了极大的灵活性,例如Java中的Spring框架等等。

reflection in Go

具体到Go语言,秉承Go语言一贯的克制与简单,反射的使用也相对容易。当前,Go语言还不支持通过类描述符动态创建对象。

Type and Value

我们在代码中定义的每一个变量,不管是基本的int,float,还是更复杂的struct,interface,都有类型和值两个概念。例如我们定义如下变量:

var num int = 1
var name string = "golang"

type Car struct {
    Brand string
    Color string
}

var myCar Car = Car {
    Brand: "Tesla",
    Color: "red",
}

var i = interface{}
i = myCar

那么,我们其实是定义了一个类型为int,值为1的整型变量;类型为string,值为"golang"的字符串变量;类型为Car,值为{"Tesla", "red"}的结构体变量;i虽然为接口,但是由于我们将Car类型的对象赋值给了它,因此它的实际类型也是Car,value同为{"Tesla", "red"}。

为了定义类型和值这两个概念,Go中于是定义了reflect.Type、reflect.Value。让我们来简单看一下它们的定义:

type Type interface {

    // 返回该类型在通常情况下的内存对齐
    Align() int

    // 返回当该类型作为struct的成员时的内存对齐
    FieldAlign() int

    // 获取指定下标的方法,第一个方法的下标是0
    Method(int) Method

    // 根据名字获取方法
    MethodByName(string) (Method, bool)

    // 返回方法个数
    NumMethod() int

    // 返回该变量的名称
    Name() string

    // 返回包路径
    PkgPath() string

    // 返回存储该类型所需的内存字节数
    Size() uintptr

    // 返回该类型的字符串表示
    String() string

    // Kind 值一个类型本质上的数据类型,例如一个名为Home的struct的kind是struct
    Kind() Kind

    // 判断当前类型是否实现了接口u
    Implements(u Type) bool

    // 判断当前类型的变量是否可以赋值给类型u的变量,
    AssignableTo(u Type) bool

    // 判断当前类型是否可以转换为类型u
    ConvertibleTo(u Type) bool

    // 判断当前类型是否是可比较的
    Comparable() bool

    // 获取该类型变量占用的bit位数
    Bits() int

    // 获取channel的方向,只适用于channel变量
    ChanDir() ChanDir

    // 判断一个方法的参数是否是可变参数
    IsVariadic() bool

    // 获取指针、数组等类型中包含的实际类型
    Elem() Type

    // 获取struct中的第i个字段
    Field(i int) StructField

    // 对于多层嵌套的struct,根据下标数组逐层获取到对应的字段
    FieldByIndex(index []int) StructField

    // 根据名称获取字段
    FieldByName(name string) (StructField, bool)

    // 通过一个指定的方法来查询所需的字段,函数式编程的具体应用
    FieldByNameFunc(match func(string) bool) (StructField, bool)

    // 获取方法的第i个参数类型
    In(i int) Type

    // 获取map类型的key类型
    Key() Type

    // 获取数组的长度
    Len() int

    // 获取struct类型的字段个数
    NumField() int

    // 获取方法的参数个数
    NumIn() int

    // 获取方法的返回值个数
    NumOut() int

    // 获取方法的第i个返回值
    Out(i int) Type

    common() *rtype
    uncommon() *uncommonType
}
type Value struct {
    // 该值所对应的类型的指针
    typ *rtype

    // 当为指针类型时,存储指针
    ptr unsafe.Pointer

    // 该值的元数据,使用位来存储特定信息
    flag
}

可以看出,Type是一个接口,而Value是一个struct。显然,Type会有很多实现类,例如int,float32等等;而Value也有很多方法,且很多方法与Type中的方法类似,例如Kind()方法,Field()方法等。

登堂入室:获取及修改运行时信息

反射的一大特性就是可以在运行时获取变量的类型和值等信息,这就好比进到了别人家里,“登堂入室”,别人家里摆着什么家具,用了哪些电器,墙壁是什么颜色,地板是什么风格……都一目了然。

知道这些有什么用呢?除了能满足部分人的“偷窥欲”,更多时候,是为了让我们的家更好,例如家里要装修,自然要请设计师到家里来,看看墙壁是不是换一种颜色?窗帘是不是换一种风格?这一切的前提是能够让设计师更好的了解我们房屋的结构信息。

说回到Go,它为我们提供了方便的方法来更好的帮助我们了解我们需要关注的类型信息。我们先来定义一个家庭的房屋基础信息:

type Home struct {
    Area float64 // 总面积,可公开信息
    RoomNum int // 房间个数,可公开信息
    WallColor string // 墙壁颜色,可公开信息
    doorKey string // 门锁密码,私密信息
}

接下来,我们就登堂入室,尝试通过反射来获取房屋信息:

xHome := Home {
    120.7,
    6,
    "White",
    "123456",
}
homeType := reflect.TypeOf(xHome)
homeValue := reflect.ValueOf(xHome)
fmt.Println(homeType)
fmt.Println(homeValue)
for i := 0; i < homeType.NumField();i++ {
    fmt.Printf("field Name:%s, fieldType:%s,filedValue:%vn", homeType.Field(i).Name, homeType.Field(i).Type, homeValue.Field(i).Interface())
}

执行结果如下:

main.Home
{120.7 6 White 123456}
field Name:Area, fieldType:float64,filedValue:120.7
field Name:RoomNum, fieldType:int,filedValue:6
field Name:WallColor, fieldType:string,filedValue:White
panic: reflect.Value.Interface: cannot return value obtained from unexported field or method

我们可以看到,在程序输出xHome的类型信息和值信息,并分别输出了Area、RoomNum、WallColor三个属性后,在尝试访问doorKey属性时报错了,提示我们:cannot return value obtained from unexported field or method,意即无法获取私密的属性或方法。由此我们可以看出,虽然我们可以在受到邀请的情况下进入到别人家中,但是对于别人家里的隐私,我们依然无法获取。

这里例子中,我们主要使用了TypeOf()方法和ValueOf()方法,前者返回接口的类型(reflect.Type的实例),后者返回接口的值(reflect.Value的实例)。获取来Type后,对于复杂类型,例如struct,我们可以更进一步的获取其中的字段(StructField类型)、方法(Method类型)等信息。一层层深入,我们将可以看到所有主人开放出来的信息。

不忘初心:接口还原

我们在使用ValueOf()方法获取到一个接口的值信息后,还可以从该值再还原到接口(这里的接口是指interface{},可以接受一切类型)。例如:

xHome := Home {
    120.7,
    6,
    "White",
    "123456",
}

fmt.Println(xHome)
xHomeValue := reflect.ValueOf(xHome)
yHome := xHomeValue.Interface().(Home)
fmt.Println(yHome)

fmt.Printf("xHome address:%p,yHome address:%pn", &xHome, &yHome)

结果如下:

{120.7 6 White 123456}
{120.7 6 White 123456}
xHome address:0xc42007a180,yHome address:0xc42007a210

可以看出,我们从Value又得到了一个Home对象,但地址不同,说明已经不是同一个对象,而是原来对象的一个拷贝。xHomeValue.Interface().(Home)这一句相当于是一个类型断言,我们断言从Value获取的接口是Home类型的,如果不是,将会报错。

Value不是你想改,想改就能改

有时我们在获取对象的类型和值信息后,还需要修改值,这个时候就要注意了,如果不注意就很容易出错:

xHome := Home {
    120.7,
    6,
    "White",
    "123456",
}

xHomeValue := reflect.ValueOf(xHome)
wallColorValue := xHomeValue.FieldByName("WallColor")
wallColorValue.SetString("Green")

运行程序,你将得到如下信息:

panic: reflect: reflect.Value.SetString using unaddressable value
……

这是因为,我们在执行reflect.ValueOf(xHome)时,由于go是值传递的,因此xHome被当作参数传递到该方法时,会产生一个xHome的拷贝,我们接下来其实一直都是在对xHome的拷贝进行操作,而我们的本意是要修改xHome,而不是它的拷贝,因此go语言为了避免产生这种无意义的修改,就会报错。

那么什么时候我们才可以修改一个对象呢?像通常我们想在一个方法里修改参数值一样,当我们传递指针的时候。

xHome := Home {
    120.7,
    6,
    "White",
    "123456",
}

xHomeValue := reflect.ValueOf(&xHome)
fmt.Printf("xHome type:%sn", xHomeValue.Type())
fmt.Printf("xHome canSet:%vn", xHomeValue.CanSet())
fmt.Printf("xHome Elem:%sn", xHomeValue.Elem().Type())
fmt.Printf("xHome Elem canSet:%vn", xHomeValue.Elem().CanSet())
wallColorValue := xHomeValue.Elem().FieldByName("WallColor")
wallColorValue.SetString("Green")
fmt.Println(xHome)

运行结果如下:

xHome type:*main.Home
xHome canSet:false
xHome Elem:main.Home
xHome Elem canSet:true
{120.7 6 Green 123456}

注意,我们传递了xHome的地址给ValueOf方法,如果此时判断xHomeValue.CanSet(),依然会得到false,因为此时xHomeValue是一个指针,我们不是要修改指针,而是要修改指针所指向的对象,因此可以使用Elem()方法获取对象,然后修改。

此时,我们总算是顺利的修改了墙壁的颜色,顺利完成了装修!关键点在于使用指针。

典型应用

在以下场景中(限于当前的go语言中),使用反射再合适不过:

字段映射

这是一种非常常见的需求:将一个对象中的字段映射为另一个对象中的对应字段(可能是系统外的,例如数据库表中的字段,等等)。

举一个json转换的例子,其实是将go对象的字段转换为json的字段:

type Home struct {
    Area float64 `json:"area"` // 总面积,可公开信息
    RoomNum int `json:"room_num"` // 房间个数,可公开信息
    WallColor string `json:"wall_color"` // 墙壁颜色,可公开信息
    doorKey string // 门锁密码,私密信息
}

func main() {
    xHome := Home {
        120.7,
        6,
        "White",
        "123456",
    }

    b, _ := json.Marshal(xHome)
    fmt.Println(string(b))
}

运行结果如下:

{"area":120.7,"room_num":6,"wall_color":"White"}

利用go自带的encoding/json包,我们可以读取struct中的字段信息,包括字段类型,tag(即json:"area"),并转化为json。

类型判断

当一个方法需要判断参数类型,并根据不同类型作出不同处理时,例如我们可以实现一个通用的Add方法用于实现两个数的相加:

func Add(a interface{}, b interface{}) (result interface{}, err error) {
    aType := reflect.TypeOf(a)
    bType := reflect.TypeOf(b)

    if aType.Kind() != bType.Kind() {
        return nil, fmt.Errorf("a and b are not the same type")
    }
    if aType.Kind() == reflect.Int {
        sum := a.(int) + b.(int)
        return sum, nil
    } else if aType.Kind() == reflect.Float32 {
        sum := a.(float32) + b.(float32)
        return sum, nil
    }

    return nil, fmt.Errorf("unsupport type")

}

func main() {

    result, err := Add(1, 1)
    if err == nil {
        fmt.Printf("1 + 1 = %dn", result)
    }
    result, err = Add(int64(1), 1)
    if err == nil {
        fmt.Printf("1 + 1 = %dn", result)
    } else {
        fmt.Println(err)
    }
}

运行结果如下:

1 + 1 = 2
a and b are not the same type

方法代理

我们可以使用反射来代理调用方法,并在调用前后做一些额外的事情,例如打日志、记录耗时等等。

例如我们可以做一个调用代理,如下:

package proxy

import (
    "fmt"
    "reflect"
)

func Run(obj interface{}, method string, params []reflect.Value) ([]reflect.Value, error){
    objValue := reflect.ValueOf(obj)
    methodToCall := objValue.MethodByName(method)

    fmt.Printf("start to call method %sn", method)
    result := methodToCall.Call(params)
    fmt.Printf("finish call method %sn", method)
    return result, nil
}

然后,我们有一个接口,如下:

type IHome interface {
    OpenDoor(key string) bool
}

type Home struct {
    Area float64 `json:"area"` // 总面积,可公开信息
    RoomNum int `json:"room_num"` // 房间个数,可公开信息
    WallColor string `json:"wall_color"` // 墙壁颜色,可公开信息
    doorKey string // 门锁密码,私密信息
}

func (h Home) OpenDoor(key string) bool {
    if key == h.doorKey {
        return true
    } else {
        return false
    }
}

现在,我们就可以使用proxy包中的Run方法动态的代理我们的方法了:

func main() {

    xHome := Home {
        120.7,
        6,
        "White",
        "123456",
    }
    var key string = "123456"
    var params []reflect.Value = make([]reflect.Value, 0, 1)
    params = append(params, reflect.ValueOf(key))
    var iHome IHome = xHome
    result, err := proxy.Run(iHome, "OpenDoor", params)
    if err == nil {
        fmt.Printf("Run OpenDoor result:%v", result)
    } else {
        fmt.Println(err)
    }
}

运行结果如下:

start to call method OpenDoor
finish call method OpenDoor
Run OpenDoor result:[<bool Value>]

特别声明:以上文章内容仅代表作者本人观点,不代表变化吧观点或立场。如有关于作品内容、版权或其它问题请于作品发表后的30日内与变化吧联系。

  • 赞助本站
  • 微信扫一扫
  • weinxin
  • 加入Q群
  • QQ扫一扫
  • weinxin
幸运草
Go语言接口规则 前端框架

Go语言接口规则

Go语言接口规则 接口是一个或多个方法签名的集合。任何类型的方法集中只要拥有该接口对应的全部方法签名。就表示它 "实现" 了该接口,无须在该类型上显式声明实现了哪个接口。对应方法,是指有相同名称、参数...
Go语言中处理 HTTP 服务器 前端框架

Go语言中处理 HTTP 服务器

1 概述 包 net/http 提供了HTTP服务器端和客户端的实现。本文说明关于服务器端的部分。 快速开始: package main import (   "log"   "net/http" )...

发表评论