深入理解 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 valuev.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.Valuev1 := v.FieldByName("foo")assert.False(t, v1.IsValid())// v1 是无效的,只有 String 方法可以调用// 其他方法调用都会 panicassert.Panics(t, func() {// panic: reflect: call of reflect.Value.NumMethod on zero Valuefmt.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()) // falses := struct{}{}// 获取不存在的字段fmt.Println(reflect.ValueOf(s).FieldByName("").IsValid())  // false// 获取不存在的方法fmt.Println(reflect.ValueOf(s).MethodByName("").IsValid()) // falsem := map[int]int{}// 获取 map 的不存在的 keyfmt.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 = 1v1 := reflect.ValueOf(i)assert.Panics(t, func() {// 下面这一行会 panic:// v1 没有 Field 方法fmt.Println(v1.Field(0).String())})
}

至于有哪些方法是某些类型特定的,可以参考一下我的另一篇文章《深入理解 go reflect - 反射基本原理》

总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/623287.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python 列表的高级应用

当前版本: Python 3.8.4 简介 列表(list)是Python编程语言中的基本数据类型之一,也是一个非常重要的通用序列。在其它编程语言中,它们通常被称为“数组”。可以存储多个元素,包括数字、字符串、甚至其他列…

【RPC】序列化:对象怎么在网络中传输?

今天来聊下RPC框架中的序列化。在不同的场景下合理地选择序列化方式,对提升RPC框架整体的稳定性和性能是至关重要的。 一、为什么需要序列化? 首先,我们得知道什么是序列化与反序列化。 网络传输的数据必须是二进制数据,但调用…

(菜鸟自学)搭建虚拟渗透实验室——安装Ubantu 8.10 靶机

安装Ubantu 8.10 靶机 新建虚拟机 选择Ubuntu系统 网络适配器模式选用桥接模式 镜像选用ubuntu8.10版本 点击“开启此虚拟机”以开始安装Ubuntu Linux系统 安装ubuntu 首先需要选择安装时的语言,这里选择“中文(简体)” 选择“安装…

Pushmall智能AI数字名片— —SCRM客户资源管理系统

推贴数字AI名片说明: **Pushmall智能AI数字名片:**基于靠谱人脉的地理位置服务,资源查询,数字名片定制服务、企业名片:相互引荐、提供需求;建人脉群、客户群,及简介、短视频、宣传文档、电子图…

上海亚商投顾:创业板指冲高回落 光伏、航运股逆势走强

上海亚商投顾前言:无惧大盘涨跌,解密龙虎榜资金,跟踪一线游资和机构资金动向,识别短期热点和强势个股。 一.市场情绪 沪指1月12日冲高回落,创业板指午后跌近1%。北证50指数跌超6%,倍益康、华信永道、众诚科…

Python基础知识:整理15 列表的sort方法

1 sorted() 方法 之前我们学习过 sorted() 方法,可以对列表、元组、集合及字典进行排序 # 1.列表 ls [1, 10, 8, 4, 5] ls_new sorted(ls, reverseTrue) print(ls_new) …

定时任务---Timer

什么是Timer Timer是Java中的一个类,用于在指定的时间间隔内执行重复的任务。它可以在后台线程中运行,不影响主线程的执行。 Timer类可以安排一个任务在未来的某个时间开始执行,或者以固定的时间间隔重复执行。它提供了一些方法来安排任…

【软件测试】学习笔记-性能测试的基本方法与应用领域

这篇文章探讨并发用户数、响应时间和系统吞吐量这三个指标之间的关系和约束,性能测试七种常用方法,以及四大应用领域。 由于性能测试是一个很宽泛的话题,所以不同的人对性能测试的看法也不完全一样,同样一种方法可能也会有不同的…

基于鸿蒙HarmonyOS 元服务开发一款公司运营应用(ArkTS API 9)

前言 最近基于Harmony OS最新版本开发了一个作品,本文来详细讲解一下,如何我是如何开发这个作品的。以及如何使用OpenHarmony,基于ArkTS,API 9来开发一个属于自己的元服务。 废话不多说,我的作品名称叫做Company Oper…

Softmax回归(多类分类模型)

