【Go语言从入门到实战】反射编程、Unsafe篇

反射编程

reflect.TypeOf vs reflect.ValueOf

image-20230517164355957

func TestTypeAndValue(t *testing.T) {var a int64 = 10t.Log(reflect.TypeOf(a), reflect.ValueOf(a))t.Log(reflect.ValueOf(a).Type())
}

image-20230517165343010

判断类型 - Kind()

当我们需要对反射回来的类型做判断时,Go 语言内置了一个枚举,可以通过 Kind() 来返回这个枚举值:

const (Invalid Kind = iotaBoolIntInt8Int16Int32Int64UintUint8Uint16Uint32Uint64// ...
)
package reflectimport ("fmt""reflect""testing"
)// 检查反射类型
// 用空接口接收任意类型
func CheckType(v interface{}) {t := reflect.TypeOf(v)switch t.Kind() {case reflect.Int, reflect.Int32, reflect.Int64:fmt.Println("Int")case reflect.Float32, reflect.Float64:fmt.Println("Float")default:fmt.Println("unknown type")}
}func TestBasicType(t *testing.T) {var f float32 = 1.23CheckType(f)
}

image-20230517164832447

利用反射编写灵活的代码

image-20230518104627148

reflect.TypeOf()reflect.ValueOf() 都有 FieldByName() 方法。

// s必须是一个 struct 类型// reflect.ValueOf()只会返回一个值
reflect.ValueOf(s).FieldByName("Name")// reflect.TypeOf()可以返回两个值,第二个值可以用来判断这个值有没有;
reflect.TypeOf(s).FieldByName("Name")

FieldByName() 方法返回的是一个 StructField 类型的值。

image-20230518104446918

我们可以通过这个 StructField 来访问 Struct Tag

type StructField struct {// Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。// 参见http://golang.org/ref/spec#Uniqueness_of_identifiersName    stringPkgPath stringType      Type      // 字段的类型Tag       StructTag // 字段的标签Offset    uintptr   // 字段在结构体中的字节偏移量Index     []int     // 用于Type.FieldByIndex时的索引切片Anonymous bool      // 是否匿名字段
}

FieldByName() 方法调用者必须是一个 struct,而不是指针,源码如下:

image-20230518104417843

// 访问 MethodByName() 必须是指针类型
reflect.ValueOf(&s).MethodByName("method_name").Call([]reflect.Value{reflect.ValueOf("new_value")})
type Employee struct {EmployeeID string// 注意后面的 struct tag 的写法,详情见第5点讲解Name string `format:"normal"`Age  int
}// 更新名字,注意这里的 e 是指针类型
func (e *Employee) UpdateName(newVal string) {e.Name = newVal
}// 通过反射调用结构体的方法
func TestInvokeByName(t *testing.T) {e := Employee{"1", "Jane", 18}// reflect.TypeOf()可以返回两个值,第二个值可以用来判断这个值有没有;// 而reflect.ValueOf()只会返回一个值t.Logf("Name: value(%[1]v), Type(%[1]T)", reflect.ValueOf(e).FieldByName("Name"))if nameField, ok := reflect.TypeOf(e).FieldByName("Name"); !ok {t.Error("Failed to get 'Name' field")} else {// 获取反射取到的字段的 tag 的值t.Log("Tag:Format", nameField.Tag.Get("format"))}// 访问 MethodByName() 必须是指针类型 reflect.ValueOf(&e).MethodByName("UpdateName").Call([]reflect.Value{reflect.ValueOf("Mike")})t.Log("After update name: ", e)
}

image-20230518104908547

Elem()

因为 FieldByName() 必须要结构体才能调用,如果参数是一个指向结构体的指针,我们需要用到 Elem() 方法,它会帮你获得指针指向的结构。

  • Elem() 用来获取指针指向的值
  • 如果参数不是指针,会报 panic 错误
  • 如果参数值是 nil,获取的值为 0
// reflect.ValueOf(demoPtr)).Elem() 返回的是字段的值
reflect.ValueOf(demoPtr).Elem()// reflect.ValueOf(st)).Elem().Type() 返回的是字段类型
reflect.ValueOf(demoPtr).Elem().Type()// 传递指针类型参数调用 FieldByName() 方法
reflect.ValueOf(demoPtr).Elem().FieldByName("Name")// 传递指针类型参数调用 FieldByName() 方法
reflect.ValueOf(demoPtr).Elem().Type().FieldByName("Name")

Struct Tag

结构体里面可以对某些字段做特殊的标记,它是一个 `key: “value”` 的格式。

image-20230518103104012

