加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 大数据 > 正文

golang reflection

发布时间:2020-12-16 09:42:55 所属栏目:大数据 来源:网络整理
导读:介绍 反射是通过types能够查看类型本身的结构,本文档将会介绍Go语言中反射是如何工作的. 类型和接口 因为反射是建立在类型type的基础之上的,首先我们来回顾一下Go语言中的类型types,Go是一个静态类型的语言,每个变量都有一个静态的类型(在编译的时候已经能够

介绍

反射是通过types能够查看类型本身的结构,本文档将会介绍Go语言中反射是如何工作的.

类型和接口

因为反射是建立在类型type的基础之上的,首先我们来回顾一下Go语言中的类型types,Go是一个静态类型的语言,每个变量都有一个静态的类型(在编译的时候已经能够确定变量的类型),比如int,float32,*MyType,[]byte等等,如果我们声明

type MyInt int

var i int
var j MyInt

这里例子中i是int类型,j是MyInt类型,i和j是不同的类型,虽然他们底层的类型是一样的,但是我们没有去进行直接的赋值.

类型分类里面有一个重要的类型是interface,interface表示包含了一系列的方法集.一个interface变量可以存储实体的值,这个值只需要实现interface中定义的方法就可以.一个比较常见的例子是io.Reader和io.Writer,Reader和Writer类型来自于golang中的io包:

// Reader is the interface that wraps the basic Read method.
type Reader interface {
    Read(p []byte) (n int,err error)
}

// Writer is the interface that wraps the basic Write method.
type Writer interface {
    Write(p []byte) (n int,err error)
}

任何类型只要包含了Read(Writer)就可以说实现了io.Reader(io.Writer).这样io.Reader类型的变量可以用来存储包含Read方法的变量:

var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on

这里需要强调的是无论r的包含的实际的变量是什么,r的类型总是io.Reader.

一个比较的interface类型是空接口:

interface{}

它表示一个空的方法集合,因为任何值都是0个或多个方法,所以任何其他类型的值都是能够满足条件的

有一些人说Go的interface是动态类型的,但是那个是错误的,interface仍然是静态类型的:一个interface类型的变量总是有一个静态的类型,虽然在实际运行的时候interface变量中存储的实际的值可能是变化的类型,但是这个值必须是满足interface类型要求的.

我们需要准确的理解这些内容,因为反射(reflection)和接口(interfaces)是紧密相关的.

interface的概念
interface的变量存储了实际赋值给该变量的值和该值的类型描述子的信息.更准确的说,值是实现了该interface接口的实际的变量的内容,类型描述子是该值完整的类型信息.这里有一个例子

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

r包含一对(value,type)信息,也就是例子中的(tty,*os.File). 这里注意*os.File实现了Read以及其他的方法,但是这个interface的变量只提供了对于Read的访问,value中包含了该值所有的类型信息,也就是为什么我们可以像这样去执行其他的函数

var w io.Writer
w = r.(io.Writer)

这里是一个类型断言,表示它会断言r中的内容是否实现了io.Writer接口,这样我们就可以将其赋值给w.通过这个赋值之后,w中将会包含(tty,*osFile).这个内容跟r所包含的信息是一样的.接口的静态类型决定了这个接口变量可以调用那些函数,即使可能接口中对应的实际的值包含了更多的方法.

我们同样可以这样:

var empty interface{}
empty = w

这里空的interface的emtpy也会包含(tty,*os.File)的内容.那是说:一个空的接口可以包含任何的值,并且该接口变量包含了其他对应的value的所有的信息.

一个重要的细节是interface变量中包含了(value,concrete type)[(赋值的变量,值实际的类型)]而不是(value,interface type)[(赋值的变量,接口的类型)]

反射

下面我们说一下反射的细节

第一个准则

  1. 反射实现从interface的值到反射对象

最根本的,反射是一种检查interface变量中存储的type以及值的一种机制.在开始之前,需要了解一下reflect包中的两个类型:Type和Value,这两种类型提供了访问变量内部内容的方式,同时也提供了两个简单的方法,分别是reflect.TypeOf和reflect.ValueOf,通过这两个函数可以获取接口中包含的值的内容.
首先我们介绍TypeOf:

package main

import (
    "fmt"
    "reflect"
)

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

程序输出

type: float64
你可以有疑问这个例子中interface在什么地方,因为程序看起来只是传递了float64类型的变量x,而不是一个interface的接口给reflect.TypeOf.但是正如godoc在定义的,reflect.TypeOf()包含了一个空的interface:

// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type

当我们调用reflect.TypeOf(x)的时候,x被存储到empty的interface中,reflect.TypeOf会解开empty interface,之后恢复出来type类型信息

当然了,reflect.ValueOf()将会恢复value的值的信息:

var x float64 = 3.4
fmt.Println("value:",reflect.ValueOf(x).String())
prints

value:

(这里我们显式的的调用String()是fmt包默认情况下会解开reflect.Value显示实际的value的内容,但是通过String方法就不会了)

reflect.Type和reflect.Value有很多其他的函数来让你检查和控制它们.一个重要的例子是reflect.Value有一个Type函数,它将会返回reflect.Value对应的Type类型是什么. 另外一个就是Type和Value类型都有一个Kind方法,他将会返回一个常量,用来表示内部存储了什么类型,比如Uint,Float64,Slice以及其他的内容.其中值的方法Int/Float也会被包含到值中进行存储:

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:",v.Type())
fmt.Println("kind is float64:",v.Kind() == reflect.Float64)
fmt.Println("value:",v.Float())

输出
type: float64
kind is float64: true
value: 3.4

像SetInt和SetFloat的方法在使用的时候需要理解它的可设置性

reflection的库有一些属性这里需要指出的.首先,为了保证简化API,值的getter和setter方法针对最大类型的,例如对于所有有符号整数都是int64的,就是说值的Int()将会返回int64,SetInt()需要传入int64位的. 有的时候我们会涉及到需要转化成实际的值:

var x uint8 = 'x'
v := reflect.ValueOf(x)
fmt.Println("type:",v.Type())                            // uint8.
fmt.Println("kind is uint8: ",v.Kind() == reflect.Uint8) // true.
x = uint8(v.Uint())                                       // v.Uint returns a uint64.

第二个属性是一个反射对象的Kind()用来描述包含值的真实类型,而不是interface对应的静态类型.如果一个反射对象包含了一个用户自定义整型类型,如下面的实验中那样

type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
v的Kind仍然是reflect.Int,而不是x对应的静态类型MyInt.换句话说,虽然Type能够区分,但是Kind不能用来从MyInt中区分int,

第二个准则

  1. 反射实现从反射对象到interface中值
  2. Reflection goes from reflection object to interface value.

就像物理上的反射,Go中的反射产生自身的反面
Like physical reflection,reflection in Go generates its own inverse.

给定一个reflect.Value,我们可以使用接口的方法来恢复接口的值;效果上这个方法包装类型和值的信息一个接口的描述并且返回它的结果:

// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}
因此我们可以

y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)

