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

汪明军_3145 · · 441 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

【译文】原文地址
关于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

理解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 

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


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:汪明军_3145

查看原文:从结构体和接口深入理解GO反射

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

441 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传