type Demo struct {// 先用这个符号(``)包起来,然后写上 key: value 的格式Name string `format:"normal"`
}

Go 内置的 Json 解析会用到 tag 来做一些标记。

反射是把双刃剑

反射是一个强大并富有表现力的工具,能让我们写出更灵活的代码。但是反射不应该被滥用,原因有以下三个:

  1. 基于反射的代码是极其脆弱的,反射中的类型错误会在真正运行的时候才会引发 panic,那很可能是在代码写完的很长时间之后。
  2. 大量使用反射的代码通常难以理解。
  3. 反射的性能低下,基于反射实现的代码通常比正常代码运行速度慢一到两个数量级。

万能程序

DeepEqual

我们都知道两个 map 类型之间是不能互相比较的,两个 slice 类型之间也不能进行比较,但是反射包中的 DeepEqual() 可以帮我们实现这个功能。

用 DeepEqual() 比较 map

// 用 DeepEqual() 比较两个 map 类型
func TestMapComparing(t *testing.T) {m1 := map[int]string{1: "one", 2: "two", 3: "three"}m2 := map[int]string{1: "one", 2: "two", 3: "three"}if reflect.DeepEqual(m1, m2) {t.Log("yes")} else {t.Log("no")}
}

image-20230519170229458

用 DeepEqual() 比较 slice

// 用 DeepEqual() 比较两个切片类型
func TestSliceComparing(t *testing.T) {s1 := []int{1, 2, 3, 4}s2 := []int{1, 2, 3, 5}if reflect.DeepEqual(s1, s2) {t.Log("yes")} else {t.Log("no")}
}

image-20230519170305142

用反射实现万能程序

场景:我们有 Employee Customer 两个结构体,二者有两个相同的字段(Name 和 Age),我们希望写一个通用的程序,可以同时填充这两个不同的结构体。

type Employee struct {EmployeeId intName       stringAge        int
}type Customer struct {CustomerId intName       stringAge        int
}// 用同一个数据填充不同的结构体
// 思路:既然是不同的结构体,那么要想通用,所以参数必须是一个空接口才行。
// 因为是空接口,所有我们需要对参数类型写断言
func fillDifferentStructByData(st interface{}, data map[string]interface{}) error {// 先判断传过来的类型是不是指针if reflect.TypeOf(st).Kind() != reflect.Ptr {return errors.New("第一个参数必须传一个指向结构体的指针")}// 再判断指针指向的类型是否为结构体// Elem() 用来获取指针指向的值// 如果参数不是指针,会报 panic 错误// 如果参数值是 nil, 获取的值为 0if reflect.TypeOf(st).Elem().Kind() != reflect.Struct {return errors.New("第一个参数必须是一个结构体类型")}if data == nil {return errors.New("填充用的数据不能为nil")}var (field reflect.StructFieldok    bool)for key, val := range data {// 如果结构体里面没有 key 这个字段,则跳过// reflect.ValueOf(st)).Elem().Type() 返回的是字段类型// reflect.ValueOf(st)).Elem().Type() 等价于 reflect.TypeOf(st)).Elem()if field, ok = reflect.TypeOf(st).Elem().FieldByName(key); !ok {continue}// 如果字段的类型相同,则用 data 的数据填充这个字段的值if field.Type == reflect.TypeOf(val) {// reflect.ValueOf(st)).Elem() 返回的是字段的值reflect.ValueOf(st).Elem().FieldByName(key).Set(reflect.ValueOf(val))}}return nil
}// 填充姓名和年龄
func TestFillNameAndAge(t *testing.T) {// 声明一个 map,用来存放数据,这些数据将会填充到 Employee 和 Customer 这两个结构体中data := map[string]interface{}{"Name": "Jane", "Age": 18}e := Employee{}// 传给通用的填充方法if err := fillDifferentStructByData(&e, data); err != nil {t.Fatal(err)}c := Customer{}// 传给通用的填充方法if err := fillDifferentStructByData(&c, data); err != nil {t.Fatal(err)}t.Log(e)t.Log(c)
}

image-20230523202241804

两个结构体的 name 和 age 都填充上了,符合预期。

不安全编程-UnSafe

不安全编程指的是 go 语言中有一个 package 叫:unsafe,它的使用场景一般是要和外部 c 程序实现的一些高效的库来进行交互。

“不安全行为”的危险性

image-20231121161423772

Go 语言中是不支持强制类型转换的,而我们一旦使用 unsafe.Pointer 拿到指针后,我们可以将它转换为任意类型的指针,这样我们是否能利用它来实现强制类型转换呢?我们可以用代码来测试一下:

