首页 > 其他分享 >(转)go深入:reflect 运行时反射

(转)go深入:reflect 运行时反射

时间:2023-04-06 21:33:56浏览次数:38  
标签:反射 struct fmt Value reflect person json go

原文:https://lingzihuan.icu/posts/go-13-depth-reflect/

啥是反射

go语言中,反射为我们提供了一种可以在运行时操作任意类型对象的能力,比如,查看一个接口变量的具体类型、看一个结构体有多少字段、修改某个字段的值等。

比如 fmt.Println

func Println(a ...interface{}) (n int, err error) {
    return Fprintln(os.Stdout, a...)
}

函数定义中有一个可变参数 a ...interface{},我们在调用的时候,可以传1个到多个参数进去。

reflect.Value 和 reflect.Type

go语言的反射定义中,任何接口都有两个部分组成:接口的具体类型,以及具体类型对应的值。如 var i in = 3,由于 interface{}可以表示任何类型,因此i可以转化为 interface{},将其当做一个接口,此时它在go反射中就表示成 <Value, Type>,其中Value为3,Type为int。

go反射中,标准库为我们提供了两种类型 reflect.Value和 reflect.Type分别表示变量的值和类型,并且可以用函数 reflect.ValueOf和 reflect.TypeOf分别获取任意对象 Value和Type。

func main(){
    var i int = 3
    iv := reflect.ValueOf(i)
    it := reflect.TypeOf(i)
    fmt.Println(iv, it)
}

reflect.Value

结构体定义

reflect.Value 可以通过 reflect.ValueOf获得,其结构体定义如下

type Value struct {
    typ *rtype
    ptr unsafe.Pointer
    flag
}

其里面的变量都是私有的,意味着我们只能使用它的方法,它的常用方法有:

//针对具体类型的系列方法
//以下是用于获取对应的值
Bool
Bytes
Complex
Float
Int
String
Uint
CanSet //是否可以修改对应的值
// 以下是用于修改对应的值
Set
SetBool
SetBytes
SetComplex
SetFloat
SetInt
SetString
Elem //获取指针指向的值,一般用于修改对应的值
//以下Field系列方法用于获取struct类型中的字段
Field
FieldByIndex
FieldByName
FieldByNameFunc
Interface //获取对应的原始类型
IsNil //值是否为nil
IsZero //值是否是零值
Kind //获取对应的类型类别,比如Array、Slice、Map等
//获取对应的方法
Method
MethodByName
NumField //获取struct类型中字段的数量
NumMethod//类型上方法集的数量
Type//获取对应的reflect.Type

可以总结为3类:

  1. 用户获取和修改对应的值

  2. 和struct类型的字段有关

  3. 和类型上的方法集有关,用于获取对应的方法

获取原始类型

我们使用 reflect.ValueOf 将任意类型的对象转为 reflect.Value,也可以通过 reflect.Interface进行逆转换

func main(){
    var i int = 3
    // int to reflect.Value
    iv := reflect.ValueOf(i)
    // reflect.Value to int
    i1 := iv.Interface().(int)
    fmt.Println(iv, i1)
}

修改对应的值

已经定义的变量可以通过反射,在运行时修改,如下所示:

func main() {
    i := 3
    ipv := reflect.ValueOf(&i)
    ipv.Elem().SetInt(4)
    fmt.Println(i)
}

需要注意的是, reflect.ValueOf 返回的是一个值的拷贝,因此,我们想要修改原始值,就需要传入指针变量

因为传入的是指针变量,所以需要调用 Elem()方法,找到这个指针指向的值,然后才能调用 Set... 方法进行修改。

所以,使用运行时修改变量值的关键点在于:

传递指针(可寻址),通过 Elem方法获取指向的值,才可以保证值可以被修改。reflect.Value为我们提供了 CanSet方法,判断是否可修改该变量。

修改struct结构体中的值,也是同理:

  • 传递strcut结构体指针,获取 reflect.ValueOf
  • 通过 Elem方法获取指针指向的值
  • 通过 Field方法获取需要修改的字段
  • 通过 Set系列方法,修改成对应的值
type person struct {
	Name string
	Age uint
}

func main(){
    p := person{Name: "mike", Age: 10}
	fmt.Println("Original person is : ", p)
	ppv := reflect.ValueOf(&p)
	ppv.Elem().FieldByName("Age").SetUint(20)	// set age to 20
    // ppv.Elem().Field(1).SetUint(20)	// 也可
	fmt.Println("person changed: ", p)
}

输出:

