• 欢迎访问开心洋葱网站,在线教程,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站,欢迎加入开心洋葱 QQ群
  • 为方便开心洋葱网用户,开心洋葱官网已经开启复制功能!
  • 欢迎访问开心洋葱网站,手机也能访问哦~欢迎加入开心洋葱多维思维学习平台 QQ群
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏开心洋葱吧~~~~~~~~~~~~~!
  • 由于近期流量激增,小站的ECS没能经的起亲们的访问,本站依然没有盈利,如果各位看如果觉着文字不错,还请看官给小站打个赏~~~~~~~~~~~~~!

golang反射规则使用详解

OC/C/C++ 水墨上仙 2639次浏览

在运行时反射是程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。它同时也是造成混淆的重要来源。在这篇文章中将试图明确解释在 Go 中的反射是如何工作的。每个语言的反射模型都不同(同时许多语言根本不支持反射)。不过这篇文章是关于 Go 的,因此接下来的内容“反射”这一词表示“在 Go 中的反射”。转自:http://www.mikespook.com/

第一次知道反射的时候还是许多年前在学校里玩&nbspC#&nbsp的时候。那时总是弄不清楚这个复杂的玩意能有什么实际用途……然后发现&nbspJava&nbsp有这个,后来发现&nbspPHP&nbsp也有了,再后来&nbspObjective-C、Python&nbsp什么的也都有……甚至连&nbspDelphi&nbsp也有&nbspTRttiContext……反射无处不在!!!Go&nbsp作为一个集大成的现代系统级语言,当然也需要有,必须的!大牛&nbspRob&nbspPike&nbsp的这篇文章相对全面的介绍了&nbspGo&nbsp语言中的反射的机制已经使用。觉得值得研读,于是翻译于此。———-翻译分割线———-反射的规则在运行时反射是程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。它同时也是造成混淆的重要来源。在这篇文章中将试图明确解释在&nbspGo&nbsp中的反射是如何工作的。每个语言的反射模型都不同(同时许多语言根本不支持反射)。不过这篇文章是关于&nbspGo&nbsp的,因此接下来的内容“反射”这一词表示“在&nbspGo&nbsp中的反射”。类型和接口由于反射构建于类型系统之上,就从复习一下&nbspGo&nbsp中的类型开始吧。Go&nbsp是静态类型的。每一个变量有一个静态的类型,也就是说,有一个已知类型并且在编译时就确定下来了:int,float32,*MyType,[]byte&nbsp等等。如果定义

type MyInt int
var i int
var j MyInt

那么&nbspi&nbsp的类型为&nbspint&nbsp而&nbspj&nbsp的类型为&nbspMyInt。即使变量&nbspi&nbsp和&nbspj&nbsp有相同的底层类型,它们仍然是有不同的静态类型的。未经转换是不能相互赋值的。在类型中有一个重要的类别就是接口类型,表达了固定的一个方法集合。一个接口变量可以存储任意实际值(非接口),只要这个值直线了接口的方法。众所周知的一个例子就是&nbspis&nbspio.Reader&nbsp和&nbspio.Writer,来自&nbspio&nbsp包的类型&nbspReader&nbsp和&nbspWriter:

// Reader 是包裹了基础的 Read 方法的接口。.
type Reader interface {
    Read(p []byte) (n int, err os.Error)
}
 
// Writer 是包裹了基础 Write 方法的接口。
type Writer interface {
    Write(p []byte) (n int, err os.Error)
}

任何用这个声明实现了&nbspRead(或&nbspWrite)方法的类型,可以说它实现了&nbspio.Reader(或&nbspio.Writer)。基于本讨论来说,这意味着&nbspio.Reader&nbsp类型的变量可以保存任意值,只要这个值的类型实现了&nbspRead&nbsp方法:

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

有一个事情是一定要明确的,不论&nbspr&nbsp保存了什么值,r&nbsp的类型总是&nbspio.Reader:Go&nbsp是静态类型,而&nbspr&nbsp的静态类型是&nbspio.Reader。接口类型的一个极端重要的例子是空接口:interface{}它表示空的方法集合,由于任何值都有另个或者多个方法,所以任何值都可以满足它。也有人说&nbspGo&nbsp的接口是动态类型的,不过这是一种误解。它们是静态类型的:接口类型的变量总是有着相同的静态类型,这个值总是满足空接口,只是存储在接口变量中的值运行时也有可能被改变类型。对于所有这些都必须严谨的对待,因为反射和接口密切相关。接口的特色Russ&nbspCox&nbsp已经写了一篇详细介绍&nbspGo&nbsp中接口值的特点的博文。所以无需在这里重复整个故事了,不过简单的总结还是必要的。接口类型的变量存储了两个内容:赋值给变量实际的值和这个值的类型描述。更准确的说,值是底层实现了接口的实际数据项目,而类型描述了这个项目完整的类型。例如下面,

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

