【go语言学习】反射reflect

一、认识反射

  • 维基百科中的定义:
    在计算机科学中,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。

不同语言的反射模型不尽相同,有些语言还不支持反射。

Go 语言支持反射,它提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法,但是在编译时并不知道这些变量的具体类型。

1、为什么要用反射
  • 需要反射的 2 个常见场景:

(1)有时你需要编写一个函数,但是并不知道传给你的参数类型是什么,可能是没约定好;也可能是传入的类型很多,这些类型并不能统一表示。这时反射就会用的上了。
(2)有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定。这时就需要对函数和函数的参数进行反射,在运行期间动态地执行函数。

  • 在讲反射的原理以及如何用之前,还是说几点不使用反射的理由:

(1)与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。
(2)Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。
(3)反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。

2、反射原理

interface是 Go 语言实现抽象的一个非常强大的工具。当向接口变量赋一个实体类型的时候,接口会存储实体的类型信息,反射就是通过接口的类型信息实现的,反射建立在类型的基础上。

go语言的两个特点:

  • go语言是静态类型语言,因此在程序编译阶段,类型已经确定。
  • interface{}空接口可以和任意类型进行交互,因此可以利用这一特点实现对任意类型的反射。

go语言的类型:

  • 变量包含(type,value)两个部分。
  • type 包括 static type和concrete type. 简单来说 static type是你在编码是看见的类型(如int、string),concrete type是runtime系统看见的类型。
  • 类型断言能否成功,取决于变量的concrete type,而不是static type。因此,一个 reader变量如果它的concrete type也实现了write方法的话,它也可以被类型断言为writer。

go语言的反射是建立在类型的基础上的,golang中对于指定类型的变量的类型是静态的(如指定为int、string类型的变量 ,他们的type是static type),也就是说在创建变量的时候类型已经确定。反射主要针对interface类型(它的type是concrete type)。

在go语言的实现中,每一个interface变量都有一个pair,pair中记录了实际变量的值和类型:

(value, type)

其中:value是实际变量值,type是实际变量类型。一个interface变量包含了2个指针,一个指向值的类型(对应concrete type),一个指向实际的值(对应value)。

代码示例:

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    // 创建一个*os.File类型的变量f
    f, _ := os.OpenFile("a.txt", os.O_RDWR, os.ModePerm)
    // 创建一个io.Reader接口类型的变量r
    var r io.Reader
    r = f.(io.Reader)
    // 创建一个io.Writer接口类型的变量w
    var w io.Writer
    w = r.(io.Writer)
    // 创建一个interface{}空接口类型的变量i
    var i interface{}
    i = w
    fmt.Printf("%T, %T, %T, %T", f, r, w, i)
}

运行结果

*os.File, *os.File, *os.File, *os.File

可以看到,接口变量w、r、i的pair相同,都是:(f, *os.File)。

interface及其pair的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。

下面,我们通过go语言的reflect包提供的API来实现反射机制

二、Type和Value

reflect包提供了两种类型(或者说两个方法)让我们可以很容易的访问接口变量内容,分别是reflect.ValueOf() 和 reflect.TypeOf(),看看官方的解释:

// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i.  ValueOf(nil) returns the zero 
func ValueOf(i interface{}) Value {...}

翻译一下:ValueOf用来获取输入参数接口中的数据的值,如果接口为空则返回0


// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {...}

翻译一下:TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil

reflect.TypeOf()是获取pair中的type,reflect.ValueOf()获取pair中的value。

首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数。

说明:

  1. reflect.TypeOf: 直接给到了我们想要的type类型,如float64int、各种pointerstruct 等等真实的类型
  2. reflect.ValueOf:直接给到了我们想要的具体的值,如1.2345这个具体数值,或者类似&{1, "Allen.Wu", 25}这样的结构体struct的值
  3. 也就是说明反射可以将“接口类型变量”转换为“反射类型对象”,反射类型指的是reflect.Type和reflect.Value这两种

Type 和 Value 都包含了大量的方法,其中第一个有用的方法应该是 Kind,这个方法返回该类型的具体信息:Uint、Float64 等。Value 类型还包含了一系列类型方法,比如 Int(),用于返回对应的值。以下是Kind的种类:

// 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
    Func
    Interface
    Map
    Ptr
    Slice
    String
    Struct
    UnsafePointer
)

三、反射的规则

下图描述了实例、Value、Type 三者之间的转换关系:

1、从实例到Value

func ValueOf(i interface {}) Value

2、从实例到Type

func TypeOf(i interface{}) Type

3、从Type到Value

Type 里面只有类型信息,所以直接从一个 Type 接口变量里面是无法获得实例的 Value 的,但可以通过该 Type 构建一个新实例的 Value。reflect 包提供了两种方法,示例如下:

//New 返回的是一个 Value,该 Value 的 type 为 PtrTo(typ),即 Value 的 Type 是指定 typ 的指针类型
func New(typ Type) Value
//Zero 返回的是一个 typ 类型的零佳,注意返回的 Value 不能寻址,位不可改变
func Zero(typ Type) Value

