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

news2024/11/15 21:38:36

反射编程

reflect.TypeOf vs reflect.ValueOf

image-20230517164355957

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

image-20230517165343010

判断类型 - Kind()

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

const (
	Invalid Kind = iota
	Bool
	Int
	Int8
	Int16
	Int32
	Int64
	Uint
	Uint8
	Uint16
	Uint32
	Uint64
	// ...
)
package reflect

import (
	"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.23
	CheckType(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_identifiers
    Name    string
    PkgPath string
    Type      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 int
	Name       string
	Age        int
}

type Customer struct {
	CustomerId int
	Name       string
	Age        int
}

// 用同一个数据填充不同的结构体
// 思路:既然是不同的结构体,那么要想通用,所以参数必须是一个空接口才行。
// 因为是空接口,所有我们需要对参数类型写断言
func fillDifferentStructByData(st interface{}, data map[string]interface{}) error {
	// 先判断传过来的类型是不是指针
	if reflect.TypeOf(st).Kind() != reflect.Ptr {
		return errors.New("第一个参数必须传一个指向结构体的指针")
	}
    // 再判断指针指向的类型是否为结构体
	// Elem() 用来获取指针指向的值
	// 如果参数不是指针,会报 panic 错误
	// 如果参数值是 nil, 获取的值为 0
	if reflect.TypeOf(st).Elem().Kind() != reflect.Struct {
		return errors.New("第一个参数必须是一个结构体类型")
	}

	if data == nil {
		return errors.New("填充用的数据不能为nil")
	}

	var (
		field reflect.StructField
		ok    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 := 10
	f := *(*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 = 10
	var 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 string
	id   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))) = 1
	fmt.Printf("%+v\n", user)
}

image-20231121170209000

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

type Person struct {
	Name   string
	Age    int
	Height 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的指针指向shareBufPtr
		atomic.StorePointer(&shareBufPtr, unsafe.Pointer(&data))
	}
	// 读方法
	readDataFn := func() {
		data := atomic.LoadPointer(&shareBufPtr) // 使用原子操作读取shareBufPtr
		fmt.Println(data, *(*[]int)(data))       // 打印shareBufPtr中的数据
	}
	var wg sync.WaitGroup
	writeDataFn()
	// 启动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.coloradmin.cn/o/1246721.html

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

相关文章

视频如何去水印?怎么下载保存无水印视频?

在社交媒体平台上&#xff0c;如某音、某手等&#xff0c;你是否曾经在观看视频时&#xff0c;因为烦人的水印而感到烦恼&#xff1f;是否曾经因为水印遮挡了关键信息&#xff0c;而错过了重要的内容&#xff1f;今天&#xff0c;我要向大家介绍三种视频去水印的方法&#xff0…

深度学习图像风格迁移 - opencv python 计算机竞赛

文章目录 0 前言1 VGG网络2 风格迁移3 内容损失4 风格损失5 主代码实现6 迁移模型实现7 效果展示8 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 深度学习图像风格迁移 - opencv python 该项目较为新颖&#xff0c;适合作为竞赛课题…

飞利浦、书客、雷士的护眼台灯到底怎么选?三款台灯测评对比

随着生活水平的提高&#xff0c;相信越来越多的家庭会比较在意生活质量的提高&#xff0c;会越来越重视健康问题&#xff0c;特别是有关孩子学习方面的。面对如今青少年儿童如此高的近视率的情况下&#xff0c;很多家长会选择选购一台专业护眼台灯为孩子的视力保驾护航。 不过想…

Zookeeper 集群中是怎样选举leader的

zookeeper集群中服务器被划分为以下四种状态&#xff1a; LOOKING&#xff1a;寻找Leader状态。处于该状态的服务器会认为集群中没有Leader&#xff0c;需要进行Leader选举&#xff1b;FOLLOWING&#xff1a;跟随着状态&#xff0c;说明当前服务器角色为Follower&#xff1b;LE…

SSM个性化旅游管理系统开发mysql数据库web结构java编程计算机网页源码eclipse项目

一、源码特点 SSM 个性化旅游管理系统是一套完善的信息系统&#xff0c;结合springMVC框架完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用SSM框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有完整的源代码和数据库 &#xff0c;系统主要采用B…

前装标配搭载率突破30%,数字钥匙赛道进入「纵深战」周期

在汽车智能化进程中&#xff0c;作为传统高频应用的车钥匙&#xff0c;也在加速数字化升级。同时&#xff0c;在硬件端&#xff0c;从蓝牙、NFC到UWB等多种通讯方式的叠加效应&#xff0c;也在大幅度提升数字钥匙的用户体验。 目前&#xff0c;部分市场在售车型&#xff0c;车企…

【漏洞复现】好视通视频会议系统(fastmeeting) toDownload.do接口存在任意文件读取漏洞 附POC

漏洞描述 “好视通”是国内云视频会议知名品牌,拥有多项创新核心技术优势、多方通信服务牌照及行业全面资质 [5] ,专注为政府、公检法司、教育、集团企业等用户提供“云+端+业务全场景”解决方案。用全国产、高清流畅、安全稳定的云视频服务助力各行各业数字化转型。 其视频…

