深入探究 Golang 反射:功能与原理及应用

Go 出于通用性的考量,提供了反射这一功能。借助反射功能,我们可以实现通用性更强的函数,传入任意的参数,在函数内通过反射动态调用参数对象的方法并访问它的属性。举例来说,下面的bridge接口为了支持灵活调用任意函数,在运行时根据传入参数funcPtr,通过反射动态调用funcPtr指向的具体函数。

func bridge(funcPtr interface{}, args ...interface{})

再如,ORM框架函数为了支持处理任意参数对象,在运行时根据传入的参数,通过反射动态对参数对象赋值。

type User struct {Name stringAge  int32
}
user := User{}
db.FindOne(&user)

本文将深入探讨Golang反射包reflect的功能和原理。同时,我们学习某种东西,一方面是为了实践运用,另一方面则是出于功利性面试的目的。所以,本文还会为大家介绍反射的典型应用以及高频面试题。

1 关键功能

reflect包提供的功能比较多,但核心功能是把interface变量转化为反射类型对象reflect.Type和reflect.Value,并通过反射类型对象提供的功能,访问真实对象的方法和属性。本文只介绍3个核心功能,其它方法可看官方文档。

1.对象类型转换。通过TypeOf和ValueOf方法,可以将interface变量转化为反射类型对象Type和Value。通过Interface方法,可以将Value转换回interface变量。

type any = interface{}// 获取反射对象reflect.Type
// TypeOf returns the reflection Type that represents the dynamic type of i. 
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i any) Type// 获取反射对象reflect.Value
// ValueOf returns a new Value initialized to the concrete value stored in the interface i. 
// ValueOf(nil) returns the zero Value.
func ValueOf(i any) Value// 反射对象转换回interface
func (v Value) Interface() (i any)

示例如下:

package mainimport ("fmt""reflect"
)func main() {age := 18fmt.Println("type: ", reflect.TypeOf(age)) // 输出type:  intvalue := reflect.ValueOf(age)fmt.Println("value: ", value) // 输出value:  18fmt.Println(value.Interface().(int)) // 输出18
}

2.变量值设置。通过reflect.Value的SetXX相关方法,可以设置真实变量的值。reflect.Value是通过reflect.ValueOf(x)获得的,只有当x是指针的时候,才可以通过reflec.Value修改实际变量x的值。

// Set assigns x to the value v. It panics if Value.CanSet returns false. 
// As in Go, x's value must be assignable to v's type and must not be derived from an unexported field.
func (v Value) Set(x Value)
func (v Value) SetInt(x int64)
...// Elem returns the value that the interface v contains or that the pointer v points to. It panics if v's Kind is not Interface or Pointer. It returns the zero Value if v is nil.
func (v Value) Elem() Value

示例如下:

package mainimport ("fmt""reflect"
)func main() {age := 18// 通过reflect.ValueOf获取age中的reflect.Value// 参数必须是指针才能修改其值pointerValue := reflect.ValueOf(&age)// Elem和Set方法结合,相当于给指针指向的变量赋值*p=值newValue := pointerValue.Elem()newValue.SetInt(28)fmt.Println(age) // 值被改变,输出28// reflect.ValueOf参数不是指针pointerValue = reflect.ValueOf(age)newValue = pointerValue.Elem() // 如果非指针,直接panic: reflect: call of reflect.Value.Elem on int Value
}

3.方法调用。Method和MethodByName可以获取到具体的方法,Call可以实现方法调用。

// Method returns a function value corresponding to v's i'th method. 
// The arguments to a Call on the returned function should not include a receiver; 
// the returned function will always use v as the receiver. Method panics if i is out of range or if v is a nil interface value.
func (v Value) Method(i int) Value// MethodByName returns a function value corresponding to the method of v with the given name.
func (v Value) MethodByName(name string) Value// Call calls the function v with the input arguments in. For example, if len(in) == 3, v.Call(in) represents the Go call v(in[0], in[1], in[2]). 
// Call panics if v's Kind is not Func. It returns the output results as Values
func (v Value) Call(in []Value) []Value