目录 1.对真实值类别编码:2.预测值:3.目标函数要求:4.使用Softmax模型将输出置信度Oi计算转换为输出匹配概率y^i:5.使用交叉熵作为损失函数:6.代码实现: 1.对真实值类别编码: y为真实值&#xf…

统计学假设检验方法简单介绍

统计学的假设检验方法有以下几种: 1. 单样本 t 检验:用于检验一个样本均值是否与给定的理论值相等。 2. 独立样本 t 检验:用于比较两个独立样本的均值是否相等。 3. 配对样本 t 检验:用于比较同一组样本的两个相关变量的均值是…

【闪电侠学netty】第10章 实现客户端与服务端收发消息

【Netty】读书笔记 - 跟闪电侠学 1. 内容概要 1.1 总结 1.1.1 该章节的业务逻辑 客户端登录 -- 服务器 登录成功 -- 开启线程 -- 如果登录成功,将输入内容转换成MessageRequestPacket指令发送服务器登录重连(省略) 服务器 -- 处理 …

Python——VScode安装

⼀、下载安装 [root192 ~]# rpm --import https://packages.microsoft.com/keys/microsoft.asc[root192 ~]# sh -c echo -e "[code]\nnameVisualStudio Code\nbaseurlhttps://packages.microsoft.com/yumrepos/vscode\nenabled1\ngpgcheck1\ngpgkeyhttps://packages.mi…

PXIe‑6378国产替代,16路AI(16位,3.5 MS/s/ch),4路AO,48路DIO,PXI多功能I/O模块

PXIe,16路AI(16位,3.5 MS/s/ch),4路AO,48路DIO,PXI多功能I/O模块 PXIe‑6378是一款同步采样的多功能DAQ设备。 该模块提供了模拟 I/O、数字I/O、四个32位计数器和模拟和数字触发。 板载NI‑STC3…

Odrive 学习系列二:将烧录工具从ST-Link V2修改为JLink

一、背景: 通过观察odrive解压后的内容,可以看到在下面配置文件及makefile文件中的配置设置的均为openOCD + stlink v2,例如makefile中: # This is only a stub for various commands. # Tup is used for the actual compilation.BUILD_DIR = build FIRMWARE = $(BUILD_DI…

PCL ISS关键点提取(C++详细过程版)

边界提取 一、概述二、代码实现三、结果展示PCL ISS关键点提取(C++详细过程版)由CSDN点云侠原创,爬虫自重。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫。 一、概述 ISS关键点提取在PCL里有现成的调用函数,具体算法原理和实现代码见:PCL ISS关键点提…

【软件测试】学习笔记-GUI测试稳定性的关键技术

这篇文章将从实际工程应用的角度,探讨GUI测试的稳定性问题。 GUI自动化测试稳定性,最典型的表现形式就是,同样的测试用例在同样的环境上,时而测试通过,时而测试失败。 这也是影响GUI测试健康发展的一个重要障碍&#x…

基于Xilinx K7-410T的高速DAC之AD9129开发笔记(二)

引言:上一篇文章我们简单介绍了AD9129的基础知识,包括芯片的重要特性,外部接口相关的信号特性等。本篇我们重点介绍下项目中FPGA与AD9129互联的原理图设计,包括LVDS IO接口设计、时钟电路以、供电设计以及PCB设计。 LVDS数据接口设…

openssl3.2 - 官方demo学习 - encode - rsa_encode.c

文章目录 openssl3.2 - 官方demo学习 - encode - rsa_encode.c概述笔记END openssl3.2 - 官方demo学习 - encode - rsa_encode.c 概述 命令行参数 server_priv_key.pem client_priv_key.pem 这2个证书是前面certs目录里面做的 官方这个程序有bug, 给出2个证书, 还要从屏幕上输…

Python - 深夜数据结构与算法之 BloomFilter

目录 一.引言 二.BloomFilter 简介 1.Hash Table 2.Bloom Filter 3.Bloom 示意图 4.Bloom 应用 三.Bloom Filter 实现 1.Python 实现 2.Python 测试 四.总结 一.引言 布隆过滤器 BloomFilter 是位运算在工业级场景应用的典范,其通过 bit 位保存元素是否存…