Golang 反射操作整理

前言

反射是什么? 我们平常也是经常用到, 而且这名词都用烂了, 这里就不再详细介绍了.

简单说, 就是有一个不知道是什么类型的变量, 通过反射可以获取其类型, 并可操作属性和方法.

反射的用途一般是用作生成工具方法, 比如你需要一个ToString方法, 要将变量转为字符串类型, 如果没有反射, 就需要写: ToStringInt, ToStringBool…等等, 每一个类型都要加一个方法. 而有了反射, 只需要一个ToString方法, 不管是什么类型的变量, 都扔给他就好啦.

对于PHP这种弱类型的语言来说, 如果要调用变量$a$b方法, 只需要$a->$b()即可. 而对于Golang这种强类型的语言就不能这么随意了. 故下面简单介绍一下Golang中反射的应用.

希望看完反射的用法之后, 至少以后再看相关代码不至于一脸懵逼. 甚至于需要的时候还能自己手撸一套.

使用

Golang中反射的操作定义在包reflect中. 此包中主要包括以下两个对象:

  • reflect.Type 用于获取变量的类型信息
  • reflect.Value 用于对变量的值进行操作

官方文档地址: reflect.Type reflect.Value

我们在反射中的使用, 也是基于这两个对象的.

对于反射的使用来说, 其实我们在平常使用中, 主要也就用到下面这几种操作, 大部分复杂的操作百年难得一用:

  • 变量类型和属性的操作
  • 变量方法的操作

下面就基于这两种操作进行简单演示.

变量类型和属性的操作

获取属性信息

反射中, 类型信息通过reflect.Type对象获取.

获取类型

u := struct {name    string
}{}
// 获取反射对象信息. 既 reflect.Type 对象
typeOf := reflect.TypeOf(u)
fmt.Println(typeOf)
// 变量的类型名 User
fmt.Println(typeOf.Name())
// 获取变量的底层类型. 
// 基础类型的底层类型就是它本身, 比如 int
// 而所有的自定义结构体, 底层类型都是 struct
// 所有的指针, 底层类型都是 ptr
// golang 的所有底层类型定义在 reflect/type.go 文件中. 
// 可通过 reflect.Array 常量进行定位
fmt.Println(typeOf.Kind())

但是, 别高兴的太早, 如果将变量u换成一个指针: u := &User{}. 使用上述方法就拿不到变量的类型了. 因为变量内容存储的是地址, 所以需要对该地址进行取值操作. 反射包中的取值操作方法为: reflect.TypeOf(u).Elem(). 拿到值后, 就都一样啦.

不止是指针, 包括: Array, Chan, Map, Ptr, Slice等, 其实存储的都是地址, 故都需要进行取值操作.

注意, 这里的底层类型Kind太有用了, 在通过反射处理的时候, 用户自定义类型太多了根本判断不过来, 但是其底层类型Kind一共就十几个, 相同底层类型的结构就可以使用相同的处理方式了.

结构体

type User struct {Gender int
}
u := struct {name    stringAge     intAddress struct {City    stringCountry string} `json:"address"`User
}{}/* 获取反射对象信息 */
typeOf := reflect.TypeOf(u)
// 结构体字段的数量
fmt.Println(typeOf.NumField())
// 获取第0个字段的信息, 返回 StructField 对象 (此对象下方有说明)
// 可拿到字段的 Name 等信息, 包括字段的 Type 对象
fmt.Println(typeOf.Field(0))
// 根据变量名称获取字段
fmt.Println(typeOf.FieldByName("name"))
// 获取第2个结构提的第0个元素
fmt.Println(typeOf.FieldByIndex([]int{2, 0}))/* StructField 字段对象内容 */
structField := typeOf.Field(2)
// 字段名
fmt.Println(structField.Name)
// 字段的可访问的包名
// 大写字母打头的公共字段, 都可以访问, 故此值为空
fmt.Println(structField.PkgPath)
// reflect.Type 对象
fmt.Println(structField.Type)
// 字段的标记字符串, 就是后面跟着的 `` 字符串
// 返回 StructTag 对象, 下方有说明
fmt.Println(structField.Tag)
// 字段在结构体的内存结构中偏移量, 字节
fmt.Println(structField.Offset)
// 字段在结构体中的索引
fmt.Println(structField.Index)
// 匿名字段. 结构体中的 Gender 就属于匿名字段
fmt.Println(structField.Anonymous)/* StructTag 标签内容 */
tag := structField.Tag
// 获取指定名称的标签值, 若不存在, 返回空字符串
fmt.Println(tag.Get("json"))
// 与 Get 方法区别是, 第二个参数会返回若标签是否存在存在
// 有些标签的空字符串与未定义行为不同时, 可使用此方法获取
fmt.Println(tag.Lookup("json"))