示例如下:

package mainimport ("fmt""reflect"
)type User struct {Age int
}func (u User) ReflectCallFunc(name string) {fmt.Printf("age %d ,name %+v\n", u.Age, name)
}func main() {user := User{18}// 1. 通过reflect.ValueOf(interface)来获取到reflect.ValuegetValue := reflect.ValueOf(user)methodValue := getValue.MethodByName("ReflectCallFunc")args := []reflect.Value{reflect.ValueOf("k哥")}// 2. 通过Call调用方法methodValue.Call(args) // 输出age 18 ,name k哥
}

2 原理

Go语言反射是建立在Go类型系统和interface设计之上的,因此在聊reflect包原理之前,不得不提及Go的类型和interface底层设计。

2.1 静态类型和动态类型

在Go中,每个变量都会在编译时确定一个静态类型。所谓静态类型(static type),就是变量声明时候的类型。比如下面的变量i,静态类型是interface

var i interface{}

所谓动态类型(concrete type,也叫具体类型),是指程序运行时系统才能看见的,变量的真实类型。比如下面的变量i,静态类型是interface,但真实类型是int

var i interface{}   i = 18 

2.2 interface底层设计

对于任意一个静态类型是interface的变量,Go运行时都会存储变量的值和动态类型。比如下面的变量age,会存储值和动态类型(18, int)

var age interface{}
age = 18

2.3 reflect原理

reflect是基于interface实现的。通过interface底层数据结构的动态类型和数据,构造反射对象。

reflect.TypeOf获取interface底层的动态类型,从而构造出reflect.Type对象。通过Type,可以获取变量包含的方法、字段等信息。

// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {eface := *(*emptyInterface)(unsafe.Pointer(&i)) // eface为interface底层结构return toType(eface.typ) // eface.typ就是interface底层的动态类型
}func toType(t *rtype) Type {if t == nil {return nil}return t
}

reflect.ValueOf获取interface底层的Type和数据,封装成reflect.Value对象。

