从结构体和接口深入理解GO反射

【译文】原文地址
关于Go反射这个主题,需要理解Go内部关于结构体、接口和类型系统,才能理解反射的底层工作机制。当然,您也使用反射,而不需要深入理解这些细节。本文的目标是向您介绍一些细节,使您能够更深入地理解反射。但是这些不是严格要求的。这篇文章假设您对结构体和接口有基本的了解。你可以通过"Go by example"快速浏览下结构体接口,也可以深入学习下Go的结构体接口

Reflection. Photo by Dawid Zawiła on Unsplash

什么是反射

In computer science, reflection is the ability of a process to examine, introspect, and modify its own structure and behavior. — Wikipedia

维基百科上面解释到,在计算机科学中,反射是一种能够对结构体和行为(本人理解为函数或者方法)进行检查、内省的过程。
反射是程序运行时的操作。它是一种元编程形式,但并不是所有的元编程都是反射。

为什么反射对Go语言重要

反射在很多方面都起作用。通过本文我将关注最明显的一个作用。Go作为一种静态编程语言,你必须提前声明所有类型才可以使用。因此,你没办法处理事先不清楚的类型,即使你需要对它进行操作、检查你也不需要了解这些信息。

一个典型的例子就是fmt包中的print函数。如果你想打印一个变量的类型可以使用%T,fmt包不需要知道你自定义的Person结构体。但是它还是能打印出Person接头体内容。

空接口

接口是一种定义了多个方法的类型,实现了这些方法的结构体就实现了该接口。这允许将接口作为一种类型传给方法使用,您可以将实现了该接口的结构体传入方法。对于一个空接口,每个结构体和每个基本类型,内建实现了空接口。

因此,使用空接口作为类型的函数参数,可以接受任意类型参数。

func main() {
  x := 100
  fmt.Println(x+1)
  myPrint(x)
}

func myPrint(item interface{}) {
  fmt.Println(item)
}

上面的代码可以正常工作,第一个print将打印101,因为对类型int的变量x执行了+1操作,然后将x传给Println,该函数也是接收一个空接口作为参数,内部是反射实现。

但是Go还是一个静态类型语言,所以使用空接口将不允许您对变量进行任何其他操作(除非使用类型断言或反射)。

func main() {
  x := 100
  myPrint(x)
}

func myPrint(item interface{}) {
  fmt.Println(item+1)
  fmt.Println(item)
}

上面的代码无法编译通过,在myPrint函数中,item是空接口类型,即使底层是整形,但是Go并不知道它,因此代码会panic。

类型断言

类型断言可以帮助你验证变量的实际类型,如果它是您断言的类型,就会以这种类型来获取对应值。

func main() {
  var myVar interface{} = 10

  v, ok := myVar.(int)
  if (ok) {
    fmt.Println(v)
  }
}

上面的代码会打印10,因为我们使用myVar.(int)得到对应的原始类型。断言成功的话,v将赋值为对应类型变量,ok将赋值为bool值。

关于类型断言的更多内容,,如果您感兴趣的话可以浏览类型断言类型切换

类型实现细节在哪里呢?

类型断言(以及代理,反射)是如何知道一个通用接口(空接口)的底层类型的呢。

要理解这点,需要通过/src/sync/atomic/value.go直接看go实现。它实现了go中每个变量的基础值。

// ifaceWords is interface{} internal representation.                       
type ifaceWords struct {
    typ  unsafe.Pointer
    data unsafe.Pointer
}

空接口和go通过扩展的每个值,在底层表示中包括两个unsafe指针:typ和data。

  • typ保存当前变量的类型信息,因此即使一个变量是空接口,实际的类型信息在typ中是完整的。
  • data保存值本身,还有其他数据信息如kind值,这个不在本文讨论范围之内。重点是data保存类型的值信息。

类型断言缺少什么?(或者为何需要反射)

当你知道要检查的类型时,断言允许验证和使用接口的底层类型值。在前面的例子中,我们专门为int使用断言。因为我们提前知道其类型,以便断言可以正常工作。即使我们用switch多个case来检查,我们仍然必须知道在编译时断言的具体类型。

在编译时不知道具体的类型情况下,就需要反射了。或者换句话说,如本文开头所述,当我们需要在运行时检查。回想下fmt例子,fmt包并不知道结构体类型但仍然可以打印它的值和类型(使用%T)。

反射