用栈实现队列的功能,用队列实现栈的功能?

我们知道队列的特点是先入先出&#xff0c;栈的特点是后入先出&#xff0c;那么如何用栈实现队列的功能&#xff0c;又如何用队列实现栈的功能呢&#xff0c;且听我一一道来 我们首先来看用栈实现队列的功能&#xff0c;首先大伙儿要知道队列和栈的特点其实是“相反”&#xf…

AnalyticDB for PostgreSQL 实时数据仓库上手指南

AnalyticDB for PostgreSQL 实时数据仓库上手指南 2019-04-016601 版权 本文涉及的产品 云原生数据仓库 ADB PostgreSQL&#xff0c;4核16G 50GB 1个月 推荐场景&#xff1a; 构建的企业专属Chatbot 立即试用 简介&#xff1a; AnalyticDB for PostgreSQL 提供企业级数…

常用数据存储格式介绍:Excel、CSV、JSON、XML

在现代数字时代&#xff0c;数据经过提炼后可以推动创新、简化运营并支持决策流程。然而&#xff0c;在提取数据之后&#xff0c;并将其加载到数据库或数据仓库之前&#xff0c;需要将数据转化为可用的数据存储格式。本文将介绍开发者常用的4种数据存储格式&#xff0c;包括 Ex…

关于鸿蒙网络请求的问题

https://developer.huawei.com/consumer/cn/forum/topic/0204136145853212268?fid0102683795438680754 鸿蒙OS 代码 import http from ohos.net.http;export const httpUtils (url: string, data: any) > {return new Promise((resolve, reject) > {let httpRequest …

Re53:读论文 How Can We Know What Language Models Know?

诸神缄默不语-个人CSDN博文目录 诸神缄默不语的论文阅读笔记和分类 论文名称&#xff1a;How Can We Know What Language Models Know? ArXiv网址&#xff1a;https://arxiv.org/abs/1911.12543 官方GitHub项目&#xff08;prompt之类的都有&#xff09;&#xff1a;https:…

问卷调查平台选择指南:哪个好用与如何选择的实用指南

问卷调查由于其成本低、数据可量化的特点&#xff0c;常被用于工作和学习中。网络的发展使得问卷调查的形式也越累越多样化&#xff0c;不少人在做问卷调查的时候可能都会提出这样一个问题——问卷调查平台哪个好用&#xff1f;怎么选择&#xff1f; 选择问卷调查平台&#xf…

【开源】基于JAVA的衣物搭配系统

项目编号&#xff1a; S 016 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S016&#xff0c;文末获取源码。} 项目编号&#xff1a;S016&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容2.1 衣物档案模块2.2 衣物搭配模块2.3 衣…

IIS 基线安全加固操作

目录 账号管理、认证授权 ELK-IIS-01-01-01 ELK-IIS-01-01-02 ELK-IIS-01-01-03 ELK-IIS-01-01-04 日志配置 ELK-IIS-02-01-01 ELK-IIS-02-01-02 ​​​​​​​ ELK-IIS-02-01-03 通信协议 ELK-IIS-03-01-01 设备其他安全要求 ELK-IIS-04-01-01 ​​​​​​​ ELK-I…

App Inventor 2 数字转文本

App Inventor 2 是弱语言类型&#xff0c;文本和数字之间不用刻意去转换&#xff0c;之间赋值就可以了。 案例&#xff1a;数字转文本 App Inventor 2 是弱语言类型&#xff0c;同理数字也能直接赋值给文本变量&#xff1a; 更多请参考&#xff1a;App Inventor 2 文本代码块…

表格视图,支持数据直接编辑丨三叠云

表格视图 路径 表单设置 >> 视图设置 功能简介 新增用户可以直接表格视图中直接点击编辑数据。管理员开启「列表编辑」后&#xff0c;用户无需再点击进入数据详情&#xff0c;可直接在列表中编辑数据&#xff0c;节约用户修改数据的时间。 使用场景&#xff1a; 通…

Win10系统Steam错误代码118的解决教程

在Win10系统中&#xff0c;用户可以打开Steam平台搜索喜欢的游戏&#xff0c;点击开始畅玩。但是&#xff0c;有用户反映自己在Steam平台上操作的时候&#xff0c;收到了错误代码118的提示&#xff0c;导致自己没有办法正常进行操作。接下来小编给大家详细介绍关于解决Win10系统…

详解Rust编程中的生命周期

1.摘要 生命周期在Rust编程中是一个重要概念, 它能确保引用像预期的那样一直有效。在Rust语言中, 每一个引用都有其生命周期, 通俗讲就是每个引用在程序执行的过程中都有其自身的作用域, 一旦离开其作用域, 其生命周期也宣告结束, 值不再有效。幸运的是, 在绝大多数时间里, 生…

Dockerfile-CentOS7.9+Python3.11.2

本文为CentOS7.9下安装Python3.11.2环境的Dockerfile # CentOS with Python3.11.2 # Author xxmail.com# build a new image with basic centos FROM centos:centos7.9.2009 # who is the author MAINTAINER xxmail.comRUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/…