数组

切片的底层类型为Slice, 但不同切片中存储的对象类型是不一样的.

说白了, 数组其实就是一个指向首地址的指针嘛. 故要想获取数组元素的内容, 做一次取值操作就可以啦.

l := []int{1, 2}typeOf := reflect.TypeOf(l)
// 空, 这里为什么空上面说过了, 数组是一个指针
fmt.Println(typeOf.Name())
// slice
fmt.Println(typeOf.Kind())
// 获取数组元素的类型
fmt.Println(typeOf.Elem().Kind())
fmt.Println(typeOf.Elem().Name())

如果数组中存放的是结构体, 在用作结构体处理就好啦

map

m := map[string]int{"a": 1,
}
typeOf := reflect.TypeOf(m)
// map 不使用取值也可以打印名字 map[string]int 不懂
fmt.Println(typeOf.Name())
// 对象底层类型. map
fmt.Println(typeOf.Kind())
// 获取 map 的 key 的类型
fmt.Println(typeOf.Key().Kind())
// 获取 map value 的类型
fmt.Println(typeOf.Elem().Kind())

获取属性值

反射中, 对于值的操作, 都是通过reflect.Value对象实现的, 此对象通过reflect.ValueOf获取.

同时, 所有的Value对象都可以调用Interface方法, 来将其转回Interface{}对象, 然后再通过类型断言进行转换.

基础类型

基础类型的取值, GO提供了对应的方法, 使用起来也很简单.

// 基础类型取值
a := int64(3)
valueOf := reflect.ValueOf(&a)
// 取基础类型.
// 注意, 若不是相关类型, 会报错. 可查看源码
// 所有的整形, 都返回 int64, 若需要 int32, 可拿到返回值后强转
fmt.Println(valueOf.Int())
//fmt.Println(valueOf.Float())
//fmt.Println(valueOf.Uint())
// ... 等等

结构体

如果是自定义的结构体怎么取值呢? 这, 一直找到基础类型. 因为所有的自定义结构体都是有基础类型组成的嘛.

u := struct {
Name string
Age  int
}{"xiao ming", 20}
valueOf := reflect.ValueOf(u)
fmt.Println(valueOf.Field(0).String())

数组

如果是数组呢? 也很简单

l := []int{1, 2, 3}valueOf := reflect.ValueOf(l)
// 修改指定索引的值
fmt.Println(valueOf.Elem().Index(0))
// 获取数组长度
fmt.Println(valueOf.Elem().Len())

map

通过反射获取Map的值时, 取到的是Value对象, 同时要使用Value对象进行取值. 毕竟Mapkeyvalue类型都是不固定的嘛.

m := map[string]string{
"a": "1",
}
valueOf := reflect.ValueOf(m)
// 获取指定索引的值
fmt.Println(valueOf.MapIndex(reflect.ValueOf("a")))
// 若指定索引的值不存在, 会返回一个 kind 为 Invalid 的 Value 对象
fmt.Println(valueOf.MapIndex(reflect.ValueOf("c")))
//  取 map 大小
fmt.Println(valueOf.Len())
// 获取 map 的所有 key, 返回 Value 对象列表
fmt.Println(valueOf.MapKeys())
// 遍历 map 用的迭代器
mapIter := valueOf.MapRange()
mapIter.Next() // 将迭代指针直线下一个, 返回是否还有数据
fmt.Println(mapIter.Value())
fmt.Println(mapIter.Key())

属性赋值

基础类型的赋值, reflect.Value对象提供了相关方法, 都以 Set 开头.

这里注意, 只有指针类型的的变量才能被赋值. 其实很好理解, 值类型在方法调用时是通过复制传值的. 只有传递指针才能够找到原始值的内存地址进行修改.