通过v的反射来打印float64的值

不过,我们可以做得更好。 fmt.Println,fmt.Printf等等的参数都是作为空的接口值传递的,然后由内部的fmt包解压缩,就像我们在前面的例子中所做的那样。 因此,只需正确地打印reflect.Value的内容即可将Interface方法的结果传递给格式化的打印例程:

fmt.Println(v.Interface())

(为什么我们不采用fmt.Println(v)?因为v是一个反射的值;我们想要的是实际包含的值).因为我们的值是一个float64,我们甚至可以使用浮点数的打印类型:

fmt.Printf("value is %7.1en",v.Interface())

来获得打印的内容

3.4e+00
同时,这里没有必要去断言v.Interface的结构是否是float64;空的interfacen内部含有实际包含值的类型信息,Printf将会恢复它.

反射的第三个准则

3.为了修改反射的对象,值必须是可设置的
第三个准则是很迷惑人的,但是如果你从第一个准则来看就相对够简单了
下面是一个不能运行的代码,但是也是很有研究价值的.

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.

如果你运行这个代码,那么它将会Panic.
panic: reflect.Value.SetFloat using unaddressable value
这个文件是值7.1是无法寻址的,也就是说v是不可设置的.Settability是反射的一个属性,但是不是所有的反射的值都有它.
Value的CanSet()方法可以返回Value值的可设置属性;就像我们的例子一样,

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:",v.CanSet())