func TestUnsafe(t *testing.T) {i := 10f := *(*float64)(unsafe.Pointer(&i))t.Log(unsafe.Pointer(&i))t.Log(f)
}

image-20231121162713754

可以看到结果根本不是 10,是一串数字字母的组合,所以这是非常危险的。

合理的类型转换

在 Go 语言中,不同类型的指针是不允许相互赋值的,但是通过合理地使用 unsafe 包,则可以打破这种限制。

例如:int 类型是可以进行转换赋值的。

func TestConvert1(t *testing.T) {var num int = 10var uintNum uint = *(*uint)(unsafe.Pointer(&num))var int32Num int32 = *(*int32)(unsafe.Pointer(&num))t.Log(num, uintNum, int32Num)t.Log(reflect.TypeOf(num), reflect.TypeOf(uintNum), reflect.TypeOf(int32Num))
}

image-20231121164940949

访问修改结构体私有成员变量

type User struct {name stringid   int
}func TestOperateStruct(t *testing.T) {user := new(User)user.name = "张三"fmt.Printf("%+v\n", user)// 突破第一个私有变量,因为是结构体的第一个字段,所以不需要额外的指针计算*(*string)(unsafe.Pointer(user)) = "李四"fmt.Printf("%+v\n", user)// 突破第二个私有变量,因为是第二个成员字段,需要偏移一个字符串占用的长度即 16 个字节*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(user)) + uintptr(16))) = 1fmt.Printf("%+v\n", user)
}

image-20231121170209000

当然我们可以更简单的获取到结构体变量的偏移量,这样就不需要自己计算了:

type Person struct {Name   stringAge    intHeight float64
}func TestUnSafeOffSet(t *testing.T) {nameOffset := unsafe.Offsetof(Person{}.Name)ageOffset := unsafe.Offsetof(Person{}.Age)heightOffset := unsafe.Offsetof(Person{}.Height)t.Log(nameOffset, ageOffset, heightOffset) // 输出字段的偏移量
}

image-20231121171243427

实现 []byte 和字符串的零拷贝转换

通过查看源码,可以发现 slice 切片类型和 string 字符串类型具有类似的结构。

// runtime/slice.go
type slice struct {array unsafe.Pointer	// 底层数组指针,真正存放数据的地方len   int				// 切片长度,通过 len(slice) 返回cap   int				// 切片容量,通过 cap(slice) 返回
}// runtime/string.go
type stringStruct struct {str unsafe.Pointer	// 底层数组指针len int				// 字符串长度,可以通过 len(string) 返回
}

看到这里,你是不是发现很神奇,这两个数据结构底层实现基本相同,而 slice 只是多了一个cap 字段。可以得出结论:slice 和 string 在内存布局上是对齐的,我们可以直接通过 unsafe 包进行转换,而不需要申请额外的内存空间。

代码实现

func StringToBytes(str string) []byte {var b []byte// 切片的底层数组、len字段,指向字符串的底层数组,len字段*(*string)(unsafe.Pointer(&b)) = str// 切片的 cap 字段赋值为 len(str) 的长度,切片的指针、len 字段各占8个字节,直接偏移16个字节*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&b)) + 2*uintptr(8))) = len(str)return b
}func BytesToString(data []byte) string {// 直接转换return *(*string)(unsafe.Pointer(&data))
}func TestStringAndBytesConvert(t *testing.T) {str := "hello"b := StringToBytes(str)t.Log(reflect.TypeOf(b), b)// 此时 b 已经是切片类型,我们再将它转换为string类型s := BytesToString(b)t.Log(reflect.TypeOf(s), s)
}

image-20231121172404565

符合预期。

原子类型操作

我们会用到 golang 内置 package 中的 atomic 原子操作,它提供了指针的原子操作,通常用在并发读写一块共享缓存时,保证线程安全。

我们在写数据的时候写在另外一块空间,完全写完之后,我们使用原子操作把读的指针和写的指针指向我们新写入的空间,保证下次再读的时候就是新写好的内容了。指针的切换要具有线程安全的特性。

func TestAtomic(t *testing.T) {var shareBufPtr unsafe.Pointer// 写方法writeDataFn := func() {data := []int{}for i := 0; i < 9; i++ {data = append(data, i)}// 使用原子操作将data的指针指向shareBufPtratomic.StorePointer(&shareBufPtr, unsafe.Pointer(&data))}// 读方法readDataFn := func() {data := atomic.LoadPointer(&shareBufPtr) // 使用原子操作读取shareBufPtrfmt.Println(data, *(*[]int)(data))       // 打印shareBufPtr中的数据}var wg sync.WaitGroupwriteDataFn()// 启动3个读协程,3个写协程,每个协程执行3次读/写操作for i := 0; i < 3; i++ {wg.Add(1)go func() {for i := 0; i < 3; i++ {writeDataFn()time.Sleep(time.Microsecond * 100)}wg.Done()}()wg.Add(1)go func() {for i := 0; i < 3; i++ {readDataFn()time.Sleep(time.Microsecond * 100)}wg.Done()}()}wg.Wait()
}

