golang中的反射reflect整理

253 阅读4分钟

Reflect 整理

反射是用程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。反射可以在运行时检查类型和变量,例如:它的大小、它的方法以及它能“动态地”调用这些方法。这对于没有源代码的包尤其有用。这是一个强大的工具,除非真得有必要,否则应当避免使用或小心使用。

反射通常用于检查一个变量的值或者类型,而这里面就和接口有很大关系,我们先举一个例子:

a := 120
b := reflect.TypeOf(a)
c := reflect.ValueOf(a)
fmt.Println(b, c)

输出自然很简单,是int和120,这样看也看不出来和接口有什么关系,我们点开源码:

func TypeOf(i any) Type {
	eface := *(*emptyInterface)(unsafe.Pointer(&i))
	// Noescape so this doesn't make i to escape. See the comment
	// at Value.typ for why this is safe.
	return toType((*abi.Type)(noescape(unsafe.Pointer(eface.typ))))
}

其中**emptyInterface是Go语言中空接口(interface{}**)的内部表示方式,也就是说它先将参数转换为空接口类型,之后再输出类型

之后我查阅了点资料,补充了以下知识:

  • eface.typ 是**emptyInterface**结构中的字段,它存储了实际类型的信息。
  • unsafe.Pointer(eface.typ) 将**eface.typ**的地址转换为不安全的指针。
  • **noescape函数用于防止接口值i**逃逸(在Go中,逃逸指的是将局部变量分配到堆上),这是一种编译器优化。在这里,它确保不会出现逃逸。

我们知道 Go 是静态类型语言,比如 int、float32、[]byte,等等。每个变量都有一个静态类型,而且在编译时就确定了。 接下来来个题目:请问,变量 i和 j是相同的类型吗?

type Myint int
var i int 
var j Myint

答:不是的,二者拥有不同的静态类型,尽管二者的底层类型都是 int,但在没有类型转换的情况下是不可以相互赋值的。 Go提供了布尔、数值和字符串类型的基础类型,还有一些使用这些基础类型组成的复合类型,比如数组、结构体、指针、切片、map 和channel 等。interface也可以称为一种复合类型

接下来看看reflect包中的一些常用函数:

看以下例子:

type MyInt int

func main() {
	var a int
	var b MyInt
	a = 120
	b = 240
	aK := reflect.ValueOf(a)
	bK := reflect.ValueOf(b)
	fmt.Println(reflect.TypeOf(a), aK.Kind(), aK.Type(), aK.Interface())
	fmt.Println(reflect.TypeOf(b), bK.Kind(), bK.Type(), bK.Interface())
}

最后输出:

Untitled

我们可以看到Kind()函数返回的是变量的底层类型,Interface()函数则是还原接口值

值修改

当我们想给元素值进行修改时,我们不能直接按照下面的方式修改:

func main() {
	var b MyInt = 240
	v := reflect.ValueOf(b)
	v.SetInt(120)
	fmt.Println(v)
}

我们运行时会发现错误:

Untitled

原因就在于v是不可设置的,我们可以看看源码:

Untitled

在函数中,我们传进去的其实是x的副本,而并非真实值,所以错误,所以应该是取出地址,获取地址对应的元素,进行修改,也就是:

type MyInt int

func main() {
	var b MyInt = 240
	a := reflect.ValueOf(&b).Elem()
	fmt.Println(a.CanSet()) //是否可以修改值,true为可修改
	a.SetInt(120)
	fmt.Println(a)
}

输出:

Untitled

反射结构

有时候反射也可以是一个结构体,那么就又有一些对应函数,先从例子入手:

package main

import (
	"fmt"
	"reflect"
)

type NotknownType struct {
	s1, s2, s3 string
}

func (n NotknownType) String() string {
	return n.s1 + " - " + n.s2 + " - " + n.s3
}

// 设置变量
var secret interface{} = NotknownType{"Ada", "Go", "Oberon"}

func main() {
	value := reflect.ValueOf(secret) // <main.NotknownType Value>
	typ := reflect.TypeOf(secret)    // 输出:main.NotknownType
	// 替代项:
	// typ := value.Type()  // main.NotknownType
	fmt.Println(typ)
	knd := value.Kind() // 输出底层类型:struct
	fmt.Println(knd)

	//NumField()输出字段数量
	for i := 0; i < value.NumField(); i++ {
		fmt.Printf("Field %d: %v\n", i, value.Field(i)) //输出字段值
	}

	// 调用第一个签名在MotKnownType上的方法:
	results := value.Method(0).Call(nil)
	fmt.Println(results) // [Ada - Go - Oberon]
}

输出:

Untitled

当在上面的代码中修改值时,会panic:

//error: panic: reflect.Value.SetString using value obtained using unexported field
value.Field(i).SetString("C#")

这是因为结构中只有被导出字段(首字母大写)才是可设置的

所以我们也是跟上面差不多的操作,取地址,然后进行更改,具体示例:

package main

import (
	"fmt"
	"reflect"
)

type T struct {
	A int
	B string
}

func main() {
	t := T{23, "skidoo"}
	s := reflect.ValueOf(&t).Elem()
	typeOfT := s.Type()
	for i := 0; i < s.NumField(); i++ {
		f := s.Field(i)
		fmt.Printf("%d: %s %s = %v\n", i,
			typeOfT.Field(i).Name, f.Type(), f.Interface())
	}
	s.Field(0).SetInt(77)
	s.Field(1).SetString("Sunset Strip")
	fmt.Println("t is now", t)
}

输出:

Untitled

标准库中应用

比如我们经常使用的控制台输出函数:

Untitled

Println() 使用反射包来解析这个参数列表。所以,Println() 能够知道它每个参数的类型。因此格式化字符串中只有 %d 而没有 %u%ld,因为它知道这个参数是 unsigned 还是 long。这也是为什么 Print()Println() 在没有格式字符串的情况下还能如此漂亮地输出。

今天的小结就到这里,给自己放个假,出去放松一下