4、从Value到Type

从反射对象 Value 到 Type 可以直接调用 Value 的方法,因为 Value 内部存放着到 Type 类型的指针。

func (v Value) Type() Type

5、从Value到实例

Value 本身就包含类型和值信息,reflect 提供了丰富的方法来实现从 Value 到实例的转换。例如:

//该方法最通用,用来将 Value 转换为空接口,该空接口内部存放具体类型实例
//可以使用接口类型查询去还原为具体的类型
func (v Value) Interface() (i interface{})

//Value 自身也提供丰富的方法,直接将 Value 转换为简单类型实例,如果类型不匹配,则直接引起 panic
func (v Value) Bool () bool
func (v Value) Float() float64
func (v Value) Int() int64
func (v Value) Uint() uint64

6、从 Value 的指针到值

从一个指针类型的 Value 获得值类型 Value 有两种方法,示例如下。

//如果 v 类型是接口,则 Elem() 返回接口绑定的实例的 Value,如采 v 类型是指针,则返回指针值的 Value,否则引起 panic
func (v Value) Elem() Value
//如果 v 是指针,则返回指针值的 Value,否则返回 v 自身,该函数不会引起 panic
func Indirect(v Value) Value

7、Type 指针和值的相互转换

指针类型 Type 到值类型 Type。例如:

//t 必须是 Array、Chan、Map、Ptr、Slice,否则会引起 panic
//Elem 返回的是其内部元素的 Type
t.Elem() Type

值类型 Type 到指针类型 Type。例如:

//PtrTo 返回的是指向 t 的指针型 Type
func PtrTo(t Type) Type

8、Value 值的可修改性

Value 值的修改涉及如下两个方法:

//通过 CanSet 判断是否能修改
func (v Value ) CanSet() bool
//通过 Set 进行修改
func (v Value ) Set(x Value)

根据 Go 官方关于反射的博客,反射有三大定律:

  1. Reflection goes from interface value to reflection object.
  2. Reflection goes from reflection object to interface value.
  3. To modify a reflection object, the value must be settable.

第一条是最基本的:反射可以从接口值得到反射对象。

第二条实际上和第一条是相反的机制,反射可以从反射对象获得接口值。

第三条不太好懂:如果需要操作一个反射变量,则其值必须可以修改。

四、反射的使用

1、从relfect.Value中获取接口interface的信息

当执行reflect.ValueOf(interface)之后,就得到了一个类型为”relfect.Value”变量,可以通过它本身的Interface()方法获得接口变量的真实内容,然后可以通过类型判断进行转换,转换为原有真实类型。

已知类型

从反射值对象(reflect.Value)中获取值得方法:

方法名 说 明
Interface() interface{} 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型
Int() int64 将值以 int 类型返回,所有有符号整型均可以此方式返回
Uint() uint64 将值以 uint 类型返回,所有无符号整型均可以此方式返回
Float() float64 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回
Bool() bool 将值以 bool 类型返回
Bytes() []bytes 将值以字节数组 []bytes 类型返回
String() string 将值以字符串类型返回

已知类型后转换为其对应的类型的做法如下,直接通过Interface方法然后强制转换,如下:

realValue := value.Interface().(已知的类型)

示例代码:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var a int = 10
    rType := reflect.TypeOf(a)
    fmt.Println(rType)
    rValue := reflect.ValueOf(a)
    fmt.Println(rValue)
    rPointer := reflect.ValueOf(&a)
    fmt.Println(rPointer)
    // 1. 转换的时候,如果转换的类型不完全符合,则直接panic,类型要求非常严格!
    // 2. 转换的时候,要区分是指针还是值
    // 3. 也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
    // convertValue := rValue.Interface().(int) // 10
    convertValue := rValue.Int()
    // convertValue := rValue.Bool() // panic: reflect: call of reflect.Value.Bool on int Value
    convertPointer := rPointer.Interface().(*int)
    fmt.Println(convertValue)
    fmt.Println(convertPointer)
}

运行结果

int
10
0xc000012090
10
0xc000012090

结构体类型

反射值对象(reflect.Value)提供对结构体访问的方法,通过这些方法可以完成对结构体字段和方法的访问,如下表所示。

方 法 说 明
Field(i int) Value 根据索引,返回索引对应的结构体成员字段的反射值对象。当值不是结构体或索引超界时发生panic
NumField() int 返回结构体成员字段数量。当值不是结构体或索引超界时发生panic
FieldByName(name string) Value 根据给定字符串返回字符串对应的结构体字段。没有找到时返回零值,当值不是结构体或索引超界时发生panic
FieldByIndex(index []int) Value 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的值。 没有找到时返回零值,当值不是结构体或索引超界时发生panic
FieldByNameFunc(match func(string) bool) Value 根据匹配函数匹配需要的字段。找到时返回零值,当值不是结构体或索引超界时发生panic
NumMethod() int 返回该类型的方法集中方法的数目
Method(int) Method 返回该类型方法集中的第i个方法
MethodByName(string)(Method, bool) 根据方法名返回该类型方法集中的方法

