一文带你了解Golang中reflect反射的常见错误

目录
  • 获取 Value 的值之前没有判断类型
  • 没有传递指针给 reflect.ValueOf
  • 在一个无效的 Value 上操作
    • 什么时候 IsValid 返回 false
    • 其他情况下 IsValid 返回 false
  • 通过反射修改不可修改的值
  • 在错误的 Value 上调用 Elem 方法
  • 调用了一个其类型不能调用的方法
  • 总结

go 的反射是很脆弱的,保证反射代码正确运行的前提是,在调用反射对象的方法之前, 先问一下自己正在调用的方法是不是适合于所有用于创建反射对象的原始类型。 go 反射的错误大多数都来自于调用了一个不适合当前类型的方法(比如在一个整型反射对象上调用 Field() 方法)。 而且,这些错误通常是在运行时才会暴露出来,而不是在编译时,如果我们传递的类型在反射代码中没有被覆盖到那么很容易就会 panic

本文就介绍一下使用 go 反射时很大概率会出现的错误。

获取 Value 的值之前没有判断类型

对于 reflect.Value,我们有很多方法可以获取它的值,比如 Int()String() 等等。 但是,这些方法都有一个前提,就是反射对象底层必须是我们调用的那个方法对应的类型,否则会 panic,比如下面这个例子:

var f float32 = 1.0
v := reflect.ValueOf(f)
// 报错:panic: reflect: call of reflect.Value.Int on float32 Value
fmt.Println(v.Int())

上面这个例子中,f 是一个 float32 类型的浮点数,然后我们尝试通过 Int() 方法来获取一个整数,但是这个方法只能用于 int 类型的反射对象,所以会报错。

  • 涉及的方法:Addr, Bool, Bytes, Complex, Int, Uint, Float, Interface;调用这些方法的时候,如果类型不对则会 panic
  • 判断反射对象能否转换为某一类型的方法:CanAddr, CanInterface, CanComplex, CanFloat, CanInt, CanUint
  • 其他类型是否能转换判断方法:CanConvert,可以判断一个反射对象能否转换为某一类型。

通过 CanConvert 方法来判断一个反射对象能否转换为某一类型:

// true
fmt.Println(v.CanConvert(reflect.TypeOf(1.0)))

如果我们想将反射对象转换为我们的自定义类型,就可以通过 CanConvert 来判断是否能转换,然后再调用 Convert 方法来转换:

type Person struct {
   Name string
}

func TestReflect(t *testing.T) {
   p := Person{Name: "foo"}
   v := reflect.ValueOf(p)

   // v 可以转换为 Person 类型
   assert.True(t, v.CanConvert(reflect.TypeOf(Person{})))

   // v 可以转换为 Person 类型
   p1 := v.Convert(reflect.TypeOf(Person{}))
   assert.Equal(t, "foo", p1.Interface().(Person).Name)
}

说明:

  • reflect.TypeOf(Person{}) 可以取得 Person 类型的信息
  • v.Convert 可以将 v 转换为 reflect.TypeOf(Person{}) 指定的类型

没有传递指针给 reflect.ValueOf

如果我们想通过反射对象来修改原变量,就必须传递一个指针,否则会报错(暂不考虑 slice, map, 结构体字段包含指针字段的特殊情况):

func TestReflect(t *testing.T) {
   p := Person{Name: "foo"}
   v := reflect.ValueOf(p)

   // 报错:panic: reflect: reflect.Value.SetString using unaddressable value
   v.FieldByName("Name").SetString("bar")
}

这个错误的原因是,v 是一个 Person 类型的值,而不是指针,所以我们不能通过 v.FieldByName("Name") 来修改它的字段。

对于反射对象来说,只拿到了 p 的拷贝,而不是 p 本身,所以我们不能通过反射对象来修改 p。

在一个无效的 Value 上操作

我们有很多方法可以创建 reflect.Value,而且这类方法没有 error 返回值,这就意味着,就算我们创建 reflect.Value 的时候传递了一个无效的值,也不会报错,而是会返回一个无效的 reflect.Value

