golang反射机制介绍

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

golang反射机制介绍

Go语言提供了一种机制,在编译时不知道类型的情况下,可更新变量、在运行时查看值、调用方法以及直接对他们的布局进行操作,这种机制称为反射(reflection)。

reflect.Type 和 reflect.Value

反射功能由reflect包提供,它定义了两个重要的类型:Type 和 Value,分别表示变量的类型和值。反射包里面,提供了reflect.TypeOf 和 reflect.ValueOf,返回被检查对象的Type 和 Value:

func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value

可以看到,TypeOf和ValueOf的参数是interface{}类型,当把一个具体值赋给一个interface{}类型时,会发生一个隐式的类型转换,转换会生成一个包含两个部分内容的接口值:动态类型部分是操作数的类型,动态值部分是操作数的值。

reflect.TypeOf返回接口值对应的动态类型,注意返回的时具体值类型而不是接口类型。比如下面的输出的是"*os.File"而不是"io.Writer":

var w io.Writer = os.Stdout
fmt.Println(reflect.TypeOf(v)) // "*os.File"

同理,reflect.ValueOf返回接口动态值,以reflect.Value的形式返回,与reflect.TypeOf类似:

v := reflect.ValueOf(3)
fmt.Println(v) //"3"

调用Value的Type方法会把它的类型以reflect.Type方式返回:

t := v.Type() //an reflect.Type
fmt.Println(t.String()) //"int"

reflect.ValueOf的逆操作是reflect.Value.Interface方法,它返回一个interface{}接口值,与reflect.Value包含同一个具体值:

v := reflect.ValueOf(3)
x := v.Interface()
i := x.(int)
fmt.Printf("%d\n", i) //"3"

reflect.Value 与 interface{}都可以包含任意值。二者的区别是空接口(interface{})隐藏了值的布局信息、内置操作和相关方法,除非我们知道它的动态类型,并用一个类型断言来渗透进去,否则我们对所包含的值能做的事情很少。作为对比,Value有很多方法可以用来分析所包含的值,而不用知道它的类型。

利用反射遍历结构体

前面介绍的是利用反射打印简单数据类型的类型和结构,接下来介绍如何用反射遍历一个结构体,可以参考下面的一个代码:

func Display(i interface{}) {
    fmt.Printf("Display %T\n", i)   
    display("", reflect.ValueOf(i))
    
}
 
func display(path string, v reflect.Value) {
    switch v.Kind() {
    case reflect.Int, reflect.Int8, reflect.Int16,
        reflect.Int32, reflect.Int64,
        reflect.Uint, reflect.Uint8, reflect.Uint16,        
        reflect.Uint32, reflect.Uint64,         
        reflect.Float32, reflect.Float64,       
        reflect.Bool,       
        reflect.String:         
        fmt.Printf("%s: %v (%s)\n", path, v, v.Type().Name())       
    case reflect.Ptr:   
        v = v.Elem()        
        display(path, v)        
    case reflect.Slice, reflect.Array:  
        for i := 0; i < v.Len(); i++ {      
            display(" "+path+"["+strconv.Itoa(i)+"]", v.Index(i))           
        }       
    case reflect.Struct:    
        t := v.Type()   
        for i := 0; i < v.NumField(); i++ {         
            display(" "+path+"."+t.Field(i).Name, v.Field(i))           
        }       
    }   
}

首先创建一个Display函数,然后在该函数里面封装了一个display,display函数可以递归遍历结构体内部的字段,如果是简单的数据类型就直接打印出来,否则就递归遍历结构体成员。接下来简单解析一下display函数:

数组与切边:如果v为数组或者切边,则调用v.Len()函数,获取该数组或切边的长度,调用v.Index(i), 获取该数组或切边的第i个元素。

指针: 如果v为指针,调用v.Elem(),获取指针指向的变量,然后递归该变量。

结构体:如果v为结构体,v.NumField()将返回该结构体的字段数,v.Field(i)将返回第i个字段。

当然,switch语句这里,v.Kind()的类型不仅仅是这点,还有一些其他类型,比如reflect.Map,reflect.Interface, reflect.Func,reflect.Chan等等,这段代码的目的仅仅是为了展示如何用反射遍历结构体,所以没有必要考虑所有的结构,都是大同小异的。

接下来我们,我们定义一个内嵌结构体的结构体,并且调用Display函数,遍历该结构体:

type Person struct {
    Name   string   
    Age    int  
    Gender bool
    
}

type Student struct {
    Person *Person  
    Course []string 
    Core   []float32    
}

func main() {
    st := &Student{
        Person: &Person{    
            Name:   "Jim",          
            Age:    18,         
            Gender: true,           
        },  
        Course: []string{"Math", "Data Structe", "Algorithm"},      
        Core:   []float32{90.5, 85, 89.9},      
    }   
    Display(st) 
}

输出如下:

Display *main.Student
  .Person.Name: Jim (string)   
  .Person.Age: 18 (int)  
  .Person.Gender: true (bool)  
  .Course[0]: Math (string)   
  .Course[1]: Data Structe (string)   
  .Course[2]: Algorithm (string)  
  .Core[0]: 90.5 (float32)   
  .Core[1]: 85 (float32)   
  .Core[2]: 89.9 (float32)

利用reflect.Value修改值

Golang中,反射不只用来解析变量值,还可以改变变量的值,接下来介绍如何用reflect.Value来设置变量的值。

我们需要知道的是,在我们传参给reflect.ValueOf获取reflect.Value时,传入的是参数的拷贝,所以reflect.ValueOf获得的是传入参数的拷贝的reflect.Value,对于它的修改对原来的数据是没有影响的。所以如果我们想要修改原来的值,就需要用指针,然后还需要利用Elem方法获取指针指向的变量,通过修改Elem()返回的变量,才能真正的修改原始变量的值,如下:

x := 2
v := reflect.ValueOf(&x)
e := v.Elem()
e.Set(reflect.ValueOf(3))
fmt.Println(x) //"3"

这里,还有一些基本类型特化的Set变种:SetInt、SetUint、SetString、SetFloat等:

e.SetInt(4)
fmt.Println(e) //"4"

在更新变量前,我们可以用CanSet方法来判断是否可更改,如果更改一个不可更改的reflect.Value,将导致panic:

fmt.Println(e.CanSet()) //"true"
d := reflect.ValueOf(2)
fmt.Println(d.CanSet()) //"false"
d.Set(reflect.ValueOf(4)) // 这里将导致panic

备注

Golang的反射是i一个功能和表达能力都很强大的工具,但是使用它是要谨慎,具体有如下一些原因:

  • 基于反射的的代码都很脆弱。能导致编译器报告类型错误的每种写法,在反射中都有一个对应的的误用写法。编译器在编译时就能向你报告的这个错误,而反射错误则要等到执行时才以崩溃的方式来报告。并且反射还降低了自动重构和分析工具的安全性和准确度,因为它们无法检测到类型信息。

  • 我们要避免使用反射的原因是,反射的相关操作无法做静态类型检查,所以对于大量使用反射的代码是很难理解的。对于接受interfacef{}或者reflect.Value的函数,一定要写清楚期望的参数类型和其他限制条件

  • 基于反射的函数会比特定类型优化的函数慢一两个数量级。测试很适合用反射,但是对于关键路径上的函数,最好避免使用反射。


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

本文来自:简书

感谢作者:lightmen

查看原文:golang反射机制介绍

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

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