Golang基础(5):Go语言反射规则
Go语言反射规则 - The Laws of Reflection
转:http://my.oschina.net/qbit/blog/213720
原文地址:http://blog.golang.org/laws-of-reflection
介绍
反射在计算机的概念里是指一段程序审查自身结构的能力,主要通过类型进行审查。它是元编程的一种形式,同样也是引起混乱的重大来源。
在这篇文章里我们试图阐明Go语言中的反射是如何工作的。每种语言的反射模型是不同的(许多语言不支持反射),然而本文只与Go有关,所以我们接下来所提到的“反射”都是指Go语言中的反射。
类型与接口
由于反射是建立在类型系统(type system)上的,所以我们先来复习一下Go语言中的类型。
Go是一门静态类型的语言。每个变量都有一个静态类型,类型在编译的时后被知晓并确定了下来。
1
2
3
4
|
type MyInt int var i int var j MyInt |
变量i
的类型是int
,变量j
的类型是MyInt
。虽然它们有着相同的基本类型,但静态类型却不一样,在没有类型转换的情况下,它们之间无法互相赋值。
接口是一个重要的类型,它意味着一个确定的的方法集合。一个接口变量可以存储任何实现了接口的方法的具体值(除了接口本身)。一个著名的例子就是io.Reader
和io.Writer
:
1
2
3
4
5
6
7
8
9
|
// 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) } |
如果一个类型声明实现了Reader
(或Writer
)方法,那么它便实现了io.Reader
(或io.Writer
)。这意味着一个io.Reader
的变量可以持有任何一个实现了Read
方法的的类型的值。
1
2
3
4
5
|
var r io.Reader r = os.Stdin r = bufio.NewReader(r) r = new (bytes.Buffer) // and so on |
必须要弄清楚的一点是,不管变量r
中的具体值是什么,r
的类型永远是io.Reader
:Go是静态类型的,r的静态类型就是io.Reader
。
在接口类型中有一个极为重要的例子——空接口:
1
|
interface {} |
它表示了一个空的方法集,一切值都可以满足它,因为它们都有零值或方法。
有人说Go的接口是动态类型,这是错误的。它们都是静态类型:虽然在运行时中,接口变量存储的值也许会变,但接口变量的类型是永不会变的。我们必须精确地了解这些,因为反射与接口是密切相关的。
深入接口
Russ Cox在博客里写了一篇详细的文章,讲述了Go中的接口变量的意义。我们不需要列出全文,只需在这里给出一点点总结。
一个接口类型的变量里有两样东西:变量的的具体值和这个值的类型描述。更准确地来讲,这个实现了接口的值是一个基础的具体数据项,而类型描述了数据项里的所有类型。
如下所示:
1
2
3
4
5
6
|
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
以外的方法;虽然接口值只提供了Read
方法,但它内置了所有的类型信息,这就是为什么我们可以么做:
1
2
|
var w io.Writer w = r.(io.Writer) |
上面的所展示表达式是一个类型断言,它断言了r
中所包含的数据项实现了io.Writer
,所以我们可以用它对w
赋值。在此之后,w
将与r
一样,包含(tty, *os.File)
组合。接口的静态类型决定了接口变量的哪些方法会被调用,即便也许它所含的具体值有一个更大的方法集。
接下来,我们可以这么做:
1
2
|
var empty interface {} empty = w |
我们的空接口变量将会在此包含同样的“组合”:(tty, *os.File)
。这非常方便:一个空接口可以包含任何值和它的类型信息,我们可以在任何需要的时候了解它。
(在这里我们无需类型断言是因为w
已经满足了空接口。在前面的例子中我们将一个值从一个Reader
传到了Writer
,因为Writer
不是Reader
的子集,所以我们需要使用类型断言。)
这里有一个重要细节:接口里“组合”的格式永远是(值,实体类型),而不是(值,接口类型)。接口不会包含接口值。
好了,现在让我们进入反射部分。
反射规则(一) - 从接口到反射对象
在此基础上,反射是一个审查在接口变量中的(type, value)
组合的机制。现在,我们需要了解reflect包中的两个类型:Type
和Value
,可以让我们访问接口变量的内容。reflect.TypeOf
函数和reflect.ValueOf
函数返回的reflect.Type
和reflect.Value
可以拼凑出一个接口值。(当然,从reflect.Value
可以很轻易地得到reflect.Type
,但现在还是让我们把Value
和Type
的概念分开来看。)
我们从TypeOf
开始:
1
2
3
4
5
6
7
8
9
10
11
|
package main import ( "fmt" "reflect" ) func main() { var x float64 = 3.4 fmt.Println( "type:" , reflect.TypeOf(x)) } |
这个程序打印了:
1
|
type: float64 |
看了这段代码你也许会想“接口在哪?”,这段程序里只有float64
的变量x
,并没有接口变量传进reflect.TypeOf
。其实它是在这儿:在godoc reports的reflect.TypeOf
的声明中包含了一个空接口:
1
2
|
// TypeOf returns the reflection Type of the value in the interface{}. func TypeOf(i interface {}) Type |
当我们调用reflect.TypeOf(x)
时,作为参数传入的x
在此之前已被存进了一个空接口。而reflect.TypeOf
解包了空接口,恢复了它所含的类型信息。
相对的,reflect.ValueOf
函数则是恢复了值(从这里开始我们将修改例子并且只关注于可执行代码):
1
2
|
var x float64 = 3.4 fmt.Println( "value:" , reflect.ValueOf(x)) |
打印:
1
|
value: <float64 Value> |
reflect.Type
和reflect.Value
拥有许多方法让我们可以审查和操作接口变量。一个重要的例子就是Value
有一个Type
方法返回reflect.Value
的Type
。另一个例子就是,Type
和Value
都有Kind
方法,它返回一个常量,这个常量表示了被存储的元素的排列顺序:Uint, Float64, Slice
等等。并且,Value
的一系列方法(如Int
或Float
),能让我们获取被存储的值(如int64
或float64
):
1
2
3
4
5
|
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()) |
打印:
1
2
3
|
type: float64 kind is float64: true value: 3.4 |
有一些方法如SetInt
和SetFloat
涉及到了“可设置”(settability)的概念,这是反射规则的第三条,我们将在后面讨论。
反射库有两个特性是需要指出的。其一,为了保持API的简洁,Value
的Getter和Setter方法是用最大的类型去操作数据:例如让所有的整型都使用int64
表示。所以,Value
的Int
方法返回一个int64
的值,SetInt
需要传入int64
参数;将数值转换成它的实际类型在某些时候是有必要的:
1
2
3
4
5
|
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
方法描述的是基础类型,而不是静态类型。如果一个反射对象包含了用户定义类型的值,如下:
1
2
3
|
type MyInt int var x MyInt = 7 v := reflect.ValueOf(x) |
虽然x
的静态类型是MyInt
而非int
,但v
的Kind
依然是reflect.Int
。虽然Type
可以区分开int
和MyInt
,但Kind
无法做到。
反射规则(二) - 从反射对象到接口
如同物理学中的反射一样,Go语言的反射也是可逆的。
通过一个reflect.Value
我们可以使用Interface
方法恢复一个接口;这个方法将类型和值信息打包成一个接口并将其返回:
1
2
|
// Interface returns v's value as an interface{}. func (v Value) Interface() interface {} |
于是我们得到一个结果:
1
2
|
y := v.Interface().(float64) // y will have type float64. fmt.Println(y) |
以上代码会打印由反射对象v
表现出的float64
值。
然而,我们还可以做得更好。fmt.Println
和fmt.Printf
的参数都是通过interface{}传入的,传入之后由fmt
的私有方法解包(就像我们前面的例子所做的一样)。正是因为fmt
把Interface
方法的返回结果传递给了格式化打印事务(formatted print routine),所以程序才能正确打印出reflect.Value
的内容:
1
|
fmt.Println(v.Interface()) |
(为什么不是fmt.Println(v)
?因为v是一个reflect.Value
,而我们想要的是它的具体值)
由于值的类型是float64
,我们可以用浮点格式化打印它:
1
|
fmt.Printf( "value is %7.1e\n" , v.Interface()) |
并得出结果:
1
|
3 .4e+ 00 |
在这里我们无需对v.Interface()
做类型断言,这个空接口值包含了具体的值的类型信息,Printf
会恢复它。
简而言之,Interface
方法就是ValueOf
函数的逆,除非ValueOf
所得结果的类型是interface{}
重申一遍:反射从接口中来,经过反射对象,又回到了接口中去。
(Reflection goes from interface values to reflection objects and back again.)
反射规则(三) - 若要修改反射对象,值必须可设置
第三条规则是最微妙同时也是最混乱的,但如果我们从它的基本原理开始,那么一切都不在话下。
以下的代码虽然无法运行,但值得学习:
1
2
3
|
var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat( 7.1 ) // Error: will panic. |
如果你运行这些代码,它会panic这些神秘信息:
1
|
panic: reflect.Value.SetFloat using unaddressable value |
问题在于7.1
是不可寻址的,这意味着v
就会变得不可设置。“可设置”(settability)是reflect.Value
的特性之一,但并非所有的Value
都是可设置的。
Value
的CanSet
方法返回一个布尔值,表示这个Value
是否可设置:
1
2
3
|
var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println( "settability of v:" , v.CanSet()) |
打印:
1
|
settability of v: false |
对一个不可设置的Value
调用的Set
方法是错误的。那么,什么是“可设置”?
“可设置”和“可寻址”(addressable)有些类似,但更严格。一个反射对象可以对创建它的实际内容进行修改,这就是“可设置”。反射对象的“可设置性”由它是否拥有原项目(orginal item)所决定。
当我们这样做的时候:
1
2
|
var x float64 = 3.4 v := reflect.ValueOf(x) |
我们传递了一份x
的拷贝到reflect.ValueOf
中,所以传到reflect.ValueOf
的接口值不是由x
,而是由x
的拷贝创建的。因此,如果下列语句
1
|
v.SetFloat( 7.1 ) |
被允许执行成功,它将不会更新x
,即使看上去v
是由x
创建的。相反,它更新的是存于反射值中的x
拷贝,x
本身将不会受到影响。这是混乱且毫无用处的,所以这么做是非法的。“可设置”作为反射的特性之一就是为了预防这样的情况。
这虽然看起来怪异,但事实恰好相反。它实际上是一个我们很熟悉的情形,只是披上了一件不寻常的外衣。思考一下x
是如何传递到一个函数里的:
1
|
f(x) |
我们不会指望f
能够修改x
因为我们传递的是一个x
的拷贝,而非x
。如果我们想让f
直接修改x
我们必须给我们的函数传入x
的地址(即是x
的指针):
1
|
f(&x) |
这是直接且熟悉的,反射的工作方式也与此相同。如果我们想用反射修改x
,我们必须把值的指针传给反射库。
开始吧。首先我们像刚才一样初始化x
,然后创建一个指向它的反射值,命名为p
:
1
2
3
4
|
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()) |
目前的输出是:
1
2
|
type of p: *float64 settability of p: false |
反射对象p
不是可设置的,但我们想要设置的不是它,而是*p
。
为了知道p
指向了哪,我们调用Value
的Elem
方法,它通过指针定向并把结果保存在了一个Value
中,命名为v
:
1
2
|
v := p.Elem() fmt.Println( "settability of v:" , v.CanSet()) |
现在的v
是一个可设置的反射对象,如下所示:
1
|
settability of v: true |
因为它表示x
,我们终于可以用v.SetFloat
来修改x
的值了:
1
2
3
|
v.SetFloat( 7.1 ) fmt.Println(v.Interface()) fmt.Println(x) |
正如意料中的一样:
1
2
|
7.1 7.1 |
反射可能很难理解,但它所做的事正是编程语言所做的,尽管通过反射类型和值可以掩饰正在发生的事。
记住,用反射修改数据的时候需要传入它的指针哦。
结构体
在前面的例子中,v
并不是指针本身,它只是来源于此。
我们一般在使用反射去修改结构体字段的时候会用到。只要我们有结构体的指针,我们就可以修改它的字段。
这里有一个解析结构体变量t
的例子。我们用结构体的地址创建了反射变量,待会儿我们要修改它。然后我们对它的类型设置了typeOfT
,并用调用简单的方法迭代字段(详情请见reflect包)。
注意,我们从结构体的类型中提取了字段的名字,但每个字段本身是正常的reflect.Value
对象。
1
2
3
4
5
6
7
8
9
10
11
12
|
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()) } |
程序输出:
1
2
|
0 : A int = 23 1 : B string = skidoo |
关于可设置性还有一点需要介绍:T
的字段名是大写(字段可导出/公共字段)的原因在于,结构体中只有可导出的的字段是“可设置”的。
因为s
包含了一个可设置的反射对象,我们可以修改结构体字段:
1
2
3
|
s.Field( 0 ).SetInt( 77 ) s.Field( 1 ).SetString( "Sunset Strip" ) fmt.Println( "t is now" , t) |
结果:
1
|
t is now { 77 Sunset Strip} |
如果我们修改了程序让s
由t
(而不是&t
)创建,程序就会在调用SetInt
和SetString
的地方失败,因为t
的字段是不可设置的。
结论
再次列出反射法则:
- 反射从接口值到反射对象中(Reflection goes from interface value to reflection object.)
- 反射从反射对象到接口值中(Reflection goes from reflection object to interface value.)
- 要修改反射对象,值必须是“可设置”的(To modify a reflection object, the value must be settable.)
一旦你了解反射法则,Go就会变得更加得心应手(虽然它仍旧微妙)。这是一个强大的工具,除非在绝对必要的时候,我们应该谨慎并避免使用它。
我们还有非常多的反射知识没有提及——chan的发送和接收,内存分配,使用slice和map,调用方法和函数——但是这篇文章已足够长了。我们将在以后的文章中涉及这些。
By Rob Pike
Golang基础(5):Go语言反射规则的更多相关文章
- Go语言反射规则
Go语言反射规则 - The Laws of Reflection 转:http://my.oschina.net/qbit/blog/213720 原文地址:http://blog.golang.o ...
- 语言反射规则 - The Laws of Reflection
[译]Go反射的三个原则(官方博客) | seven的分享 https://sevenyu.top/2019/12/21/laws-of-reflection.html wilhg/The-Laws- ...
- 10. Go 语言反射
Go 语言反射 反射是指在程序运行期对程序本身进行访问和修改的能力.程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分.在运行程序时,程序无法获取自身的信息. 支持反射的语言可以在 ...
- Golang 基础之基础语法梳理 (三)
大家好,今天将梳理出的 Go语言基础语法内容,分享给大家. 请多多指教,谢谢. 本次<Go语言基础语法内容>共分为三个章节,本文为第三章节 Golang 基础之基础语法梳理 (一) Gol ...
- golang基础归纳
1. hello-world package main import "fmt" func main(){ fmt.Println("Hello world, Go Go ...
- Golang 基础之基础语法梳理 (二)
大家好,今天将梳理出的 Go语言基础语法内容,分享给大家. 请多多指教,谢谢. 本次<Go语言基础语法内容>共分为三个章节,本文为第二章节 Golang 基础之基础语法梳理 (一) Gol ...
- golang基础知识之encoding/json package
golang基础知识之json 简介 JSON(JavaScript Object Notation)是一种轻量级的数据交换格式.可以去json.org 查看json标准的清晰定义.json pack ...
- Golang基础之函数
golang基础之函数 1.为什么需要函数? 有些相同的代码可能出现多次,如果不进行封装,那么多次写入到程序中,会造成程序冗余,并且可读性降低 2.什么是函数 为完成某些特定功能的程序指令集合称为函数 ...
- GoLang基础数据类型--->字典(map)详解
GoLang基础数据类型--->字典(map)详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 可能大家刚刚接触Golang的小伙伴都会跟我一样,这个map是干嘛的,是 ...
随机推荐
- libusb读写
https://blog.csdn.net/u012247418/article/details/83684980 https://github.com/crazybaoli/libusb-test ...
- 多线程(三) 同步synchronized
五.同步 1.锁 多线程程序一般是为了完成一些相同的工作而存在的,因此有时间也会共享一些资源,例如对象.变量等等,此时如果不对各个线程进行资源协调,就会出现一些冲突,从而导致程序功能失效.例如下面的示 ...
- 集合(一)Collection、List、ArrayList和Vector
一.Collection 集合存放在java.util包中,可以看作是集成好的数据结构,供你调用,十分方便,集合经常拿来和数组对比,其实我觉得没啥可比性,不过还是简单来看看它们的区别: 1.数组长度固 ...
- MyEclipse使用教程:添加和更新插件(二)
[MyEclipse CI 2019.4.0安装包下载] 通过Eclipse Marketplace目录或各种更新站点类型添加插件来自定义您的Genuitec IDE. Genuitec提供以下IDE ...
- Eclipse快捷键和IDEA对比
- jquery isDefaultPrevented()方法 语法
jquery isDefaultPrevented()方法 语法 作用:isDefaultPrevented() 方法返回指定的 event 对象上是否调用了 preventDefault() 方法. ...
- Angular 应用的外壳
你首先需要使用 Angular CLI 来创建一个初始化的应用.随后,你将对你已经初始化的应用进行修改来让你构建出 Tour of Heroes app(英雄指南) 这个应用. 在教程的本部分,你需要 ...
- 614A - Link/Cut Tree 数乘
A. Link/Cut Tree time limit per test 2 seconds memory limit per test 256 megabytes input standard in ...
- codevs 1077 多源最短路x
题目描述 Description 已知n个点(n<=100),给你n*n的方阵,a[i,j]表示从第i个点到第j个点的直接距离. 现在有Q个询问,每个询 ...
- codevs 2602 最短路径问题x
题目描述 Description 平面上有n个点(n<=100),每个点的坐标均在-10000~10000之间.其中的一些点之间有连线.若有连线,则表示 ...