image-20231121191610325

使用 atomic + unsafe 来实现共享 buffer 安全的读写。

总结

通过 unsafe 包,我们可以绕过 golang 编译器的检查,直接操作地址,实现一些高效的操作。但正如 golang 官方给它的命名一样,它是不安全的,滥用的话可能会导致程序意外的崩溃。关于 unsafe 包,我们应该更关注于它的用法,生产环境不建议使用!!!

  • 笔记整理自极客时间视频教程:Go语言从入门到实战
  • UnSafe部分内容参考:go unsafe包使用指南

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

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

相关文章

【23真题】最简单的211!均分141分!

今天分享的是23年河海大学863的信号与系统试题及解析。 我猜测是由于23年太简单&#xff0c;均分都141分&#xff0c;导致24考研临时新增一门数字信号处理&#xff01;今年考研的同学赶不上这么简单的专业课啦&#xff01; 本套试卷难度分析&#xff1a;平均分为102和141分&a…

ECharts与DataV:数据可视化的得力助手

文章目录 引言一、ECharts简介优势&#xff1a;劣势&#xff1a; 二、DataV简介优势&#xff1a;劣势&#xff1a; 三、ECharts与DataV的联系四、区别与选择五、如何选择根据需求选择技术栈考虑预算和商业考虑 结论我是将军&#xff0c;我一直都在&#xff0c;。&#xff01; 引…

3.10-容器的操作

这一节讲解一下对于container我们可以进行哪些操作&#xff1f; 可以使用以下命令来停止正在运行的Docker容器&#xff1a; docker container stop <CONTAINER ID> 关于运行中的容器&#xff0c;我们可以进行的操作&#xff1a; 第一个是docker exec命令&#xff0c;这个…

实时语音克隆:5 秒内生成任意文本的语音 | 开源日报 No.84

CorentinJ/Real-Time-Voice-Cloning Stars: 43.3k License: NOASSERTION 这个开源项目是一个实时语音克隆工具&#xff0c;可以在5秒内复制一种声音&#xff0c;并生成任意文本的语音。 该项目的主要功能包括&#xff1a; 从几秒钟的录音中创建声纹模型根据给定文本使用参考…

数字化转型没钱?没人?没IT?低代码平台轻松帮你搞定

随着数字技术的不断渗透&#xff0c;数字化已经不仅仅是一个趋势&#xff0c;而是深入人心的日常生活部分。在这样的时代背景下&#xff0c;企业面临的挑战也愈发严峻&#xff1a;如何不断创新&#xff0c;满足用户日益增长的业务需求&#xff1f; 传统的开发方式&#xff0c;随…

基于单片机设计的大气气压检测装置(STC89C52+BMP180实现)

一、前言 本项目设计一个大气气压检测装置&#xff0c;该装置以单片机为基础&#xff0c;采用STC89C52作为核心控制芯片&#xff0c;结合BMP180模块作为气压传感器。大气气压&#xff0c;也就是由气体重力在大气层中产生的压力&#xff0c;其变化与天气预报、气象观测以及高度…

11.7统一功能处理

一.登录拦截器 1.实现一个普通的类,实现HeadlerInterceptor接口,重写preHeadler方法. 2.将拦截器添加到配置中,并设定拦截规则. 二.访问前缀添加 方法1: 方法2:properties 三.统一异常处理 以上返回的是空指针异常,如果是别的异常就不会识别,建议加上最终异常 . 四.统一数据格…

英语学习软件 Eudic欧路词典 mac中文版介绍说明

欧路词典 mac (Eudic) 是一个功能强大的英语学习工具&#xff0c;它包含了丰富的英语词汇、短语和例句&#xff0c;并提供了发音、例句朗读、单词笔记等功能。 Eudic欧路词典 mac 软件介绍 多语种支持&#xff1a;欧路词典支持多种语言&#xff0c;包括英语、中文、日语、法语…

uni微信小程序 map 添加padding

问题背景&#xff1a; 规划驾车线路的时候&#xff0c;使用uni的include-points指定可视范围的时候&#xff0c;会很极限。导致marker不能完全显示。 解决方法 给地图显示范围添加padding (推荐) <mapid"myMap":markers"markers":polyline"pol…