用模式的形式来表达&nbspr&nbsp包含了的是&nbsp(value,&nbsptype)&nbsp对,如&nbsp(tty,&nbsp*os.File)。注意类型&nbsp*os.File&nbsp除了&nbspRead&nbsp方法还实现了其他方法:尽管接口值仅仅提供了访问&nbspRead&nbsp方法的可能,但是内部包含了这个值的完整的类型信息。这也就是为什么可以这样做:

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

在这个赋值中的断言是一个类型断言:它断言了&nbspr&nbsp内部的条目同时也实现了&nbspio.Writer,因此可以赋值它到&nbspw。在赋值之后,w&nbsp将会包含&nbsp(tty,&nbsp*os.File)。跟在&nbspr&nbsp中保存的一致。接口的静态类型决定了哪个方法可以通过接口变量调用,即便内部实际的值可能有一个更大的方法集。接下来,可以这样做:

var empty interface{}
empty = w

而空接口值&nbspe&nbsp也将包含同样的&nbsp(tty,&nbsp*os.File)。这很方便:空接口可以保存任何值同时保留关于那个值的所有信息。(这里无需类型断言,因为&nbspw&nbsp是肯定满足空接口的。在这个例子中,将一个值从&nbspReader&nbsp变为&nbspWriter,由于&nbspWriter&nbsp的方法不是&nbspReader&nbsp的子集,所以就必须明确使用类型断言。)一个很重要的细节是接口内部的对总是&nbsp(value,&nbsp实际类型)&nbsp的格式,而不会有&nbsp(value,&nbsp接口类型)&nbsp的格式。接口不能保存接口值。现在准备好来反射了。反射的第一条规则1.&nbsp从接口值到反射对象的反射。在基本的层面上,反射只是一个检查存储在接口变量中的类型和值的算法。从头来讲,在&nbspreflect&nbsp包中有两个类型需要了解:Type&nbsp和&nbspValue。这两个类型使得可以访问接口变量的内容,还有两个简单的函数,reflect.TypeOf&nbsp和&nbspreflect.ValueOf,从接口值中分别获取&nbspreflect.Type&nbsp和&nbspreflect.Value。(同样,从&nbspreflect.Value&nbsp也很容易能够获得&nbspreflect.Type,不过这里让&nbspValue&nbsp和&nbspType&nbsp在概念上分离了。)从&nbspTypeOf&nbsp开始:

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

这个程序打印type:&nbspfloat64接口在哪里呢,读者可能会对此有疑虑,看起来程序传递了一个&nbspfloat64&nbsp类型的变量&nbspx,而不是一个接口值,到&nbspreflect.TypeOf。但是,它确实就在那里:如同&nbspgodoc&nbsp报告的那样,reflect.TypeOf&nbsp的声明包含了空接口://&nbspTypeOf&nbsp返回&nbspinterface{}&nbsp中的值反射的类型。func&nbspTypeOf(i&nbspinterface{})&nbspType当调用&nbspreflect.TypeOf(x)&nbsp的时候,x&nbsp首先存储于一个作为参数传递的空接口中;reflect.TypeOf&nbsp解包这个空接口来还原类型信息。reflect.ValueOf&nbsp函数,当然就是还原那个值(从这里开始将会略过那些概念示例,而聚焦于可执行的代码):

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

