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

 更新时间:2023年01月05日 08:20:07   作者:eleven26  
go 反射的错误大多数都来自于调用了一个不适合当前类型的方法, 而且,这些错误通常是在运行时才会暴露出来,而不是在编译时,如果我们传递的类型在反射代码中没有被覆盖到那么很容易就会 panic。本文就介绍一下使用 go 反射时很大概率会出现的错误,需要的可以参考一下

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.Value 的 IsValid 的返回值表示 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(例如 SetBool、SetInt)将 panic。CanSet 的条件是可寻址。

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

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

在错误的 Value 上调用 Elem 方法

reflect.Value 的 Elem() 返回 interface 的反射对象包含的值或指针反射对象指向的值。如果反射对象的 Kind 不是 reflect.Interface 或 reflect.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.Type 和 reflect.Value,它们都存在一些方法只适用于某些特定的类型,也就是说, 在 go 的反射设计中,只分为了类型和值两大类。但是实际的 go 中的类型就有很多种,比如 int、string、struct、interface、slice、map、chan、func 等等。

我们先不说 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())
   })
}

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

总结

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

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

相关文章

  • golang解析域名的步骤全纪录

    golang解析域名的步骤全纪录

    这篇文章主要给大家介绍了利用golang如何解析域名的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴,下面随着小编来一起学习学习吧
    2018-12-12
  • 深入理解go sync.Once的具体使用

    深入理解go sync.Once的具体使用

    在很多情况下,我们可能需要控制某一段代码只执行一次,go 为我们提供了 sync.Once 对象,它保证了某个动作只被执行一次,本文主要介绍了深入理解go sync.Once的具体使用,感兴趣的可以了解一下
    2024-01-01
  • golang修改结构体中的切片值方法

    golang修改结构体中的切片值方法

    这篇文章主要介绍了golang修改结构体中的切片值方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • Go语言Swagger实现为项目生成 API 文档

    Go语言Swagger实现为项目生成 API 文档

    Swagger 是一个基于 OpenAPI 规范设计的工具,用于为 RESTful API 生成交互式文档,下面小编就来介绍一下如何在 Go 项目中集成 Swagger,特别是结合 Gin 框架生成 API 文档
    2025-03-03
  • Linux环境下编译并运行go项目的全过程

    Linux环境下编译并运行go项目的全过程

    Go语言是Google的开源编程语言,广泛应用于云计算、分布式系统开发等领域,在Linux上也有大量的应用场景,这篇文章主要给大家介绍了关于Linux环境下编译并运行go项目的相关资料,需要的朋友可以参考下
    2023-11-11
  • 深入浅出Go:掌握基础知识的关键要点

    深入浅出Go:掌握基础知识的关键要点

    Go是一种开源的编程语言,由Google开发,它具有简洁、高效、并发性强的特点,适用于构建可靠的、高性能的软件系统,本文将介绍Go的基础知识,需要的朋友可以参考下
    2023-10-10
  • 浅谈Golang内存逃逸

    浅谈Golang内存逃逸

    本文主要介绍了Golang内存逃逸,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • golang 之import和package的使用

    golang 之import和package的使用

    这篇文章主要介绍了golang 之import和package的使用,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-02-02
  • Go语言中sync包使用方法教程

    Go语言中sync包使用方法教程

    在Go语言的并发编程实践中,性能优化总是绕不开的话题,下面这篇文章主要介绍了Go语言中sync包使用方法的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-03-03
  • logrus hook输出日志到本地磁盘的操作

    logrus hook输出日志到本地磁盘的操作

    这篇文章主要介绍了logrus hook输出日志到本地磁盘的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-11-11

最新评论