深入理解 go reflect - 反射常见错误

这篇具有很好参考价值的文章主要介绍了深入理解 go reflect - 反射常见错误。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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())
	})
}

至于有哪些方法是某些类型特定的,可以参考一下我的另一篇文章《深入理解 go reflect - 反射基本原理》文章来源地址https://www.toymoban.com/news/detail-805931.html

总结

  • 在调用 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

到了这里,关于深入理解 go reflect - 反射常见错误的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • go 笔记 十二章 断言 assertion 和 反射 reflect

    断言 把一个接口类型指定为它的原始类型 反射 官方说法:在编译时不知道类型的情况下,可更新变量、运行时查看值、调用方法以及直接对他们的布局进行操作的机制,称为反射。 通俗说法:可以知道变量原始数据类型和内容、方法等,并且可以进行一定的操作 为什么要

    2024年02月17日
    浏览(49)
  • 深入了解Golang中的反射机制

    目录 反射 反射的分类 值反射 类型反射 运行时反射 编译时反射 接口反射 结构体反射 常用函数 值反射 类型反射 值反射和类型反射的区别 结构体反射 示例代码         反射是指在程序运行时动态地检查和修改对象的能力。在Go语言中,通过反射可以在运行时检查变量的

    2024年02月06日
    浏览(49)
  • Go语言入门记录:从channel的池应用、sync的Pool、benchmark、反射reflect、json处理、http、性能分析和一些编程习惯

    channel的一对一会阻塞,添加buffer不会阻塞。 buffered Channel实现对象池。 sync.Pool 的介绍。 获取时先去私有对象中获取,如果不存在就在相同Processor中的共享池中获取,如果还没有,则去其他Processor中去获取。 存放时,如果私有对象不存在,就放在私有对象中,如果存在就放在

    2024年02月10日
    浏览(49)
  • 【Java基础】深入理解反射、反射的应用(工厂模式、代理模式)

    Java 反射机制是指在 运行时动态地获取和操作类的信息、调用对象的方法和访问对象的属性的能力 。通过反射,可以在程序运行时分析和修改类的结构、行为和状态。 Java 反射机制提供了以下功能: 获取类的信息:可以获取类的名称、修饰符、父类、实现的接口等。 创建对

    2024年02月09日
    浏览(59)
  • Java中的Reflection(反射)、暴力反射

    1.1 反射的出现背景 Java程序中,所有的对象都有两种类型: 编译时类型 和 运行时类型 ,而很多时候对象的编译时类型和运行时类型 不一致 。 例如: 如上 :某些变量或形参的声明类型是 Object 类型,但是程序却需要调用该对象运行时类型的方法,该方法不是Object中的方法

    2024年02月04日
    浏览(45)
  • golang 中 go func() {} 理解

    在Golang 中,go func() {} 表示创建一个新的 Goroutine(轻量级线程),用于异步执行函数。 具体来说,go func() {} 创建了一个匿名函数(即没有函数名的函数),并在其前面加上 go,以表示该函数应该在一个新的 Goroutine 中异步执行。因此,当程序执行到该语句时,它会立即

    2024年02月15日
    浏览(40)
  • 深入理解 Golang: 网络编程

    关于计算机网络分层与 TCP 通信过程过程此处不再赘述。 考虑到 TCP 通信过程中各种复杂操作,包括三次握手,四次挥手等,多数操作系统都提供了 Socket 作为 TCP 网络连接的抽象。 Linux - Internet domain socket - SOCK_STREAM Linux 中 Socket 以 “文件描述符” FD 作为标识 在进行 Socket 通

    2024年02月11日
    浏览(41)
  • 深入理解 Golang: Goroutine 协程

    进程用来分配内存空间,是操作系统分配资源的最小单位;线程用来分配 CPU 时间,多个线程共享内存空间,是操作系统或 CPU 调度的最小单位;协程用来精细利用线程。协程就是将一段程序的运行状态打包,可以在线程之间调度。或者说将一段生产流程打包,使流程不固定在

    2024年02月11日
    浏览(81)
  • .net反射(Reflection)

    .NET 反射(Reflection)是指在运行时动态地检查、访问和修改程序集中的类型、成员和对象的能力。通过反射,你可以在运行时获取类型的信息、调用方法、访问字段和属性,以及创建对象实例,而无需在编译时知道这些类型的具体信息。 换句话说,反射可以在类的内部成员不

    2024年04月25日
    浏览(39)
  • c#反射(Reflection)

    当我们在C#中使用反射时,可以动态地获取和操作程序集、类型和成员。下面是一个简单的C#反射示例,展示了如何使用反射来调用一个类的方法: 在这个示例中,我们首先获取了MyClass的类型,并使用Activator.CreateInstance创建了一个MyClass的实例。然后,我们使用GetMethod方法获取

    2024年02月10日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包