Go语言3小时光速入门07——*反射

反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。

每种语言的反射模型都不同,Go语言的反射机制就是在运行时动态的调用对象的方法和属性,其中reflect包就是反射相关的,只要包含这个包就可以使用。像是go语言的gRPC等都是通过反射实现的。

*Go语言中类型设计的一些原则

在go中变量包括类型type和值value两部分。(这也是为什么nil != nil)

type包括static type和concrete type。简单来说static type是你在编码是看见的类型(如int、string),concrete type是runtime系统看见的类型

而类型断言能否成功,取决于变量的concrete type,而不是static type。因此,假设一个reader变量如果它的concrete type也实现了write函数的话,它也可以被类型断言为writer(回想前面的面向对象)。

Go的指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的type是static type),在创建变量的时候就已经确定,反射主要与Go的interface类型相关(它的type是concrete type),只有interface类型才有反射一说。

每个interface变量都有一个对应pair,pair中记录了实际变量的值和类型:(value,type)即变量的真实值和实际类型。在interface中包含两个指针分别指向类型(concrete type)和实际的值。

Reflect

go语言中反射由reflect包实现(感兴趣的话可以自行去查看相应的源码也有很详细的说明和示例,光type完整代码就3千多行就不贴在这里了...),它定义了两个重要类型:Type和Value。

在reflect中主要提供了两个方法让我们可以很容易的访问接口变量内容,分别是reflect.ValueOf() 和 reflect.TypeOf()

// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i.  ValueOf(nil) returns the zero 
func ValueOf(i interface{}) Value {...}

ValueOf用来获取输入参数接口中的数据的值,如果接口为空则返回0


// 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 {...}

TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil


reflect.TypeOf()获取pair中的type

reflect.ValueOf()获取pair中的value

如:
reflect_test.go:

/*
将“接口类型变量”转换为“反射类型对象”,反射类型指的是reflect.Type和reflect.Value这两种
 */
func TestReflect(t *testing.T){
	var num int = 123
	//reflect.TypeOf: 直接给到了我们想要的type类型,如float64、int、各种pointer、struct 等等真实的类型
	fmt.Println("type: ", reflect.TypeOf(num))
	//reflect.ValueOf:直接给到了我们想要的具体的值,如123这个具体数值,或者类似&{值1, 值2, 值3} 这样的结构体struct的值
	fmt.Println("value: ", reflect.ValueOf(num))
}

reflect.ValueOf(interface)返回一个relfect.Value变量,可以通过它本身的Interface()方法获得接口变量的真实内容,然后可以通过类型判断进行转换,转换为原有真实类型。

如果我们知道转换的原有的类型是什么,可以通过value.Interface().(类型)进行转换

realValue := value.Interface().(已知的类型)
  1. 先获取interface的reflect.Type,然后通过NumField进行遍历
  2. 再通过reflect.Type的Field获取其Field
  3. 最后通过Field的Interface()得到对应的value

如:
reflect_test.go:

func TestReflectKnowType(t *testing.T){
	var num int = 123
	//主要在go中的指针类型和类型
	refValue := reflect.ValueOf(&num)
	//这里是 *int 而不是int。同样如果上面是num那么这里就是int
	//如果类型错了会直接panic
	value := refValue.Interface().(*int)
	t.Log(value)
}

当然知道类型的情况并不多见,所以如果我们不知道类型的话就需要遍历探测其Filed。通过reflect.TypeOf()返回一个type。通过getType.NumField()获取field数量,然后通过type.Field(第几个)来进行遍历。

  1. 先获取interface的reflect.Type,然后通过NumMethod进行遍历
  2. 再分别通过reflect.Type的Method获取对应的真实的方法(函数)
  3. 最后对结果取其Name和Type得知具体的方法名
  4. 也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
  5. struct 或者 struct 的嵌套都是一样的判断处理方式

如:

type User struct {
	Id   int
	Name string
	Sex  int
}

func (u User) ReflectCall() {
	fmt.Println("233333")
}

func TestFiled(t *testing.T) {
	user := User{1, "张三", 1}
	DoFiledAndMethod(user)
}