打印value:&nbspreflect.Type&nbsp和&nbspreflect.Value&nbsp都有许多方法用于检查和操作它们。一个重要的例子是&nbspValue&nbsp有一个&nbspType&nbsp方法返回&nbspreflect.Value&nbsp的&nbspType。另一个是&nbspType&nbsp和&nbspValue&nbsp都有&nbspKind&nbsp方法返回一个常量来表示类型:Uint、Float64、Slice&nbsp等等。同样&nbspValue&nbsp有叫做&nbspInt&nbsp和&nbspFloat&nbsp的方法可以获取存储在内部的值(跟&nbspint64&nbsp和&nbspfloat64&nbsp一样):

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:&nbspfloat64kind&nbspis&nbspfloat64:&nbsptruevalue:&nbsp3.4同时也有类似&nbspSetInt&nbsp和&nbspSetFloat&nbsp的方法,不过在使用它们之前需要理解可设置性,这部分的主题在下面的第三条军规中讨论。反射库有着若干特性值得特别说明。首先,为了保持&nbspAPI&nbsp的简洁,“获取者”和“设置者”用&nbspValue&nbsp的最宽泛的类型来处理值:例如,int64&nbsp可用于所有带符号整数。也就是说&nbspValue&nbsp的&nbspInt&nbsp方法返回一个&nbspint64,而&nbspSetInt&nbsp值接受一个&nbspint64;所以可能必须转换到实际的类型:

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 返回一个 uint64.

第二个特性是反射对象的&nbspKind&nbsp描述了底层类型,而不是静态类型。如果一个反射对象包含了用户定义的整数类型的值,就像

type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)

v&nbsp的&nbspKind&nbsp仍然是&nbspreflect.Int,尽管&nbspx&nbsp的静态类型是&nbspMyInt,而不是&nbspint。换句话说,Kind&nbsp无法从&nbspMyInt&nbsp中区分&nbspint,而&nbspType&nbsp可以。反射的第二条规则2.&nbsp从反射对象到接口值的反射。如同物理中的反射,在&nbspGo&nbsp中的反射也存在它自己的镜像。从&nbspreflect.Value&nbsp可以使用&nbspInterface&nbsp方法还原接口值;方法高效的打包类型和值信息到接口表达中,并返回这个结果:

// Interface 以 interface{} 返回 v 的值。
func (v Value) Interface() interface{}

可以这样作为结果

y := v.Interface().(float64) // y 将为类型 float64。
fmt.Println(y)

通过反射对象&nbspv&nbsp可以打印&nbspfloat64&nbsp的表达值。然而,还可以做得更好。fmt.Println,fmt.Printf&nbsp和其他所有传递一个空接口值作为参数的,由&nbspfmt&nbsp包在内部解包的方式就像之前的例子这样。因此正确的打印&nbspreflect.Value&nbsp的内容的方法就是将&nbspInterface&nbsp方法的结果传递给格式化打印:formatted&nbspprint&nbsproutine:

fmt.Println(v.Interface())

(为什么不是&nbspfmt.Println(v)?因为&nbspv&nbsp是一个&nbspreflect.Value;这里希望是它保存的实际的值。)由于值是&nbspfloat64,如果需要的话,甚至可以使用浮点格式化:

fmt.Printf("value is %7.1e\n", v.Interface())

然后就得到这个3.4e+00再次强调,对于&nbspv.Interface()&nbsp无需类型断言其为&nbspfloat64;空接口值在内部有实际值的类型信息,而&nbspPrintf&nbsp会发现它。简单来说,Interface&nbsp方法是&nbspValueOf&nbsp函数的镜像,除了返回值总是静态类型&nbspinterface{}。回顾:反射可以从接口值到反射对象,也可以反过来。反射的第三条规则3.&nbsp为了修改反射对象,其值必须可设置。第三条军规是最为精细和迷惑的,但是如果从第一个规则开始,还是足以让人明白的。这里有一些不能工作的代码,值得学习。

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

如果运行这个代码,它报出神秘的&nbsppanic&nbsp消息panic:&nbspreflect.Value.SetFloat&nbspusing&nbspunaddressable&nbspvalue问题不在于值&nbsp7.1&nbsp不能地址化;在于&nbspv&nbsp不可设置。设置性是反射值的一个属性,并不是所有的反射值有它。值的&nbspCanSet&nbsp方法提供了值的设置性;在这个例子中,

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

打印settability&nbspof&nbspv:&nbspfalse对不可设置值调用&nbspSet&nbsp方法会有错误。但是什么是设置性?设置性有一点点像地址化,但是更严格。这是用于创建反射对象的时候,能够修改实际存储的属性。设置性用于决定反射对象是否保存原始项目。当这样

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