故, 我们在赋值之前, 要调用Kind方法对其类型进行判断, 若不是通过指针创建的Value对象, 一定不能赋值.

以下所有的赋值操作, 都可以与取值操作联动进行.

基础类型

a := int64(3)
valueOf := reflect.ValueOf(a)
// 此方法用于判断 Value 对象是否可以赋值
valueOf.CanSet()
// 因为是指针, 所以这里需要进行取值操作
valueOf.Elem().SetInt(20)
fmt.Println(a)

结构体

结构体的赋值与上面的获取属性值相同, 使用指针获取Value对象, 然后对其基础类型赋值.

需要注意的一点, 结构体只有公共字段才能够通过反射进行赋值, 若赋值给一个私有字段, 会抛出异常.

u := struct {Name stringAge  int
}{"xiao ming", 20}valueOf := reflect.ValueOf(&u)
valueOf.Elem().Field(0).SetString("xiao hei")
fmt.Println(u)

数组

其基础的Set方法确实提供了很多, 但是我查了一圈, 对于数组类型怎么赋值呢? 于是我看到了这个方法:

func (v Value) Set(x Value)

这个Set方法, 接收的参数是Value对象? 那不就成了么. 注意, Set是直接进行替换, 而不是追加.

l := []int{1, 2, 3}valueOf := reflect.ValueOf(&l)
// 创建一个数组用于后面进行赋值
// 注意, 数组类型要相同
setValueOf := reflect.ValueOf([]int{4, 5})
valueOf.Elem().Set(setValueOf)
fmt.Println(l)// 修改指定索引的值
// 通过指针, 将指定索引的值取出来后, 进行赋值
valueOf.Elem().Index(0).SetInt(9)
fmt.Println(l)

map

m := map[string]string{"a": "1",
}
valueOf := reflect.ValueOf(&m)
// 给指定的 key 设置
valueOf.Elem().SetMapIndex(reflect.ValueOf("b"), reflect.ValueOf("2"))
fmt.Println(m)

创建空值 Value

除了上面的赋值操作, 还有一种不需要判断对象类型的方式, 通过方法New, 可以创建一个相同类型的空值Value对象, 返回的是一个指针的Value类型.

此操作的好处是, 在使用过程中, 完全不需要判断对象类型.

a := int64(3)
// 创建一个相同类型内容. 返回的是一个指针
fmt.Println(reflect.New(reflect.TypeOf(a)).Elem())

变量方法的操作

普通方法

普通方法指未依附与结构体的方法.

func add(a, b int) int {return a + b
}func main() {valueOf := reflect.ValueOf(add)// 构造函数参数paramList := []reflect.Value{reflect.ValueOf(2), reflect.ValueOf(3)}// 调用函数. 返回一个 Value 数组retList := valueOf.Call(paramList)// 获取返回值fmt.Println(retList[0].Int())
}

结构体方法

获取方法信息

这里需要注意, 结构体指针和对象所拥有的方法数量是不同的, 具体可看: https://hujingnb.com/archives/348

type User struct {Name string
}func (u User) GetName() string {return u.Name
}func (u *User) SetName(name string) {u.Name = name
}func main() {u := User{}typeOf := reflect.TypeOf(&u)// 获取结构体中方法数量. 私有方法是拿不到的fmt.Println(typeOf.NumMethod())// 获取第0个方法, 返回 Method 对象. 下面介绍fmt.Println(typeOf.Method(0))// 根据方法名获取, 返回 Method 对象fmt.Println(typeOf.MethodByName("GetName"))/* Method 对象 */setNameFunc, _ := typeOf.MethodByName("GetName")// 方法名fmt.Println(setNameFunc.Name)// 方法签名fmt.Println(setNameFunc.Type)fmt.Println(setNameFunc.Index)// 字段的可访问的包名. 公共方法为空fmt.Println(setNameFunc.PkgPath)
}

方法调用