func TestReflect(t *testing.T) {
   var p = Person{}
   v := reflect.ValueOf(p)

   // Person 不存在 foo 方法
   // FieldByName 返回一个表示 Field 的反射对象 reflect.Value
   v1 := v.FieldByName("foo")
   assert.False(t, v1.IsValid())

   // v1 是无效的,只有 String 方法可以调用
   // 其他方法调用都会 panic
   assert.Panics(t, func() {
      // panic: reflect: call of reflect.Value.NumMethod on zero Value
      fmt.Println(v1.NumMethod())
   })
}

对于这个问题,我们可以通过 IsValid 方法来判断 reflect.Value 是否有效:

func TestReflect(t *testing.T) {
   var p = Person{}
   v := reflect.ValueOf(p)

   v1 := v.FieldByName("foo")
   // 通过 IsValid 判断 reflect.Value 是否有效
   if v1.IsValid() {
      fmt.Println("p has foo field")
   } else {
      fmt.Println("p has no foo field")
   }
}

Field() 方法在传递的索引超出范围的时候,直接 panic,而不会返回一个 invalid 的 reflect.Value。

IsValid 报告反射对象 v 是否代表一个值。 如果 v 是零值,则返回 false。 如果 IsValid 返回 false,则除 String 之外的所有其他方法都将发生 panic。 大多数函数和方法从不返回无效值。

什么时候 IsValid 返回 false

reflect.ValueIsValid 的返回值表示 reflect.Value 是否有效,而不是它代表的值是否有效。比如:

var b *int = nil
v := reflect.ValueOf(b)
fmt.Println(v.IsValid())                   // true
fmt.Println(v.Elem().IsValid())            // false
fmt.Println(reflect.Indirect(v).IsValid()) // false

在上面这个例子中,v 是有效的,它表示了一个指针,指针指向的对象为 nil。 但是 v.Elem()reflect.Indirect(v) 都是无效的,因为它们表示的是指针指向的对象,而指针指向的对象为 nil。 我们无法基于 nil 来做任何反射操作。

其他情况下 IsValid 返回 false

除了上面的情况,IsValid 还有其他情况下会返回 false

  • 空的反射值对象,获取通过 nil 创建的反射对象,其 IsValid 会返回 false
  • 结构体反射对象通过 FieldByName 获取了一个不存在的字段,其 IsValid 会返回 false
  • 结构体反射对象通过 MethodByName 获取了一个不存在的方法,其 IsValid 会返回 false
  • map 反射对象通过 MapIndex 获取了一个不存在的 key,其 IsValid 会返回 false

示例:

func TestReflect(t *testing.T) {
   // 空的反射对象
   fmt.Println(reflect.Value{}.IsValid())      // false
   // 基于 nil 创建的反射对象
   fmt.Println(reflect.ValueOf(nil).IsValid()) // false

   s := struct{}{}
   // 获取不存在的字段
   fmt.Println(reflect.ValueOf(s).FieldByName("").IsValid())  // false
   // 获取不存在的方法
   fmt.Println(reflect.ValueOf(s).MethodByName("").IsValid()) // false

   m := map[int]int{}
   // 获取 map 的不存在的 key
   fmt.Println(reflect.ValueOf(m).MapIndex(reflect.ValueOf(3)).IsValid())
}

注意:还有其他一些情况也会使 IsValid 返回 false,这里只是列出了部分情况。 我们在使用的时候需要注意我们正在使用的反射对象会不会是无效的。

通过反射修改不可修改的值

对于 reflect.Value 对象,我们可以通过 CanSet 方法来判断它是否可以被设置:

func TestReflect(t *testing.T) {
   p := Person{Name: "foo"}

   // 传递值来创建的发射对象,
   // 不能修改其值,因为它是一个副本
   v := reflect.ValueOf(p)
   assert.False(t, v.CanSet())
   assert.False(t, v.Field(0).CanSet())

   // 下面这一行代码会 panic:
   // panic: reflect: reflect.Value.SetString using unaddressable value
   // v.Field(0).SetString("bar")

   // 指针反射对象本身不能修改,
   // 其指向的对象(也就是 v1.Elem())可以修改
   v1 := reflect.ValueOf(&p)
   assert.False(t, v1.CanSet())
   assert.True(t, v1.Elem().CanSet())
}

CanSet 报告 v 的值是否可以更改。只有可寻址(addressable)且不是通过使用未导出的结构字段获得的值才能更改。 如果 CanSet 返回 false,调用 Set 或任何类型特定的 setter(例如 SetBoolSetInt)将 panicCanSet 的条件是可寻址。