示例代码:

package main

import (
    "fmt"
    "reflect"
)

// Student 学生结构体
type Student struct {
    Name    string
    Age     int
    Address string
}

// Say Student结构体的方法
func (s Student) Say(msg string) {
    fmt.Println(msg)
}

// PrintInfo Student结构体的方法
func (s Student) PrintInfo() {
    fmt.Printf("姓名:%s\t年龄:%d\t地址:%s\n", s.Name, s.Age, s.Address)
}

func main() {
    s := Student{"tom", 20, "上海市"}
    Test(s)
}

// Test 测试函数
func Test(i interface{}) {
    // 获取i的类型
    rType := reflect.TypeOf(i)
    fmt.Println("i的类型是:", rType.Name()) // Student
    fmt.Println("i的种类是:", rType.Kind()) // struct
    // 获取i的值
    rValue := reflect.ValueOf(i)
    fmt.Println("i的值是:", rValue)
    // 获取i的字段信息
    for i := 0; i < rValue.NumField(); i++ {
        field := rType.Field(i)
        value := rValue.Field(i).Interface()
        fmt.Printf("字段名称:%s, 字段类型:%s, 字段值:%v\n", field.Name, field.Type, value)
    }
    // 获取i的方法信息
    for i := 0; i < rValue.NumMethod(); i++ {
        method := rType.Method(i)
        fmt.Printf("方法的名称:%s, 方法的类型:%s\n", method.Name, method.Type)

    }
}

运行结果:

i的类型是: Student
i的种类是: struct
i的值是: {tom 20 上海市}
字段名称:Name, 字段类型:string, 字段值:tom
字段名称:Age, 字段类型:int, 字段值:20
字段名称:Address, 字段类型:string, 字段值:上海市
方法的名称:PrintInfo, 方法的类型:func(main.Student)
方法的名称:Say, 方法的类型:func(main.Student, string)
2、通过reflect.Value设置实际变量的值

reflect.Value是通过reflect.ValueOf(X)获得的,只有当X是指针的时候,才可以通过reflec.Value修改实际变量X的值,即:要修改反射类型的对象就一定要保证其值是“addressable”的。

// Elem returns the value that the interface v contains
// or that the pointer v points to.
// It panics if v's Kind is not Interface or Ptr.
// It returns the zero Value if v is nil.
func (v Value) Elem() Value {}
// 翻译:
// Elem返回接口v包含的值或指针v指向的值。
// 如果v的类型不是interface或ptr,它会恐慌。
// 如果v为零,则返回零值。

示例代码:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var a int = 10
    rValue := reflect.ValueOf(&a)
    fmt.Println("是否可修改:", rValue.Elem().CanSet())
    rValue.Elem().SetInt(20)
    fmt.Println(a)
}

运行结果

是否可修改: true
20
3、通过reflect.Value来进行函数的调用

如果反射值对象(reflect.Value)中值的类型为函数时,可以通过 reflect.Value 调用该函数。使用反射调用函数时,需要将参数使用反射值对象的切片 []reflect.Value 构造后传入 Call() 方法中,调用完成时,函数的返回值通过 []reflect.Value 返回。

示例代码:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    rValue := reflect.ValueOf(add)
    res := rValue.Call([]reflect.Value{reflect.ValueOf(10), reflect.ValueOf(20)})
    fmt.Printf("%v, %T\n", res, res)
    fmt.Println(res[0].Int())
}

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

运行结果

[<int Value>], []reflect.Value
30

提示:

反射调用函数的过程需要构造大量的 reflect.Value 和中间变量,对函数参数值进行逐一检查,还需要将调用参数复制到调用函数的参数内存中。调用完毕后,还需要将返回值转换为 reflect.Value,用户还需要从中取出调用值。因此,反射调用函数的性能问题尤为突出,不建议大量使用反射函数调用。

4、通过反射,调用方法

调用方法和调用函数是一样的,只不过结构体需要先通过rValue.Method()先获取方法再调用。

示例代码:

package main

import (
    "fmt"
    "reflect"
)

// Student 学生结构体
type Student struct {
    Name    string
    Age     int
    Address string
}

// Say Student结构体的方法
func (s Student) Say(msg string) {
    fmt.Println(msg)
}

// PrintInfo Student结构体的方法
func (s Student) PrintInfo() {
    fmt.Printf("姓名:%s\t年龄:%d\t地址:%s\n", s.Name, s.Age, s.Address)
}

func main() {
    s := Student{"tom", 20, "上海市"}

    rValue := reflect.ValueOf(s)
    m1 := rValue.MethodByName("Say")
    m1.Call([]reflect.Value{reflect.ValueOf("hello world")})
    m2 := rValue.MethodByName("PrintInfo")
    m2.Call(nil)
}

运行结果

hello world
姓名:tom       年龄:20        地址:上海市
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342