Original person is :  {mike 10}
person changed:  {mike 20}

小总结,通过反射修改一个值的规则:

  • 可被寻址,即需要向 reflect.ValueOf 传递一个指针作为参数
  • 如果想修改struct结构体的字段值,则对应的字段必须是可导出的,即该字段的首字母是大写的
  • 需要使用 Elem 方法获得指针指向的值,才能通过 Set 系列方法进行修改

获取对应的底层类型

可以使用 Kind 方法获取对应的底层类型:

func main(){
    p := person{"Mike", 20}
    ppv := reflect.ValueOf(&p)
    pv := reflect.ValueOf(p)
    fmt.Println(ppv.Kind(), pv.Kind())	// ptr, struct
}

Kind返回的值可以为:

type Kind uint
const (
   Invalid Kind = iota
   Bool
   Int
   Int8
   Int16
   Int32
   Int64
   Uint
   Uint8
   Uint16
   Uint32
   Uint64
   Uintptr
   Float32
   Float64
   Complex64
   Complex128
   Array
   Chan
   Func
   Interface
   Map
   Ptr
   Slice
   String
   Struct
   UnsafePointer
)

reflect.Type

我们使用 reflect.TypeOf 获取一个 reflect.Type

reflect.Value 用于于值有关的操作中,而如果是和变量类型本身有关的操作,则最好使用reflectType,如:获取结构体对应的字段名称或方法。

接口定义

reflect.Value是一个结构体,而 reflect.Type是一个接口,大部分常用方法同 reflect.Value是相同的,如下:

type Type interface {
    Implements(u Type) bool
    AssignableTo(u Type) bool
    ConvertibleTo(u Type) bool
    Comparable() bool
    
    // 同 reflect.Value功能相同
    Kind() Kind
    Method(int) Method
    MethodByName(string) (Method, bool)
    NumMethod() int
    Elem() Type
    Field(i int) StructField
    FieldByIndex(index []int) StructField
    FieldByName(name string) (StructField, bool)
    FieldByNameFunc(match func(string) bool) (StructField, bool)
    NumField() int
}

特有的方法:

  1. Implements:用于判断是否实现了该接口
  2. AssignableTo:用于判断是否可以赋值给类型u,即使用=进行赋值
  3. ConvertibleTo:用于判断是否可以转换为类型u
  4. Comparable:用于判断该类型是否可比较,使用关系运算符进行比较

遍历结构体的字段和方法

// 实现String方法
func (p person) String() string {
	return fmt.Sprintf("This person name is %s, age = %d", p.Name, p.Age)
}

func main(){
    p := person{"Mike", 20}
    pt := reflect.TypeOf(p)
    // 遍历字段
    for i := 0; i < pt.NumField(); i++ {
    	fmt.Printf("Field %d: %s\n", i+1, pt.Field(i).Name)
	}
	// 遍历方法
	for i := 0; i < pt.NumMethod(); i++ {
		fmt.Printf("Method %d: %s\n", i+1, pt.Method(i).Name)
	}
}

输出:

Field 1: Name
Field 2: Age
Method 1: String

是否实现某接口

下面检查person是否实现 fmt.Stringer 和 io.Writer 接口:

func main(){
    p := person{"Mike", 20}
    pt := reflect.TypeOf(p)
    stringerType := reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
    writerType := reflect.TypeOf((*io.Writer)(nil)).Elem()
    fmt.Println("person implement stringer: ", pt.Implements(stringerType))
    fmt.Println("person implement writer: ", pt.Implements(writerType))
}

输出:

person implement stringer:  true
person implement writer:  false

由于 fmt.Stringer 是一个接口,而传入 reflect.TypeOf 里面的必须是一个,因此需要转化一下,传入一个空接口指针 (*fmt.Stringer)(nil),然后取其对应的 Elem 进行判断。

这样的做法很少,我们一般使用断言进行判断,而不是反射,以下写法更简单:

func main(){
    p := person{"Mike", 20}
    pt := reflect.TypeOf(p)
    _, ok := pt.(fmt.Stringer)
    fmt.Println("Stringer implemented by person: ", ok)
}

通过反射调用方法

type person struct {
	Name string `json:"name"`
	Age uint	`json:"age"`
}

func (p person) Greet(who string) {
	fmt.Printf("Hello! %s is greeting to %s.\n", who, p.Name)
}