type Value struct {// typ holds the type of the value represented by a Value.typ *rtype // 动态类型// Pointer-valued data or, if flagIndir is set, pointer to data.// Valid when either flagIndir is set or typ.pointers() is true.ptr unsafe.Pointer // 数据指针// flag holds metadata about the value.// The lowest bits are flag bits://  - flagStickyRO: obtained via unexported not embedded field, so read-only//  - flagEmbedRO: obtained via unexported embedded field, so read-only//  - flagIndir: val holds a pointer to the data//  - flagAddr: v.CanAddr is true (implies flagIndir)//  - flagMethod: v is a method value.// The next five bits give the Kind of the value.// This repeats typ.Kind() except for method values.// The remaining 23+ bits give a method number for method values.// If flag.kind() != Func, code can assume that flagMethod is unset.// If ifaceIndir(typ), code can assume that flagIndir is set.flag // 标记位,用于标记此value是否是方法、是否是指针等}type flag uintptr// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero Value.
func ValueOf(i interface{}) Value {if i == nil {return Value{}}return unpackEface(i)
}// unpackEface converts the empty interface i to a Value.
func unpackEface(i interface{}) Value {// interface底层结构e := (*emptyInterface)(unsafe.Pointer(&i))// NOTE: don't read e.word until we know whether it is really a pointer or not.// 动态类型t := e.typif t == nil {return Value{}}// 标记位,用于标记此value是否是方法、是否是指针等f := flag(t.Kind())if ifaceIndir(t) {f |= flagIndir}return Value{t, e.word, f} // t为类型,e.word为数据,
}

3 应用

工作中,反射常见应用场景有以下两种:

1.不知道接口调用哪个函数,根据传入参数在运行时通过反射调用。例如以下这种桥接模式:

package mainimport ("fmt""reflect"
)// 函数内通过反射调用funcPtr
func bridge(funcPtr interface{}, args ...interface{}) {n := len(args)inValue := make([]reflect.Value, n)for i := 0; i < n; i++ {inValue[i] = reflect.ValueOf(args[i])}function := reflect.ValueOf(funcPtr)function.Call(inValue)
}func call1(v1 int, v2 int) {fmt.Println(v1, v2)
}
func call2(v1 int, v2 int, s string) {fmt.Println(v1, v2, s)
}
func main() {bridge(call1, 1, 2)         // 输出1 2bridge(call2, 1, 2, "test") // 输出1 2 test
}

2.不知道传入函数的参数类型,函数需要在运行时处理任意参数对象,这种需要对结构体对象反射。典型应用场景是ORM,orm示例如下:

package mainimport ("fmt""reflect"
)type User struct {Name stringAge  int32
}func FindOne(x interface{}) {sv := reflect.ValueOf(x)sv = sv.Elem()// 对于orm,改成从db里查出来再通过反射设置进去sv.FieldByName("Name").SetString("k哥")sv.FieldByName("Age").SetInt(18)
}func main() {user := &User{}FindOne(user)fmt.Println(*user) // 输出 {k哥 18}
}

4 高频面试题

1.reflect(反射包)如何获取字段 tag?

通过反射包获取tag。步骤如下:

  • 通过reflect.TypeOf生成反射对象reflect.Type

  • 通过reflect.Type获取Field

  • 通过Field访问tag

package mainimport ("fmt""reflect"
)type User struct {Name string `json:"name" otherTag:"name"`age  string `json:"age"`
}func main() {user := User{}userType := reflect.TypeOf(user)field := userType.Field(0)fmt.Println(field.Tag.Get("json"), field.Tag.Get("otherTag")) // 输出name namefield = userType.Field(1)fmt.Println(field.Tag.Get("json")) // 输出age
}

2.为什么 json 包不能导出私有变量的 tag?

从1中的例子中可知,反射可以访问私有变量age的tag。json包之所以不能导出私有变量,是因为json包的实现,将私有变量的tag跳过了。

func typeFields(t reflect.Type) structFields {// Scan f.typ for fields to include.for i := 0; i < f.typ.NumField(); i++ {sf := f.typ.Field(i)// 非导出成员(私有变量),忽略tagif !sf.IsExported() {// Ignore unexported non-embedded fields.continue}tag := sf.Tag.Get("json")if tag == "-" {continue}          }
}

3.json包里使用的时候,结构体里的变量不加tag能不能正常转成json里的字段?

(1)如果是私有成员,不能转,因为json包会忽略私有成员的tag信息。比如下面的demo中,User结构体中的a和b都不能json序列化。

(2)如果是公有成员。

  • 不加tag,可以正常转为json里的字段,json的key跟结构体内字段名一致。比如下面的demo,User中的C序列化后,key和结构体字段名保持一致是C。

  • 加了tag,从struct转json的时候,json的key跟tag的值一致。比如下面的demo,User中的D序列化后是d。

package mainimport ("encoding/json""fmt"
)type User struct {a string // 小写无tagb string `json:"b"` //小写+tagC string //大写无tagD string `json:"d"` //大写+tag
}func main() {u := User{a: "1",b: "2",C: "3",D: "4",}fmt.Printf("%+v\n", u) // 输出{a:1 b:2 C:3 D:4}jsonInfo, _ := json.Marshal(u)fmt.Printf("%+v\n", string(jsonInfo)) // 输出{"C":"3","d":"4"}
}

文章转载自:golang架构师k哥

原文链接:https://www.cnblogs.com/killianxu/p/18314594

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

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

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

相关文章

python一维表转二维表

一维表转二维表 import pandas as pd # 读取数据 product_df pd.read_csv(rD:\excelFile\practice\物品属性值一维表.csv,encodingutf-8) # print(product_df)# 将一维表转变二维 s pd.Series(list(product_df[属性值]),index[product_df[物品编号],product_df[属性名]]) …

GMSSL2.x编译鸿蒙静态库和动态库及使用

一、编译环境准备 1.1 开发工具 DevEco-Studio下载。 1.2 SDK下载 ​ 下载编译第三方库的SDK有两种方式&#xff0c;第一种方式从官方渠道根据电脑系统选择对应的SDK版本&#xff0c;第二种方式通过DevEco-Studio下载SDK。本文只介绍通过DevEco-Studio下载SDK的方式。 安装…

centos中zabbix安装、卸载及遇到的问题

目录 Zabbix简介Zabbix5.0和Zabbix7.0的区别监控能力方面模板和 API 方面性能、速度方面 centos7安装Zabbix(5.0)安装zabbix遇到的问题卸载Zabbix Zabbix简介 Zabbix 是一个基于 WEB 界面的提供分布式系统监视以及网络监视功能的企业级的开源解决方案。zabbix 能监视各种网络参…

大数据架构体系演进

传统离线大数据架构 ​ 21世纪初随着互联网时代的到来&#xff0c;数据量暴增&#xff0c;大数据时代到来。Hadoop生态群及衍生技术慢慢走向“舞台”&#xff0c;Hadoop是以HDFS为核心存储&#xff0c;以MapReduce&#xff08;简称MR&#xff09;为基本计算模型的批量数据处理…

MATLAB实验五:MATLAB数据分析

1. 某线路上不同时间对应的电压如下表所示&#xff1a; 1&#xff09;用 3 次多项式拟合(polyfit)该实验曲线&#xff0c;要求绘制 2 原始采样 点&#xff0c;并在 1~8 范围内&#xff0c;使用时间间隔为 0.2 的数据绘制拟合曲线。 建立一个脚本文件&#xff1a;text5_1.m 如下…

黑马JavaWeb企业级开发(知识清单)01——前端介绍,HTML实现标题:排版

文章目录 前言一、认识web前端、HTML、CSS二、VS Code开发工具&#xff08;插件弃用问题&#xff09;三、HTML结构标签介绍1. 标签页标题< title >2. 图片标签< img >1) 常见属性2) src路径书写方式 3. 标题标签< h >4. 水平分页线标签< hr > 四、用Vs…

安全的备忘录工具有哪些 安全好用的备忘录

在这个数字化的时代&#xff0c;我们的生活中充斥着各种各样的信息&#xff0c;从工作计划到个人琐事&#xff0c;从账号密码到重要日期&#xff0c;这些信息都需要我们牢记。然而&#xff0c;人的记忆毕竟有限&#xff0c;于是&#xff0c;备忘录工具成为了我们日常生活中不可…

运行 npm install 报错-4048

我在已经开发中的项目&#xff0c;执行 npm install 命令时&#xff0c;出现报错&#xff1a; 并且之前在帖子中提到的报错类型还不一样&#xff08;帖子内容如下&#xff09;&#xff1a; 运行 npm run dev 总报错_运行npm run dev报错-CSDN博客 该报错内容主要为权限导致的&…

C# 编程机器人

右边写代码&#xff0c;控制左边机器人移动 冯腾飞/编程机器人 - Gitee.com

SpringBoot框架学习笔记(五):静态资源访问、Rest风格请求处理、配置视图解析器、接收参数的相关注解详解

1 WEB开发-静态资源访问 1.1 基本介绍 &#xff08;1&#xff09;只要静态资源放在类路径的以下目录&#xff1a;/static、/public、/resources、/META-INF/resources 可以被直接访问。maven项目的类路径即为main/resources目录--对应SpringBoot源码为WebProperties.java类 …

基于STM32的PM2.5监测系统设计

目录 1、设计要求 2、系统功能 3、演示视频和实物 4、系统设计框图 5、软件设计流程图 6、原理图 7、主程序 8、总结 &#x1f91e;大家好&#xff0c;这里是5132单片机毕业设计&#xff0c;今天给大家分享的是《基于STM32的PM2.5监测系统设计》。 设备的详细功能见网…

Nginx 怎样处理请求的重试机制?

&#x1f345;关注博主&#x1f397;️ 带你畅游技术世界&#xff0c;不错过每一次成长机会&#xff01; 文章目录 Nginx 怎样处理请求的重试机制&#xff1f;一、为何需要重试机制&#xff1f;二、Nginx 中的重试机制原理三、Nginx 重试机制的配置参数四、Nginx 重试机制的实际…

GPT盘新增容量后如何扩容?

场景&#xff1a;一块5T的GPT盘&#xff0c;现有需求再加10T&#xff0c; 在虚拟化平台加10T盘后&#xff0c;机器不重启&#xff0c;执行命令 echo 1 > /sys/block/sdb/device/rescan刷新磁盘容量&#xff0c;可看到容量已刷出。 但执行fdisk /dev/sdb时&#xff0c;发现创…

《0基础》学习Python——第二十二讲__网络爬虫/<5>爬取豆瓣电影封面图

一、爬取豆瓣电影的图片封面 1、经过上节课我们所爬取的豆瓣电影的电影名、年份、国家、导演、主演、剧情&#xff0c;那么接下来我们将学习如何去爬取这些电影的图片&#xff0c;并将这些图片存放在文件夹中。 2、过程实现&#xff1a; 2.1、获取网页源码 首先还是和爬取电影名…

Air780EP-AT开发-HTTP应用指南

简介 关联文档和使用工具&#xff1a; AT固件获取AT指令手册 概述 4G模块支持HTTP和HTTPS协议&#xff0c; HTTP应用的基本流程如下&#xff1a; 1、激活PDP&#xff08;参考&#xff1a;http://oldask.openluat.com/article/937&#xff09;2、初始化HTTP服务3、设置HTTP会话…

服务器上使用Docker部署sonarQube,并集成到Jenkins实现自动化。

目标是要在目标服务器上使用docker工具部署好sonar环境&#xff0c;然后再集成到Jenkins中实现自动化的代码审查工作。 Docker 首先Dokcer的源大部分现在都用不了&#xff0c;于是我上网查询&#xff0c;终于找到了一个可用的镜像。 编辑/etc/docker/daemon.json文件&#x…

Linux中的时间函数

参考&#xff1a; 几种取时间的方法&#xff08;附代码&#xff09; Linux中gmtime和localtime的区别(time_t格式转换为tm格式) C 库函数 - time() mktime和localtime_r能在多线程环境下使用么&#xff1f; Linux_C环境编程&#xff1a;时间日期函数总结 细说时间测量RDT…

探索NVM:让Node.js开发如虎添翼的利器

文章目录 前言一、NVM简介&#xff1a;版本管理的瑞士军刀二、NVM能解决什么问题&#xff1f;三、如何使用NVM​&#xff1f;总结 前言 在这个日新月异的编程世界里&#xff0c;Node.js凭借其高效的非阻塞I/O操作和轻量级的事件驱动模型&#xff0c;成为了全栈开发、微服务架构…

音视频入门基础:H.264专题(16)——FFmpeg源码中,判断某文件是否为H.264裸流文件的实现

一、引言 通过FFmpeg命令可以判断出某个文件是否为AnnexB格式的H.264裸流&#xff1a; 所以FFmpeg是怎样判断出某个文件是否为AnnexB格式的H.264裸流呢&#xff1f;它内部其实是通过h264_probe函数来判断的。从文章《FFmpeg源码&#xff1a;av_probe_input_format3函数分析》中…

winfrom 文件自动生成

数据页面展示 添加定时器执行每个表数据的生成计划 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.IO; using System.Windows.Forms; using …