对于传值创建的反射对象,我们无法通过反射对象来修改原变量,CanSet 方法返回 false例外的情况是,如果这个值中包含了指针,我们依然可以通过那个指针来修改其指向的对象。

只有通过 Elem 方法的返回值才能设置指针指向的对象。

在错误的 Value 上调用 Elem 方法

reflect.ValueElem() 返回 interface 的反射对象包含的值或指针反射对象指向的值。如果反射对象的 Kind 不是 reflect.Interfacereflect.Pointer,它会发生 panic。 如果反射对象为 nil,则返回零值。

我们知道,interface 类型实际上包含了类型和数据。而我们传递给 reflect.ValueOf 的参数就是 interface,所以在反射对象中也提供了方法来获取 interface 类型的类型和数据:

func TestReflect(t *testing.T) {
   p := Person{Name: "foo"}

   v := reflect.ValueOf(p)

   // 下面这一行会报错:
   // panic: reflect: call of reflect.Value.Elem on struct Value
   // v.Elem()
   fmt.Println(v.Type())

   // v1 是 *Person 类型的反射对象,是一个指针
   v1 := reflect.ValueOf(&p)
   fmt.Println(v1.Elem(), v1.Type())
}

在上面的例子中,v 是一个 Person 类型的反射对象,它不是一个指针,所以我们不能通过 v.Elem() 来获取它指向的对象。 而 v1 是一个指针,所以我们可以通过 v1.Elem() 来获取它指向的对象。

调用了一个其类型不能调用的方法

这可能是最常见的一类错误了,因为在 go 的反射系统中,我们调用的一些方法又会返回一个相同类型的反射对象,但是这个新的反射对象可能是一个不同的类型了。同时返回的这个反射对象是否有效也是未知的。

在 go 中,反射有两大对象 reflect.Typereflect.Value,它们都存在一些方法只适用于某些特定的类型,也就是说, 在 go 的反射设计中,只分为了类型两大类。但是实际的 go 中的类型就有很多种,比如 intstringstructinterfaceslicemapchanfunc 等等。

我们先不说 reflect.Type,我们从 reflect.Value 的角度看看,将这么多类型的值都抽象为 reflect.Value 之后, 我们如何获取某些类型值特定的信息呢?比如获取结构体的某一个字段的值,或者调用某一个方法。 这个问题很好解决,需要获取结构体字段是吧,那给你提供一个 Field() 方法,需要调用方法吧,那给你提供一个 Call() 方法。

但是这样一来,有另外一个问题就是,如果我们的 reflect.Value 是从一个 int 类型的值创建的, 那么我们调用 Field() 方法就会发生 panic,因为 int 类型的值是没有 Field() 方法的:

func TestReflect(t *testing.T) {
   p := Person{Name: "foo"}
   v := reflect.ValueOf(p)

   // 获取反射对象的 Name 字段
   assert.Equal(t, "foo", v.Field(0).String())

   var i = 1
   v1 := reflect.ValueOf(i)
   assert.Panics(t, func() {
      // 下面这一行会 panic:
      // v1 没有 Field 方法
      fmt.Println(v1.Field(0).String())
   })
}

至于有哪些方法是某些类型特定的,可以参考一下下面两个文档:

  • 类型特定的 reflect.Value 方法
  • 类型特定的 reflect.Type 方法

总结

  • 在调用 Int()Float() 等方法时,需要确保反射对象的类型是正确的类型,否则会 panic,比如在一个 flaot 类型的反射对象上调用 Int() 方法就会 panic
  • 如果想修改原始的变量,创建 reflect.Value 时需要传入原始变量的指针。
  • 如果 reflect.ValueIsValid() 方法返回 false,那么它就是一个无效的反射对象,调用它的任何方法都会 panic,除了 String 方法。
  • 对于基于值创建的 reflect.Value,如果想要修改它的值,我们无法调用这个反射对象的 Set* 方法,因为修改一个变量的拷贝没有任何意义。
  • 同时,我们也无法通过 reflect.Value 去修改结构体中未导出的字段,即使我们创建 reflect.Value 时传入的是结构体的指针。
  • Elem() 只可以在指针或者 interface 类型的反射对象上调用,否则会 panic,它的作用是获取指针指向的对象的反射对象,又或者获取接口 data 的反射对象。
  • reflect.Valuereflect.Type 都有很多类型特定的方法,比如 Field()Call() 等,这些方法只能在某些类型的反射对象上调用,否则会 panic

