Go的反射

介绍

反射能力是一个程序自己检查自己的结构的能力,是一种元编程(metaprogramming)的形式。

类型和接口(Types and interfaces)

我们必须明确以下三个概念,因为反射和它们息息相关。

区分三个词:

  • 静态类型(static type
  • 基础类型(underlying type
  • 接口类型(interface types

静态类型和基础类型

  • 可以说 Go 中的每一个类型都是静态类型。因为 Go 是静态类型的(Statically Typed)语言。即开发者自定义的类型也是一种静态类型。
  • 基础类型是指 GO 内部定义的几种类型,如 intfloat64 等等。

如下方示例,

  • MyInt 是一个静态类型,j 的类型(就是指静态类型)就是 MyInt
  • int 也是一个静态类型,同时它也是一个基础类型;
  • 所以 ij 有不同的类型(静态类型),但有相同的基础类型;
  • 静态类型不同的变量相互之间是不可以直接赋值的。
type MyInt int

var i int
var j MyInt

静态类型和动态类型的区别(参考

如果在编译时就知道某一个变量是什么类型,这个语言就是静态类型的编程语言。
所以开发者在开发时必须为每一个变量指定类型,它的好处就在于编译器可以做一些检查工作,即在编译阶段就发现了一些 bug。

如果一种语言是动态的,那变量只在运行时有类型。
所以动态编程语言没有编译器检查它们在类型,这让开发变得快速。这类语言大多是脚本语言,即一般比较小,这样方便开发者自己去找 bug。

接口类型

接口类型是提供了一套固定的方法的类型。一个接口变量可以存储任何非接口的固定的值,只要这个值实现了接口的方法。

一个很好的例子就是 io.Readerio.Writer。下面这些使用都是可以的。

    var r io.Reader        // interface{}
    r = os.Stdin           // *io.File
    r = bufio.NewReader(r) // *bufio.Reader
    r = new(bytes.Buffer)  // bytes.Buffer
    // and so on

一定要注意: r 定义为 io.Reader 接口类型,不论 r 的值如何变化,是 os.Stdin*io.File 类型)还是 bufio.NerReader(r)*bufio.Reader 类型),它的静态类型都是 io.Reader

一个极重要的接口类型是空接口 interface{} 它表示了一组空的方法,因为任何类型都有零个或多个方法 ,所以值就可以是任何类型。(一些人说 Go 的 interface{} 是动态类型的,这是错误的。)

一个接口的表示

这里有一篇博文详细介绍了接口:Go Data Structures: Interfaces,可以看一下,这里简单总结一下。

一个接口类型的变量存了两部分:一个是具体的值,和这个值的类型描述符(type descriptor)。

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 实现了 io.Reader 的接口。
所以我们可以用如下方法:

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

这个表达示叫 类型断言(type assertion),上例中断言了 r 的内部也实现了 io.Writer,所以我们可以将它赋值给 w,所以 w 包含一对信息 (tty, *os.File)

进一步地,我们还可以这样做:

var empty interface{}
empty = w

这样我们的空接口就包含了相同的一对信息 (tty, *os.File)。这种方式很灵巧:一个空的接口可以包含任何值并且包含我们可能需要的所有信息。(这里不用断言,因为 it's known statically that w satisfies the empty interface)

有一点要注意,一个接口的内部的一对信息,必须是 (value, concrete type) 而不能是 (value, iinterface value)。接口不能有接口类型的值。

下面我们来介绍反射。

反射

反射有三条法则:

  • 从接口值反射为反射对象
  • 从反射对象反射为接口值
  • 更新值时必须是可更新的

从接口值反射为对象

reflect 包有两个类型: Type 和 Value,这两个类型让我们有方法 reflect.TypeOfreflect.ValueOf 获取到接口内部的信息 reflect.Typereflect.Value。(同样通过 reflect.Value 也可以得到 reflect.Type,我们稍后再说。)

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

当我们调用 reflect.TypeOf(x) 时,它会将 x copy 一份到空的 interface{} 类型中,然后在 TypeOf() 从空接口中恢复到类型信息。

reflect.ValueOf() 方法同样从接口中恢复到值信息。

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

(由于 fmt 自动会获取到具体的类型的值,所以这里我们用了 String() 方法。)

总览
  • reflect.Valuereflect.Type 都有 Kind() 方法是获取反射对象的 基础类型(underlying type),而不是静态类型。
  • reflect.Value 上有 Type() 方法,可以获取到值的类型
  • reflect.Value 上有 Int() 等方法可以获取对应的值
  • reflect.Value 上有 SetInt() 等方法,可以设置值(下面介绍)

从对象反射为接口值

给定一个 reflect.Value 我们可以恢复它对应接口的值,这用到 Interface() 方法。它会将 type 信息和 value 信息打包到 interface{} 中,并返回 interface{} 值。

// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}

要更新一个反射对象,它的值必须是可更新的

第三个法则很微妙,但也容易理解。

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 是不可寻址的,可设置性也是反射的一个属性,可以通过 CanSet() 方法来获取。

为什么它是不可设置的呢?
因为上例中 reflect.ValueOf(x) 和其他普通方法 f(x) 一样,会 copy 一个 x 的值传入方法中,而不是 x 本身,所以如果我们需要通过反射修改一个变量的值时,必须传入它的地址 reflect.ValueOf(&x)

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

但这样仍不够,因为 p 的类型是一个指针,所以我们需要设置它对应的值,所以需要方法 Elem()。所以,变成下面:

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

这样就完成了通过反射的方式,更新一个变量的值。重申:传入一定要是地址,并且修改的是地址中对应的值(Elem()方法获取值)。

结构体(Structs)

我们前面都是在以基础类型做示例,下面是一个结构体的例子。

t := T{23, "robin"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
fmt.Println(typeOfT) // main.T
for i := 0; i < s.NumField(); i++ {
    filed := s.Field(i)                // 返回类型 reflect.Value
    fmt.Println(typeOfT.Field(i).Name) // A B
    fmt.Println(filed.Type())          // int string
    fmt.Println(filed.Interface())     // 23 robin
    fmt.Println()
}
s.Field(0).SetInt(28)       // 设置值
s.Field(1).SetString("cai") // 设置值
fmt.Println(t) // {28 cai}

基本是一样的,不过结构体中

  • s := reflect.ValueOf(&t).Elem() 获取元素的值 Value
  • s.NumField() 获取值对应的长度
  • s.Field(i) 获取对应下标的元素的 Value

总结

记住这三条法则:

  • 反射可以从接口值反射为反射对象
  • 反射可以从反射对象反射为接口值
  • 要更新反射对象,其值必须可设置

反射中还有许多在这里没讲到,比如收发 channel、分配内存、用 slices 和 maps、调方法等。可以阅读一下其他文章。


附:

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

推荐阅读更多精彩内容

  • Golang语言实现了反射,反射机制就是在运行时动态的调用对象的方法和属性,官方自带的reflect包就是反射相关...
    OOM_Killer阅读 765评论 0 2
  • [TOC] Golang的反射reflect深入理解和示例 【记录于2018年2月】 编程语言中反射的概念 在计算...
    AllenWu阅读 867评论 1 14
  • 编程语言中反射的概念 在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机...
    豆瓣奶茶阅读 12,650评论 0 31
  • 第一次知道反射的时候还是许多年前在学校里玩 C# 的时候。那时总是弄不清楚这个复杂的玩意能有什么实际用途……然后发...
    勿以浮沙筑高台阅读 1,127评论 0 9
  • 这是2015年冬上映的动画电影,故事不新鲜,却仍打动了我。 时至2016年夏,影片已无热度,不少人曾吐槽这是皮克斯...
    栁絮阅读 752评论 0 0