// 通过接口来获取任意参数,然后一一揭晓
func DoFiledAndMethod(input interface{}) {

	getType := reflect.TypeOf(input)
	fmt.Println("Type : ", getType.Name())

	getValue := reflect.ValueOf(input)
	fmt.Println("Fields : ", getValue)

	// 获取方法字段
	// 1. 先获取interface的reflect.Type,然后通过NumField进行遍历
	// 2. 再通过reflect.Type的Field获取其Field
	// 3. 最后通过Field的Interface()得到对应的value
	for i := 0; i < getType.NumField(); i++ {
		field := getType.Field(i)
		value := getValue.Field(i).Interface()
		fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
	}

	// 获取方法
	// 1. 先获取interface的reflect.Type,然后通过.NumMethod进行遍历
	for i := 0; i < getType.NumMethod(); i++ {
		m := getType.Method(i)
		fmt.Printf("%s: %v\n", m.Name, m.Type)
	}
}

reflect.Value设置实际变量的值

reflect.Value是通过reflect.ValueOf(X)获得的,只有当X是指针的时候,才可以通过reflec.Value修改实际变量X的值,即:要修改反射类型的对象就一定要保证其值是“addressable”的。

我们可以通过通过pointer.Elem()去获取所指向的Value,然后对newValue.Set(xxx)进行修改设置值

newValue.CantSet()表示是否可以重新设置其值,如果输出的是true则可修改,否则不能修改

reflect_test.go:

func TestReflectSetValue(t *testing.T){
	num  := 123
	fmt.Println("num : ", num)

	// 通过reflect.ValueOf获取num中的reflect.Value,注意,参数必须是指针才能修改其值
	refValue := reflect.ValueOf(&num)
	//refValue = reflect.ValueOf(num)
	// 如果非指针,这里直接panic,
	newValue := refValue.Elem()

	fmt.Println("refValue type :", newValue.Type())
	fmt.Println("refValue settability :", newValue.CanSet())

	// 重新赋值,除了SetInt外当然还要SetString,Set...之类的
	newValue.SetInt(321)
	fmt.Println("num :", num)
}

注意:Value.Elem()的Value一定要是指针否则会直接panic,设值之前通过CanSet()检查。

reflect.ValueOf函数的调用

我们可以通过Value.MethodByName(函数名)来获取一个函数(返回一个函数的Value),然后通过call调用。

如:

type User struct {
	Id   int
	Name string
	Sex  int
}

func (u User) PrintName() {
	fmt.Println("233333")
	fmt.Println("name: ",u.Name)
}

func (u User) SetName(name string) {
	u.Name = name
	fmt.Println("name: ",u.Name)
}

func TestFiled(t *testing.T) {
	user := User{1, "张三", 1}
	DoFiledAndMethod(user)
}

// 通过接口来获取任意参数,然后一一揭晓
func DoFiledAndMethod(input interface{}) {

	getType := reflect.TypeOf(input)
	fmt.Println("Type : ", getType.Name())

	getValue := reflect.ValueOf(input)
	fmt.Println("Fields : ", getValue)
	//如果方法名错误会panic
	method := getValue.MethodByName("PrintName")
	args := make([]reflect.Value, 0)
	method.Call(args)
	//按照方法的参数顺序构建对应的参数
	method = getValue.MethodByName("SetName")
	args = []reflect.Value{reflect.ValueOf("李四")}
	method.Call(args)
}

关于反射的坏处基本都知道,破坏结构啊什么的,这里需要额外提醒的是Go中的反射非常慢(如果Java的反射算比较慢的话)。提高 golang 的反射性能
.

*unsafe.Sizeof,Alignof和Offestof

函数unsafe.Sizeof(参数): 报告传递给它的参数在内存中占用的字节长度,这个参数可以是任何类型的表达式,但是它并不会计算表达式。如:unsafe.Sizeof(int(1234567 + 7654321))
Sizeof调用返回一个uintptr类型的常量表达式,所以这个结果可以作为数组类型的维度或者用于计算其他的常量。Sizeof仅报告每个数据结构固定部分的内存占用的字节长度(比如指针或字符串的长度),但是不会报告如字符串内存这种间接内容。

函数unsafe.Alignof(参数):报告它参数类型要求的对齐方式。和sizeof一样,参数可以是任何类型的表达式,并返回一个常量。(比如:布尔类型和数值类型对齐到它们的长度最大8字节,其他类型则按字对齐)