Reflect.Type和Reflect.Value是反射包提供的两个基本且最终要的类型。它们是Reflect包中定义的两个结构体,reflect包有操作接口变量的方法,底层实现其实都是通过将接口的typ和data信息复制到这两个结构体上。这样通过这两个结构体对应的方法即可处理接口了。
Reflect.TypeOf()和Reflect.ValueOf()是两个可用的基本方法,分别返回Reflect.Type和Reflect.Value,如下所示:

import (
  "fmt"
  "reflect"
)

func main() {
  var myVar interface{} = 10

  myType := reflect.TypeOf(myVar)
  myValue := reflect.ValueOf(myVar)

  fmt.Println(myType) // > int
  fmt.Println(myValue) // > 10
}

为了更清楚的说明:我们可以看一下自己定义的struct例子:


type Person struct {
  name string
}

func main() {
  var myPerson interface{} = Person{name: "Snir David"}

  myType := reflect.TypeOf(myPerson)
  myValue := reflect.ValueOf(myPerson)

  fmt.Println(myType) // > main.Person
  fmt.Println(myValue) // > {Snir David}
}

使用TypeOf和VauleOf返回底层类型,和一个指向值的指针。需要注意的是ValueOf返回的的值类型是Reflect.Value类型的,并不是变量原始类型。

例如,前面的例子中,我们不能取接口的值并对其做算数运算,比如myValue + 1。这是无法通过编译的,因为Go编译器无法根据Reflect.Value类型识别这个操作,但对原始类型int是可识别的。

Reflect.Kind

The kind is what the type is made of — a slice, a map, a pointer, a struct, an interface, a string, an array, a function, an int or some other primitive type.

理解Kind是有点棘手的,而且网上的一些介绍也会让您感到困惑,因为大部分介绍kind都是根据type理论,和讨论Haskell之类的语言实现。
关于Go你需要知道的是,每个变量都有一个Kind类型,是从type派生出来的。Kind可以理解为是类型中的类型。
最容易说清楚kind概念就是自己定义的结构体。让我们回到前面创建Person结构体的代码。我们定义的myPerson是一个Person类型。Person结构体本身类型,即Kind是struct。获取kind类型可以通过对Reflect.Type变量使用Kind()方法
例如上面的代码可以修改为:

 myKind := reflect.TypeOf(myPerson).Kind()

可以在如下链接查看所有的Kind值:
https://golang.org/pkg/reflect/#Kind

对一些例子,不像上面struct比较明显,Kind看起来似乎有点重复。例如type是int64其Kind也是int64。无需强调的是,我们了解Kind是因为它有助于重用空接口值(译者:这里似乎没解释清楚)。

将Reflect.Value转换为原始类型值

因此我们根据Reflect.ValueOf()函数可以对一个空接口类型变量进行分析,并得到一个类型为Rreflect.Value值。但是这个值并不正真有用,因为Go类型系统无法识别出它的原始类型。

我们希望将该值转换成其原始类型。这个过程如下:
1、使用Reflect.TypeOf或Reflect.Kind()识别出其原始类型。
2、使用指向值的指针来获取原始值(unsafe指针不在本文范围内)
3、对指针进行类型转换
很幸运,reflect包已经提供了处理所有的基本类型转换函数。如下所示:

func main() {
  var myVar interface{} = 10
  reflectValue := reflect.ValueOf(myVar)
  intValue := reflectValue.Int()
  // Arithmetic will now work, as this is typed int
  fmt.Println(intValue + 1) // > 11
}

所有的基本类型都有转换方法可用,Bool、Float、String等。

复杂类型的探究

上面介绍了基本类型,但是我们如何处理结构体呢?
reflect包也提供了方法来查看结构体内部信息。如下代码所示:

type Person struct {
  name string
  age int
}

func investigateStruct(s interface{}) {
  reflValue := reflect.ValueOf(s)
  // Make sure we are handling with a struct here
  if (reflValue.Kind() == reflect.Struct) {
    fieldCount := reflValue.NumField()
    fmt.Println("Num of fields: ", fieldCount)
    for i := 0; i < fieldCount; i++ {
      // Get individual field details
      field := reflValue.Field(i)
      fmt.Printf("type: %T, value: %v \n", field, field)
    }
  }
}

func main() {
  var myVar interface{} = Person{name: "Snir", age: 27}
  investigateStruct(myVar)
}

Output:

Num of fields:  2
type: reflect.Value, value: Snir 
type: reflect.Value, value: 27 

以上代码查看了结构体中包含的字段数,以及每个字段类型和值。

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

推荐阅读更多精彩内容