type User struct {Name string
}func (u User) GetName() string {return u.Name
}func (u *User) SetName(name string) {u.Name = name
}func main() {u := User{}valueOf := reflect.ValueOf(&u)// 获取结构体中方法数量. 私有方法是拿不到的fmt.Println(valueOf.NumMethod())// 获取第0个方法, 返回 Method 对象. 下面介绍fmt.Println(valueOf.Method(0))// 根据方法名获取, 返回 Method 对象fmt.Println(valueOf.MethodByName("GetName"))/* Method 对象 */setNameFunc := valueOf.MethodByName("SetName")// 调用方法params := []reflect.Value{reflect.ValueOf("xiao ming")}setNameFunc.Call(params)// 此时对象的值已经改变fmt.Println(u)// 接收方法返回值getNameFunc := valueOf.MethodByName("GetName")fmt.Println(getNameFunc.Call([]reflect.Value{}))
}

原文地址: https://hujingnb.com/archives/676

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

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

相关文章

Wordpress不同页面显示不同小工具

问题 想做一个在右侧显示的文章目录, 使用文章目录的插件 Easy Table of Contents, 将其添加到右侧的侧边栏中, 很轻松做到了这点. 但是, 一个新的问题出现了. 这个目录的工具, 需要在文章页面显示, 而在其他页面不显示. 那么问题来了, 如何让不同的页面显示不同的侧边栏工具…

虚拟内存分页机制的地址映射

概述 在之前的文章虚拟内存对分页机制做了简单的介绍. 还有一个疑问, 那就是如何将虚存中的逻辑地址映射为物理地址呢? 今天就来简单分析一下. 对于一个分页的地址来说, 一般包含两个元素: 页号: 第几页偏移量: 当前页的第几个字节 以下以 addr_virtual(p, o)表示一个逻辑…

虚拟内存分页机制的页面置换

前言 之前简单介绍过虚拟内存是如何与物理内存进行地址映射的: 虚拟内存分页机制的地址映射, 但是仅仅地址映射是不够的, 在地址映射说过会有缺页的情况, 此时就需要操作操作系统将缺少的页加载到内存中. 但是, 如果内存满了怎么办呢? 毕竟虚拟内存一般都要大于物理内存的, 不…

Kubernetes各个组件的概念

前言 Kubernetes中的概念太多了, 什么Pod Service Deployment 等等等等, 给刚接触的我都整蒙了. 通过几天观察下来, 说一下我对各个组件的理解. 此文章仅仅对这些概念做一个简单的介绍, 不至于后面看其他文章的时候一头雾水. Node Node很好理解. 就是服务实际运行的实例, 可…

Kubernetes中Pod生命周期

在 Kubernetes中Pod是容器管理的最小单位, 有着各种各样的Pod管理器. 那么一个Pod从启动到释放, 在这期间经历了哪些过程呢? Pod自开始创建, 到正常运行, 再到释放, 其时间跨度及经历的阶段大致如下: 说一下各个阶段的作用以及是为了解决什么问题. 容器调度和下载镜像的过程就…

Kubernetes存储卷的使用

在Kubernetes中, 有这不同方式的内容挂载, 简单记录一下他们的配置方式. ConfigMap 配置内容 内容配置 apiVersion: v1 kind: ConfigMap metadata:name: test-config data: # 添加配置的 key-value 内容test-key: test-value引入 apiVersion: v1 kind: Pod spec: containe…

响应HTTP服务的shell脚本

以下内容均为第一版, 实际使用请查看最新信息, 转至: https://hujingnb.com/archives/729 前言 兄弟萌, 我实现了一个实用的小工具, 特来分享. 事情刚开始是这样的, 我需要一个脚本来实现代码仓库web hook的任务, 首先想到的是直接调用php, 但是php-fpm是以www-data用户运行…

wait函数的作用

前言 在编写C程序的时候, 通过fork函数来创建新的进程, wait函数来等待子进程结束. 那么就有一个问题了, 什么情况下父进程需要等待子进程结束后继续执行呢? 如果需要等待子进程结束, 那直接将操作放到父进程执行不就醒了么? 反正等着也是等着. 当然, 还有有一种情况, 任务…

OAuth1.0介绍

背景 为什么需要OAuth授权呢? 最典型的应用场景就是第三方登录了, 我们开发了一个网站希望用户可以QQ登录, 但是怎么能拿到用户的 QQ 信息呢? 用户将 账号密码告诉我们当然可以, 但是这样有如下隐患: 我们拿到了用户的密码, 这样很不安全. 而且任意一个应用被黑, 所有相关…