函数unsafe.Offestof:计算成员f相对于结构体x起始地址的偏移值,如果内存有空位,也计算在内。函数的操作数必须是一个成员选择器x.f

如:在数据

var x struct{
	a bool
	b int16
	c []int
}

在64位系统中

Sizeof(x)=16Alignof(x)=4
Sizeof(x.a)=1Alignof(x.a)=1Offsetof(x.a)=0
Sizeof(x.b)=2Alignof(x.b)=2Offsetof(x.b)=2
Sizeof(x.c)=12Alignof(x.c)=4Offsetof(x.c)=4

在32位系统中

Sizeof(x)=32Alignof(x)=8
Sizeof(x.a)=1Alignof(x.a)=1Offsetof(x.a)=0
Sizeof(x.b)=2Alignof(x.b)=2Offsetof(x.b)=2
Sizeof(x.c)=12Alignof(x.c)=8Offsetof(x.c)=8

*unsafe.Pointer

我们知道指针类型写作*类型,意思是:"一个指向该类型变量的指针"。unsafe.Pointer类型是一个特殊类型的指针,可以存储任何类型的变量的地址。即它表示任意类型且可寻址的指针值,可以在不同的指针类型之间进行转换(类似 C 语言的 void * 的用途)

最要特性(作用):

  1. 任何类型的指针值都可以转换为 Pointer
  2. Pointer 可以转换为任何类型的指针值
  3. uintptr 可以转换为 Pointer
  4. Pointer 可以转换为 uintptr

基本使用场景:

因为Go语言是严格的强类型语言,所以当我们写出如下代码时,编译器会直接报错的:

unsaf_test.go:

func TestPoniter(t *testing.T){
	num := 5
	numPointer := &num
	//编译错误:
	flnum := (*float64)(numPointer)
	fmt.Println(flnum)
}

有没有什么骚操作能让他强行通过呢?上面我们知道任何类型的指针可以转换为unsafe.Pointer所以:

func TestPoniter(t *testing.T){
	num := 123
	numPointer := &num
	flnum := (*float64)(unsafe.Pointer(numPointer))
	fmt.Println(flnum)
}

另外我们可以和unsafe.Offsetof联合使用发挥更强大的功效:

type Price struct{
	name  string
	price int64
}

func TestPointAndOff(t *testing.T){
	price := Price{name: "寻非", price: 99999}
	pointer := unsafe.Pointer(&price)

	niPointer := (*string)(unsafe.Pointer(pointer))
	*niPointer = "咸鱼"

	njPointer := (*int64)(unsafe.Pointer(uintptr(pointer) + unsafe.Offsetof(price.price)))
	*njPointer = 2

	fmt.Printf("name: %s, price: %d", price.name, price.price)
}

当然这些都是些"骚操作"并不建议使用

*cgo

在一个大型混合项目中我们一般使用go作为中间层或者偏基础模块的开发工作,但有时候我们可能会需要直接调用c语言的代码,在GO语言中就提供了cgo来完成对C代码的调用。

在cgo中主要支持三种方式对C进行调用:

  1. 直接嵌套C源码
    直接在注释上写,我们可以通过C.GoString()函数来完成调用,同时可以用C.函数()的方式调用C的函数库.如:
/*
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* test_hello(const char* name){
    const char* hello=" -> hello ";
    char* result=(char*)malloc(sizeof(char)*(strlen(name))+strlen(hello));
    strcpy(result,name);
    strcat(result,hello);
    return result;
}
*/
import "C"
import "fmt"

func main() {
	fmt.Println(C.GoString(C.test_hello(C.CString("world"))));
}

  1. 引用C源码
    不用在go里面写c代码,在main函数目录下创建对应的*.c、*.h文件。然后和上面的嵌套调用一样调用即可。

  2. 动态链接库方式
    将C文件*.c、.h编译成动态链接库.so然后在Go main函数所在文件的同级目录下新建两个目录,lib和include。lib目录放入*.so, include里面放入对应的.h

cgo详细使用可以参考

👉cgo

至此Go语言的基本知识就都已经讲完了,剩下的都是一些工具包的应用等...

更新时间:2020-02-25 10:17:40

本文由 寻非 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://www.zhouning.group/archives/go语言3小时光速入门07反射
最后更新:2020-02-25 10:17:40

评论

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×