就传递了一个&nbspx&nbsp的副本到&nbspreflect.ValueOf,所以接口值作为&nbspreflect.ValueOf&nbsp参数创建了&nbspx&nbsp的副本,而不是&nbspx&nbsp本身。因此,如果语句

v.SetFloat(7.1)

允许执行,虽然&nbspv&nbsp看起来是从&nbspx&nbsp创建的,它也无法更新&nbspx。反之,如果在反射值内部允许更新&nbspx&nbsp的副本,那么&nbspx&nbsp本身不会收到影响。这会造成混淆,并且毫无意义,因此这是非法的,而设置性是用于解决这个问题的属性。这很神奇?其实不是。这实际上是一个常见的非同寻常的情况。考虑传递&nbspx&nbsp到函数:f(x)由于传递的是&nbspx&nbsp的值的副本,而不是&nbspx&nbsp本身,所以并不期望&nbspf&nbsp可以修改&nbspx。如果想要&nbspf&nbsp直接修改&nbspx,必须向函数传递&nbspx&nbsp的地址(也就是,指向&nbspx&nbsp的指针):f(&x)这是清晰且熟悉的,而反射通过同样的途径工作。如果希望通过反射来修改&nbspx,必须向反射库提供一个希望修改的值的指针。来试试吧。首先像平常那样初始化&nbspx,然后创建指向它的反射值,叫做&nbspp。

var x float64 = 3.4
p := reflect.ValueOf(&x) // 注意:获取 X 的地址。
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:" , p.CanSet())

这样输出为

type of p: *float64
settability of p: false

反射对象&nbspp&nbsp并不是可设置的,但是并不希望设置&nbspp,(实际上)是&nbsp*p。为了获得&nbspp&nbsp指向的内容,调用值上的&nbspElem&nbsp方法,从指针间接指向,然后保存反射值的结果叫做&nbspv:

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

现在&nbspv&nbsp是可设置的反射对象,如同示例的输出,

settability of v: true

而由于它来自&nbspx,最终可以使用&nbspv.SetFloat&nbsp来修改&nbspx&nbsp的值:

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

得到期望的输出

7.1
7.1

反射可能很难理解,但是语言做了它应该做的,尽管底层的实现被反射的&nbspType&nbsp和&nbspValue&nbsp隐藏了。务必记得反射值需要某些内容的地址来修改它指向的东西。结构体在之前的例子中&nbspv&nbsp本身不是指针,它只是从一个指针中获取的。这种情况更加常见的是当使用反射修改结构体的字段的时候。也就是当有结构体的地址的时候,可以修改它的字段。这里有一个分析结构值&nbspt&nbsp的简单例子。由于希望等下对结构体进行修改,所以从它的地址创建了反射对象。设置了&nbsptypeOfT&nbsp为其类型,然后用直白的方法调用来遍历其字段(参考&nbspreflect&nbsp包了解更多信息)。注意从结构类型中解析了字段名字,但是字段本身是原始的&nbspreflect.Value&nbsp对象。

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 = %v\n", i,
        typeOfT.Field(i).Name, f.Type(), f.Interface())
}

这个程序的输出是

0: A int = 23
1: B string = skidoo

这里还有一个关于设置性的要点:T&nbsp的字段名要大写(可导出),因为只有可导出的字段是可设置的。由于&nbsps&nbsp包含可设置的反射对象,所以可以修改结构体的字段。

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

这里是结果:

t is now {77 Sunset Strip}

如果修改程序使得&nbsps&nbsp创建于&nbspt,而不是&nbsp&t,调用&nbspSetInt&nbsp和&nbspSetString&nbsp会失败,因为&nbspt&nbsp的字段不可设置。总结再次提示,反射的规则如下:从接口值到反射对象的反射。从反射对象到接口值的反射。为了修改反射对象,其值必须可设置。一旦理解了&nbspGo&nbsp中的反射的这些规则,就会变得容易使用了,虽然它仍然很微妙。这是一个强大的工具,除非真得有必要,否则应当避免使用或小心使用。还有大量的关于反射的内容没有涉及到——channel&nbsp上的发送和接收、分配内存、使用&nbspslice&nbsp和&nbspmap、调用方法和函数——但是这篇文章已经够长了。这些话题将会在以后的文章中逐一讲解。


开心洋葱 , 版权所有丨如未注明 , 均为原创丨未经授权请勿修改 , 转载请注明golang反射规则使用详解
喜欢 (0)
加载中……