func main() {
    p := person{Name: "Mike", Age: 10}
    pv := reflect.ValueOf(p)
    method := pv.MethodByName("Greet")
	// call method
	args := []reflect.Value{
		reflect.ValueOf("Amy"),
	}
    // 相当于  p.Greet("Amy")
	method.Call(args)
    /*
    // 同样可以这样写
    pt := reflect.TypeOf(p)
    method, ok := pt.MethodByName("Greet")
    if !ok {
    	fmt.Println("Method [Greet] not exist")
	} else {
		// call method
		args := []reflect.Value{
			reflect.ValueOf(p),
			reflect.ValueOf("Amy"),
		}
        // 相当于 Greet(p, "Amy")
		method.Func.Call(args)
	}
    */
}

输出:

Hello! Amy is greeting to Mike.

字符串和结构体互换

字符串和结构体互转,最多是Json和Struct互相转换,这样的转换相当于python里面将字典序列化成为json字符串或者反序列化。

Json和Struct互转

go语言提供了一个json包,可以让我们实现json字符串和struct结构体的互转:

func main() {
    p := person{Name: "Mike", Age: 10}
    // struct 转换成字符串
    pJson, err := json.Marshal(p)
    if err == nil {
    	fmt.Printf("pJson string : %s\n", pJson)
	}
	// Json 字符串转化为struct结构体
	jsonString := "{\"Name\": \"Jack\", \"Age\": 20}"
	if err := json.Unmarshal([]byte(jsonString), &p); err == nil {
		fmt.Printf("Unmarshaled struct: %s\n", p)
	} else {
		fmt.Printf("Error unmarshal: %e\n", err)
	}
}

输出:

pJson string : {"Name":"Mike","Age":10}
Unmarshaled struct: This person name is Jack, age = 20
  • 通过 json.Marshal 将struct转化成 字符串(返回的是 []byte)
  • 通过 json.Unmarshal 将json字符串([]byte)转化成 strcut结构体

需要注意的是,如果 person 的定义中,含有私有成员变量(小写开头),那么在json序列化和反序列化的过程中,将不会解析/赋值该字段。

Struct Tag

Struct Tag是struct结构体字段的标签,用其辅助完成一些额外的操作,如果 json和struct 互转,使用tag让json化的字段变成小写:

type person struct {
	Name string `json:"name"`
	Age uint	`json:"age"`
}

func main() {
    p := person{Name: "Mike", Age: 10}
    // struct 转换成字符串
    pJson, err := json.Marshal(p)
    if err == nil {
    	fmt.Printf("pJson string : %s\n", pJson)
	}
	// Json 字符串转化为struct结构体
    jsonString := "{\"name\": \"Jack\", \"age\": 20}"
	if err := json.Unmarshal([]byte(jsonString), &p); err == nil {
		fmt.Printf("Unmarshaled struct: %s\n", p)
	}
}

输出:

pJson string : {"name":"Mike","age":10}
Unmarshaled struct: This person name is Jack, age = 20

需要注意的是,json.Unmarshal传入的字符串,如果存在 "{\"age\": 10, \"Age\": 20}" 两个字段,那么在反序列化之后,得到的结构体对应的值是最后一个,即20。

我们通过反射获取结构体的tag,通过 Field方法返回一个StructField,然后取Tag.Get 获取对应的tag

func main() {
    p := person{Name: "Mike", Age: 10}
    pt := reflect.TypeOf(p)
    for i := 0; i < pt.NumField(); i++ {
    	f := pt.Field(i)
    	fmt.Println(f.Tag.Get("json"))
	}
}

同一个结构体可以定义多个tag:

type person struct {
    Name string `json:"name" bson:"b_name"`
    Age uint `json:"age" bson:"b_age"`
}

实现Struct转Json

func StructToJson(i interface{}) string {
	builder := strings.Builder{}
	builder.WriteString("{")
	iv := reflect.ValueOf(i)
	it := reflect.TypeOf(i)
	numFields := it.NumField()
	for i := 0; i < numFields; i++ {
		f := it.Field(i)
		jTag := f.Tag.Get("json")
		builder.WriteString("\"" + jTag + "\":")
		builder.WriteString(fmt.Sprintf("\"%v\"", iv.Field(i)))
		if i < numFields - 1 {
			builder.WriteString(",")
		}
	}
	builder.WriteString("}")
	return fmt.Sprintf("%s", builder.String())
}

func main() {
    p := person{Name: "Mike", Age: 10}
    s := StructToJson(p)
    fmt.Println(s)
}

输出:

{"name":Mike,"age":10}

反射定律