输出

settability of v: false
在一个不可以设置的Value上执行Set()将会发生错误,但是什么是可设置属性了?
可设置属性有一点类似可寻址,可设置属性是指一个反射对象可以修改用来创建反射读对象时候实际的存储.通过是否反射对象是否可以来决定可设置属性,当我们说

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

我们传递x的副本给reflect.ValueOf,就像接口的值想一个参数传递给reflect.Value,实际上是x的一个副本,而不是x本身,如果执行下面的表达式

v.SetFloat(7.1)

是被允许执行成功的,虽然v看上去是通过x来创建的,但是它将不会去更新x,而是,它会去更新存储到反射Value中的副本,但是x本身是不会收到影响的.
这点是迷惑和无用的,所以它是不合理,通过可设置这个属性可以避免这个问题.

这个看上去很奇怪,但是并不是这样的.想想我们传递x给一个函数:
f(x)
我们并不期望去修改x,因为我们传递的是x的一个副本,而不是x本身.如果你想直接修改x的内容,那么我们将会传递x的地址
f(&x)
这个是直接而且是很常见的,其实反射是类似的工作.如果你想要通过反射来修改x,那么我们必须给reflection 库一个指针来指向我们想要修改的值value.

让我们来验证一下,首先我们初始化x,然后创建一个反射的value来指向它,我们称之为p

var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:",p.Type())
fmt.Println("settability of p:",p.CanSet())

执行输出如下所示

type of p: *float64
settability of p: false

反射对象p不可设置,但它不是我们想设置的,它是* p。 为了得到什么指向,我们称之为Value的Elem方法,它通过指针进行间接寻址,并将结果保存在一个名为v的反射值中:

v := p.Elem()
fmt.Println("settability of v:",v.CanSet())

这里v是一个可设置的反射对象

v的可设置属性为true.
因为它表示x,我们最终能够使用v.SetFloat来修改x的值的内容

v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

输出

7.1
7.1

Reflection可能很难理解,但它正在做语言所做的事情,尽管反射的Type和Value可以掩盖正在发生的事情。 只要记住反射的值想要修改那么它必须需要是可寻址的.

Structs
结构体

在我们前面的例子中,v不是一个指针本身,它只是从一个派生而来的。 出现这种情况的一种常见方式是使用反射来修改结构的字段。 只要我们有结构的地址,我们可以修改它的字段。

下面是一个简单的例子,分析一个结构值t。 我们用结构体的地址创建反射对象,因为我们稍后要修改它。 然后,我们将typeOfT设置为其类型,并使用直接的方法调用遍历字段(请参阅package reflect了解详细信息)。 请注意,我们从结构类型中提取字段的名称,但是字段本身是常规reflect.Value对象。

type T struct {
    A int
    B string
}
t := T{23,"skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
    f := s.Field(i)
    fmt.Printf("%d: %s %s = %vn",i,typeOfT.Field(i).Name,f.Type(),f.Interface())
}

程序输出如下

0: A int = 23
1: B string = skidoo
在这里介绍的可设置性还有一点:T的字段名是大写字母(导出),因为只有结构体的导出字段是可设置的。

因为s包含了一个可设置的反射对象,我们可以修改该结构体的字段

s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now",t)

它的输出如下

t is now {77 Sunset Strip}
如果我们修改程序使得s是从t创建的,而不是&t,那么对SetInt和SetString的调用将失败,因为t的字段将不可设置。

结论

这里也是反射的准则:

  • 反射从接口值到反射对象。
  • 反射从反射对象到接口的值。
  • 要修改反射对象,该值必须是可设置的。
    一旦你理解了这些规则,Go中的反射变得更容易使用,尽管它仍然微妙。 这是一个强大的工具,应该小心使用和避免,除非需要。

还有很多值得我们反思的是,我们没有涉及通道上发送和接收,分配内存,使用切片和map,调用方法和函数。 我们将在后面的文章中介绍其中的一些主题。

by Rob Pike
[1] https://blog.golang.org/laws-of-reflection

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读