到此这篇关于一文带你了解Golang中reflect反射的常见错误的文章就介绍到这了,更多相关Golang reflect反射内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入了解Golang中reflect反射的使用

    目录 1. 介绍 2. 方法示例 2.1 通过反射获取对象的键(类型)和值 2.2 反射对象的类型和属性 3. 反射对Json的操作 3.1 反射与Json属性解析 3.2 Json包的序列化与反序列化 4. 实战巩固 4.1 需求 4.2 代码实现 1. 介绍 在反射的世界里,我们拥有了获取一个对象的类型,属性及方法的能力. 在 Go 反射的世界里,有两种类型非常重要,是整个反射的核心,在学习 reflect 包的使用时,先得学习下这两种类型: reflect.Type reflect.Val

  • 详解Golang利用反射reflect动态调用方法

    编程语言中反射的概念 在计算机科学领域,反射是指一类应用,它们能够自描述和自控制.也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义. 每种语言的反射模型都不同,并且有些语言根本不支持反射.Golang语言实现了反射,反射机制就是在运行时动态的调用对象的方法和属性,官方自带的reflect包就是反射相关的,只要包含这个包就可以使用. 多插一句,

  • 深入理解Golang的反射reflect示例

    目录 编程语言中反射的概念 interface 和 反射 Golang的反射reflect reflect的基本功能TypeOf和ValueOf 说明 从relfect.Value中获取接口interface的信息 已知原有类型[进行“强制转换”] 说明 未知原有类型[遍历探测其Filed] 说明 通过reflect.Value设置实际变量的值 说明 通过reflect.ValueOf来进行方法的调用 说明 Golang的反射reflect性能 小结 总结 参考链接 编程语言中反射的概念 在计算

  • Golang反射模块reflect使用方式示例详解

    Golang的反射功能,在很多场景都会用到,最基础的莫过于rpc.orm跟json的编解码,更复杂的可能会到做另外一门语言的虚拟机.通过反射模块,我们可以在编程语言的runtime运行时期间去访问内部产生对象的信息.了解反射模块的实现,对我们了解Golang对象机制本身,也是莫大的帮助. 今天,恰逢阳康+新年,就决定来探究一下Golang的反射模块——reflect. 从最基础的开始,reflect模块,以获取整数对象的类型信息为例,我们可以这么用: func TestReflect_Integ

  • golang 如何用反射reflect操作结构体

    背景 需要遍历结构体的所有field 对于exported的field, 动态set这个field的value 对于unexported的field, 通过强行取址的方法来获取该值(tricky?) 思路 下面的代码实现了从一个strct ptr对一个包外结构体进行取值的操作,这种场合在笔者需要用到反射的场合中出现比较多 simpleStrtuctField 函数接受一个结构体指针,因为最后希望改变其值,所以传参必须是指针.然后解引用. 接下来遍历结构体的每个field, exported字段是

  • 一文带你了解Golang中interface的设计与实现

    目录 前言 接口是什么 iface 和 eface 结构体 _type 是什么 itab 是什么 生成的 itab 是怎么被使用的 itab 关键方法的实现 根据 interfacetype 和 _type 初始化 itab 接口断言过程总览(类型转换的关键) panicdottypeI 与 panicdottypeE iface 和 eface 里面的 data 是怎么来的 convT* 方法 Java 里面的小整数享元模式 总结 在上一篇文章<go interface 基本用法>中,我们了

  • 一文带你了解Golang中的WaitGroups

    目录 什么是WaitGroups 如何使用WaitGroups 为什么使用WaitGroups而不是channel 需要注意的一件事 总结 什么是WaitGroups WaitGroups是同步你的goroutines的一种有效方式.想象一下,你和你的家人一起驾车旅行.你的父亲在一个条形商场或快餐店停下来,买些食物和上厕所.你最好想等大家回来后再开车去地平线.WaitGroups帮助你做到这一点. WaitGroups是通过调用标准库中的sync包来定义的. var wg sync.WaitGr

  • 一文带你了解Golang中的并发性

    目录 什么是并发性,为什么它很重要 并发性与平行性 Goroutines, the worker Mortys Channels, the green portal 总结 并发是一个很酷的话题,一旦你掌握了它,就会成为一笔巨大的财富.说实话,我一开始很害怕写这篇文章,因为我自己直到最近才对并发性不太适应.我已经掌握了基础知识,所以我想帮助其他初学者学习Go的并发性.这是众多并发性教程中的第一篇,请继续关注更多的教程. 什么是并发性,为什么它很重要 并发是指在同一时间运行多个事物的能力.你的电脑有

  • 一文带你了解Golang中select的实现原理

    目录 概述 结构 现象 非阻塞的收发 随机执行 编译 直接阻塞 独立情况 非阻塞操作 通用情况 运行时 初始化 循环 总结 概述 select是go提供的一种跟并发相关的语法,非常有用.本文将介绍 Go 语言中的 select 的实现原理,包括 select 的结构和常见问题.编译期间的多种优化以及运行时的执行过程. select 是一种与 switch 非常相似的控制结构,与 switch 不同的是,select 中虽然也有多个 case,但是这些 case 中的表达式都必须与 Channel

  • 一文带你掌握Golang Interface原理和使用技巧

    目录 1. interface 的基本概念 2. interface 的原理 3. interface 的使用技巧 3.1 使用空接口 3.2 使用类型断言 3.3 使用类型switch 3.4 使用接口组合 3.5 将方法定义在interface类型中 3.6 使用匿名接口嵌套 4. interface 的常见使用场景 4.1 依赖注入 4.2 测试驱动开发 4.3 框架设计 5. 总结 Golang 中的 interface 是一种非常重要的特性,可以让我们写出更加灵活的代码.interfa

  • 一文带你理解 Vue 中的生命周期

    目录 1.beforeCreate & created 2.beforeMount & mounted 3.beforeUpdate & updated 4.beforeDestroy & destroyed 5.activated & deactivated 前言: 每个 Vue 实例在被创建之前都要经过一系列的初始化过程.例如需要设置数据监听.编译模板.挂载实例到 DOM.在数据变化时更新 DOM 等.同时在这个过程中也会运行一些叫做生命周期钩子的函数,给予用户

  • 一文带你掌握Java8中Lambda表达式 函数式接口及方法构造器数组的引用

    目录 函数式接口概述 函数式接口示例 1.Runnable接口 2.自定义函数式接口 3.作为参数传递 Lambda 表达式 内置函数式接口 Lambda简述 Lambda语法 方法引用 构造器引用 数组引用 函数式接口概述 只包含一个抽象方法的接口,称为函数式接口. 可以通过 Lambda 表达式来创建该接口的对象. 可以在一个接口上使用 @FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口.同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口.

  • 一文带你了解Java中的ForkJoin

    目录 什么是ForkJoin? ForkJoinTask 任务 ForkJoinPool 线程池 工作窃取算法 构造方法 提交方法 创建工人(线程) 例:ForkJoinTask实现归并排序 ForkJoin计算流程 前言: ForkJoin是在Java7中新加入的特性,大家可能对其比较陌生,但是Java8中Stream的并行流parallelStream就是依赖于ForkJoin.在ForkJoin体系中最为关键的就是ForkJoinTask和ForkJoinPool,ForkJoin就是利用

  • 一文带你了解Java中的Object类及类中方法

    目录 1. Object类介绍 2. 重写toString方法打印对象 3. 对象比较equals方法 4. hashCode方法 1. Object类介绍 Object是Java默认提供的一个类.Java里面除了Object类,所有的类都是存在继承关系的.默认会继承Object父 类.即所有类的对象都可以使用Object的引用进行接收. 范例:使用Object接收所有类的对象 class Person{} class Student{} public class Test { public s

  • 一文带你了解Qt中槽的使用

    目录 一.建立槽和按钮之间的连接 二.槽函数的定义 一.建立槽和按钮之间的连接 connect(信号发送者,发送的信号,信号接收者,信号接收者的槽函数) 1.例子 connect(ui->pushButton,SIGNAL(clicked(bool)),this,SLOT(showinfo())); 解释: 信号反发送者:pushButton(这是一个按钮),发送信号:clicked(点击按钮),信号接收者:this(本类),信号接收者的槽函数:showinfo(点击按钮后响应的函数) 二.槽函

随机推荐