反射是计算机语言中程序检视自身结构的一种方法,灵活、强大,可以绕过编译器的很多静态检查,过多使用会造成混乱。

  1. 任何接口值 interface{}都可以反射出反射对象,即 reflect.Value和reflect.Type,通过函数 reflect.ValueOf和reflect.TypeOf获得
  2. 反射对象也可以还原为 interface{}变量,即定律1 的可逆性,通过 reflect.Value的 Interface方法获得
  3. 要修改反射的对象,该值必须可设置(传入指针)

总结

在反射中,获取变量的值、修改变量的值等,优先使用 reflect.Value;获取结构体内的字段、类型拥有的方法集等,优先使用 reflect.Type

标签:反射,struct,fmt,Value,reflect,person,json,go
From: https://www.cnblogs.com/liujiacai/p/17294280.html

相关文章

  • day7 golang GMP
    大名鼎鼎的GMP模型需要自行学习的知识:进程、线程、协程、多线程、线程池、io多路复用,内核态用户态,,同步阻塞异步非阻塞等等相关知识代码是在线程中运行的,协程也是,所以当协程阻塞的时候该线程也阻塞了,其他任务就无法调度了,该线程就死了。如何解决,那就需要让协程自由的在线程中移......
  • goodFeaturesToTrack
    一、goodFeaturesToTrack1、过程:1)函数查找图像中或指定图像区域中最突出的角点(1)函数使用cornerMinEigenVal或cornerHarris计算每个源图像像素的角点质量度量。(2)函数执行非最大值抑制(保留3x3邻域中的局部最大值)。(3)最小特征值小于qualityLevel*maxx,yqualityMeasureMap(x,y)......
  • Django之models
    常用字段and非常用字段autofieldint自增列,必须填入参数primary_key=True。当model中如果没有自增列,则自动会创建一个列名为id的列。但是这个基本咋没用过,建表也都是使用的默认idIntegerField一个整数类型,范围在-2147483648to2147483647CharField这个最常用,啥都能用他,......
  • Golang之常用方法[总结]
    1.有一堆数字,如果除了一个数字以外,其他数字都出现了两次,那么如何找到出现一次的数字?nums:=[]int{1,5,1,6,5,3,6}i:=0for_,v:=rangenums{i^=v}fmt.Print(i)重点是异或的使用......
  • (转)使用 Golang 创建和读取 Excel 文件
    原文:https://juejin.cn/post/7117578016858849293摘要本文提出一种使用Golang进行Excel文件创建和读取的方案。首先对问题进行分析,引出方案的基本架构;然后分章节描述了Excelize基础库的基本用法,以及Excel数据在Golang中的表示和解析方式,并进一步提出了应对大规模数......
  • GO框架 - gin简介
    Gin是一个用Go(Golang)编写的web框架。它是一个类似于martini但拥有更好性能的API框架,由于httprouter,速度提高了近40倍。如果你是性能和高效的追求者,你会爱上Gin.快速:基于Radix树的路由,小内存占用。没有反射。可预测的API性能。支持中间件:传入的HTTP请......
  • GO框架 - beego简介
    bee工具是一个为了协助快速开发beego项目而创建的项目,通过bee您可以很容易的进行beego项目的创建、热编译、开发、测试、和部署。简单化:RESTful支持、MVC模型,可以使用bee工具快速地开发应用,包括监控代码修改进行热编译、自动化测试代码以及自动化打包部署。智能化:支......
  • GO框架 - iris简介
    专注于高性能简单流畅的API高扩展性强大的路由和中间件生态系统使用iris独特的表达主义路径解释器构建RESTfulAPI动态路径参数化或通配符路由与静态路由不冲突使用重定向选项从URL中删除尾部斜杠使用虚拟主机和子域名变得容易分组API和静态或甚至动态子域名net/http......
  • 运行 Golang 程序时让程序不会在运行结束后立即关闭命令行窗口
    在运行Golang程序时,为了让程序不会在运行结束后立即关闭命令行窗口,可以采用以下两种方法:使用fmt.Scanln()函数在程序的末尾添加一行fmt.Scanln()可以暂停程序运行,等待用户在命令行中输入任何字符,程序将等待用户输入,然后再继续执行直到程序结束。packagemainimport"fm......
  • C#高级--反射详解
    原文:C#高级--反射详解_c#反射_李宥小哥的博客-CSDN博客C#高级–反射详解零、文章目录一、反射是什么1、C#编译运行过程高级语言->编译->dll/exe文件->CLR/JIT->机器码 2、原理解析metadata:元数据数据清单,记录了dll中包含了哪些东西,是一个描述。IL:中间语言,编译把高级语言编译......