PHP 数组的内部实现

前言 这几天在翻github的时候, 碰巧看到了php的源码, 就 down 下来随便翻了翻. 地址: https://github.com/php/php-src 那么PHP中什么玩意最引人注目嘞? 一定是数组了, PHP中的数组太强大了, 于是就想着不如进去看看数组的实现部分. 这篇文章打算全程针对代码进行解读了. 以…

go1.18新特性

前言 最近突然发现golang更新版本1.18了, 于是迫不及待的来看看这个版本加了些什么新特性. 没准就有之前困扰很久的问题, 在新版本被官方解决了呢. 先简单概述一下都有些什么变化, 后面再细说: 增加泛型的支持系统库方法增加修复 bug 另外, 像"系统内核更新"这种…

base64编码原理

引出 众所周知, ASICC编码共127个, 使用了7个bit进行编码. 而文件在存储的时候是以 字节为单位, 也就是8bit. 这就难免导致有一部分编码是没有定义在ASICC编码中的. 而在网络中传输二进制数据的时候(字符串本质上也是二进制数据嘛), 如果直接传输比特流, 倒也不是不可以, 只是…

页面加载速度-合并资源文件

前言 一直觉得自己的博客站点页面加载很慢, 就想着去优化一下. 呐, 下图是一次文章页面的加载, 需要2.5s. 其中 js 文件就有18个. 众所周知, 浏览器对资源文件的并行下载数量是有限制的(不同浏览器限制不同). 也就是说, 这18个 js 文件是无法同时下载的, 再说了, 页面中还有其…

hbase/thrift/go连接失败

问题 在通过Go连接hbase的过程中, 发现 get操作可以查到数据, 但是scanner命令访问数据失败, 也没有报错, 就是单纯的查不到数据. 而且Python PHP都一切正常. 这里简单复述一下我出现问题的情况, 安装过程和网上大部分内容一致, 这里简单列一下, 只是为了查询问题时参考安装过…

常用搜索引擎及语法

在平常需要进行搜索的时候是不是只知道Google Baidu ?? 他们其实是全文搜索引擎, 还有一些特定领域的搜索引擎. 而且, 搜索时可以添加特定语法, 让你的搜索事半功倍. 本文整理各种场景下使用的搜索引擎, 以及各个搜索引擎支持的语法, 不定期进行更新. 如果你知道其他搜索引…

自旋锁与互斥锁

前言 在编程中经常需要使用到互斥. 互斥就是, 这个事情只能有一个人干, 我正在做着的时候, 别人要想做这件事就得等我做完了. 互斥的实现是通过锁的机制, 也就是我把这块锁上了, 别人就进不来了, 等我做完再把锁释放掉. 但是, 前辈们已经证明了, 要想单纯的在软件层面上实现…

printf缓冲区踩坑

问题 碰到了这样一段代码(经过简化的): #include "stdio.h" #include "unistd.h" #include "sys/wait.h"int main(){fork();printf("1\n");fork();printf("1\n");wait(NULL);return 0; }这里我们简单算一下, 结果会打印几…

进程切换时是如何保存上下文的

前言 当前操作系统大部分采用分时的进程调度, 既每个进程运行一小段时间, 然后切换到下一个进程运行, 依次往复. 当进程运行的时候是独占CPU的, 此时操作系统是无法强行介入的, 为了将执行权让出来, 就需要硬件的配合了. 硬件每个一个时钟周期(比如10ms), 就会产生一个时钟中…

GO/testing包

前言 之前在写GO单元测试的时候, 使用了这个结构testing.T. 进来无事翻了翻, 发现testing包中还有一些其他的结构体, 想来是不同用处. 没想到GO的testing包竟然默默做了这么多支持, 之前竟然不知道. 在testing包中包含一下结构体: testing.T: 这就是我们平常使用的单元测试t…

CPU的分支预测

前言 最近在进行性能调优的时候, 碰到了这样的一段代码(为了展示问题而简化的代码): <?php // 第一次运行 $start microtime(true); for ($i 0; $i < 100; $i) {for ($j 0; $j <1000; $j) {for ($k 0;$k < 10000; $k) {}} } $end microtime(true); echo fi…