视频服务网关的三大部署(二)

视频网关是软硬一体的一款产品&#xff0c;可提供多协议&#xff08;RTSP/ONVIF/GB28181/海康ISUP/EHOME/大华、海康SDK等&#xff09;的设备视频接入、采集、处理、存储和分发等服务&#xff0c; 配合视频网关云管理平台&#xff0c;可广泛应用于安防监控、智能检测、智慧园区…

c语言习题1124

分别定义函数求圆的面积和周长。 写一个函数&#xff0c;分别求三个数当中的最大数。 写一个函数&#xff0c;计算输入n个数的乘积 一个判断素数的函数&#xff0c;在主函数输入一个整数&#xff0c;输出是否为素数的信息 写一个函数求n! ,利用该函数求1&#xff01;2&…

功率半导体器件CV测试系统

概述 电容-电压(C-V)测量广泛用于测量半导体参数&#xff0c;尤其是MOS CAP和MOSFET结构。MOS(金属-氧化物-半导体)结构的电容是外加电压的函数&#xff0c;MOS电容随外加电压变化的曲线称之为C-V曲线&#xff08;简称C-V特性&#xff09;&#xff0c;C-V 曲线测试可以方便的确…

opencv-使用 Haar 分类器进行面部检测

Haar 分类器是一种用于对象检测的方法&#xff0c;最常见的应用之一是面部检测。Haar 分类器基于Haar-like 特征&#xff0c;这些特征可以通过计算图像中的积分图来高效地计算。 在OpenCV中&#xff0c;Haar 分类器被广泛用于面部检测。以下是一个简单的使用OpenCV进行面部检测…

鸿蒙系统使用hdc_std.exe使用身份证读卡器等外设USB获得权限方法

hdc_std.exe是OpenHarmony 的命令行工具&#xff0c;由于使用的开源鸿蒙开发板上面没有文件管理器&#xff0c;所以无法通过U盘等方式进行安装.hap应用。 下面是使用hdc_std.exe安装身份证读卡器的步骤&#xff1a; 1、hdc_std.exe放桌面&#xff0c;然后WINR&#xff0c;打开…

CBTC 2023氢能展倒计时6天,最新同期会议活动Plus版发布

随着时间的推移&#xff0c;CBTC2023深圳氢能技术展览会即将拉开序幕。这场盛会将于11月30日在深圳福田会展中心盛大开幕&#xff0c;以“以储赋能&#xff0c;智造未来”为主题&#xff0c;旨在搭建一个商务交流、供需合作、创新产品发布的平台&#xff0c;让氢能全产业链之间…

详解Java中的异常体系机构(throw,throws,try catch,finally)

目录 一.异常的概念 二.异常的体系结构 三.异常的处理 异常处理思路 LBYL&#xff1a;Look Before You Leap EAFP: Its Easier to Ask Forgiveness than Permission 异常抛出throw 异常的捕获 提醒声明throws try-catch捕获处理 finally的作用 四.自定义异常类 一.异…

微信小程序:This Mini Program cannot be opened as your Weixin version is out-of-date.

项目场景&#xff1a; 问题描述 升级基础库3.2.0&#xff0c;然后PC端整个小程序都打不开了&#xff0c;点击小程序提示”This Mini Program cannot be opened as your Weixin version is out-of-date. Update Weixin to the latest version.“&#xff0c;并且点击Update Wei…

一个悄然崛起的国产软件!!AI 又进化了!!

大家好&#xff0c;我是 Jack。 AI 写代码想必很多人都体验过了&#xff0c;使用 AI 编程工具是一个大趋势&#xff0c;越早学会使用 AI 辅助你写代码&#xff0c;你的效率也会越高。 甚至有些公司已经要求员工具备 AI 编程能力。 对于学生党&#xff0c;AI 编程可以帮助我们…

MyBatisPlus总结

MyBatis-Plus时Mybatis的Best Partner MyBatis-Plus (opens new window)&#xff08;简称 MP&#xff09;是一个 MyBatis (opens new window)的增强工具&#xff0c;在 MyBatis 的基础上只做增强不做改变&#xff0c;为简化开发、提高效率而生。 特性 无侵入损耗小强大的 CR…

Android开发从0开始(广播)

应用广播 发送标准广播的三步骤 发送标准广播&#xff1a; //发送标准广播 Intent intent new Intent("com.dongnaoedu.chapter09.standard"); sendBroadcast(intent); 定义广播接受者: public class StanderdReceiver extends BroadcastReceiver { public s…