首页 > 其他分享 >对 Golang 中 reflect 反射包的示例

对 Golang 中 reflect 反射包的示例

时间:2023-07-14 10:55:47浏览次数:44  
标签:reflect 示例 type fmt Golang func Printf Type

引子


// 由于反射是基于类型系统 (type system) 的,所以先简单了解下类型系统

type MyInt int
README
var i int
var j MyInt

// 上面的 i 是 int 类型,j 是 MyInt 类型,i 和 j 是不同的静态类型,尽管他们都有相同的相关类型(这里是int)
// 它们不能互相赋值,除非通过强制的类型转换

/*
一种非常重要的类型分类是接口类型,接口代表方法的集合,只要一个值实现了接口定义的方法,那么这个接口就可以存储这个具体的值
一个著名的例子就是 io 包中的 Reader 和 Writer
*/
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

/* 
任何是实现了 Read 或 Write 方法的签名的类型就是实现了 io.Reader 或 io.Writer
也就是说一个 io.Reader 的变量可以持有任何实现了 Read 方法的值
*/
var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// ....

/* 
我们要非常清楚的知道不管 r 持有了哪种具体的值,r 的类型永远都是 io.Reader
一个非常重要的的例子就是空接口 interface{},它代表一个空的方法集合并且满足任何值,只要这个值有零或多个方法
简单来说,interface 值存储了一个赋给它的具体值和对这个值类型的描述
*/
var r io.Reader
tty,err := os.OpenFile("/dev/tty",os.O_RDWR,0)
if err != nil {
    return nil,err
}
r = tty

// 在上面这个具体例子中的 r 包含了一个 (value, type) 对,具体就是 (tty, *os.File)
// *os.File 实现了 Read 等很多方法,但是 io.Reader 的接口只允许访问 Read 方法,所以我们还可以这样做:
var w io.Writer
w = r.(io.Writer)

// 通过类型断言 "type assertion",因为 r 照样实现了 io.Writer,所以我们可以将 r 赋值给 w

https://pkg.go.dev/reflect

https://go.dev/blog/laws-of-reflection


# 理解变量的内在机制
# ● 类型信息(元信息): 是预先定义好的、静态的
# ● 值信息: 是在程序运行过程中动态变化的

# 反射和空接口 ...
# 空接口就像是能接受任何东西的容器,虽然可以通过类型断言来判断空接口变量中保存的是什么类型,但这只是比较基础的方法
# 如果需要获取变量的类型信息和值信息就得使用反射机制(反射就是动态的获取变量的类型信息和值信息的机制)
# 通过 reflect 包能获取 interface{} 变量的具体类型以及值信息(分析空接口里面的信息)
# Golang 中,反射是指程序在运行过程中分析和操作类型、变量和函数等程序结构的能力
# 通过反射可以在运行时动态的获取和修改变量的类型、值和方法等信息,而不需要在编译时就确定这些信息(是元编程的一种形式)
# 使用反射可以使代码更加灵活和通用,但是由于反射是在运行时进行的,所以它的性能相对较低
# 反射是语言非常重要的特性,但除非真的有必要,否则应避免或小心使用反射

# 在计算机科学领域,反射是指一类应用,它们能够自描述和自控制
# 也就是说这类应用通过某种机制实现对自己行为的描述 self-representation 和监测 examination
# 并根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义

# ------------------------------------------------------------------------------------------ 

# 总结:
# 本质上来说,反射就是一种检查接口变量的类型和值的机制 ...
# ● 从 interface{} 变量可以反射出反射对象
# ● 从反射对象可以获取 interface{} 变量
# ● 要修改反射对象,其值必须可设置

# 场景:
# ● 动态的创建对象、获取和修改对象的字段值、动态的调用对象的方法
# ● 序列化与反序列化,如 json、protobuf 等各种数据协议
# ● 各种数据库的 ORM 如 gorm、sqlx 等数据库中间件
# ● 配置文件解析相关的库,如 YAML、INI、...

# ------------------------------------------------------------------------------------------ 

func reflect.TypeOf(i any) reflect.Type
# 返回的是 reflect.Type 接口,表示任意变量的具体类型信息
# 该接口中定义了一些有趣的方法,例如 MethodByName 获取当前类型对应方法的引用、Implements 判断当前类型是否实现了某接口

func reflect.ValueOf(i any) reflect.Value
# 返回的是 reflect.Value 结构体,表示接口的具体值(数据的运行时表示)
# 这个结构体没有对外暴露的字段,但是提供了获取和写入数据的方法

# 反射包中的所有方法基本都是围绕着 Type 和 Value 这两个类型设计的
# 我们通过 TypeOf 和 ValueOf 可以将普通的变量转换成反射包中提供的 Type 和 Value
# 如果我们知道了一个变量的类型和值,那么就意味着知道了这个变量的全部信息
# 然后就可以使用反射包中的方法对它们进行复杂的操作了

reflect.TypeOf()


var a interface{} = 123.456

// 获取变量的类型信息
func main() {

	// 返回的是 reflect.Type 接口,该接口中定义了若干方法,可通过它们获取关于类型的所有信息
	t := reflect.TypeOf(a)              // func reflect.TypeOf(i any) reflect.Type

	// 相应值的默认格式: %v
	fmt.Printf("%v\n", t)               // float64

	// 通过该接口的 Kind 方法返回表示底层具体数据类型的常量(返回类型的类型值)
	switch t.Kind() {
	case reflect.Float64:
		fmt.Printf("arg is Float64\n")  // arg is Float64
	case reflect.String:
		fmt.Printf("arg is string\n")
	}
}

reflect.ValueOf()


// 通过反射可以在变量、interface{}、reflect.Value 三者之间相互转换 ...
// ------------------------------------------------------------------------------------------

var a string = "hello, world"

func main() {

	// 返回的是 reflect.Value 结构体类型,包含类型信息以及实际值
	v := reflect.ValueOf(a)                 // func reflect.ValueOf(i any) reflect.Value
	// 因为值是动态的,所以不仅可以获取这个变量的类型,还能获取其中存储的值,利用 v.Int()、v.String()、... 就能取得

    // 也可以用 reflect.Value 轻松得到 reflect.Type
	// 例如用 v.Type() 获取该变量的类型,这与上例中 reflect.TypeOf() 返回的接口对象的 Kind() 方法的结果相同
    fmt.Println("type:", v.Type())          // type: string

	k := v.Kind()
	switch k {
	case reflect.Int64:
		fmt.Printf("a is Int64, store value is: %d\n", v.Int())
	case reflect.String:
		fmt.Printf("a is String, store value is: %s\n", v.String()) // a is String, store value is: hello, world
	}
}

// 这里存在一个问题:
// 若传进去一个类型,使用了错误的解析,则会在运行时报错,例如将 string 类型强行的 v.Int()

reflect.Kind


// reflect.Kind 类型虽然看起来和 reflect.Type 相似,但两者有很大差异
// ------------------------------------------------------------------------------------------ 

type order struct {
	ordId      int
	customerId int
}

var o = order{
	ordId:      456,
	customerId: 56,
}
func main() {

	// 返回的是 reflect.Type 接口,该接口中定义了若干方法,可通过它们获取关于类型的所有信息
	t := reflect.TypeOf(o)  // func reflect.TypeOf(i any) reflect.Type

	// 通过接口 Kind 方法返回表示底层具体数据类型的常量(返回类型的类型值)
	k := t.Kind()           // func (reflect.Type).Kind() reflect.Kind

	fmt.Println(t)          // main.order   即 reflect.Type 代表实际的类型 main.order
	fmt.Println(k)          // struct       即 reflect.Kind 代表类型的分类 

修改变量的值


// 既然值类型是动态的,能取到保存的值同样也可以设置值
// 在反射中提供了很多 set 方法 SetFloat、SetInt()、SetString()、... 可以帮助设置类型的值

// 下例将 x 的值设为 6.28 会报错
func main() {
    var x float64 = 3.14
    v := reflect.ValueOf(x)
    v.SetFloat(6.28)
    fmt.Printf("After Set Value is %f", x)
}
// panic: reflect: reflect.Value.SetFloat using unaddressable value
// 报错结果说明是不可设置的,这是因为 x 是值类型,而值类型传递的是副本
// 因为 v := reflect.ValueOf(x) 通过传递 x 的拷贝创建了 v,所以 v 的改变并不能更改原始的 x,所以必须传递 x 的地址:
func main() {
    var x float64 = 3.14
    v := reflect.ValueOf(&x)
    // fmt.Printf("type of v is %v", v.Type())  // type of v is *float64
    v.SetFloat(6.28)
    fmt.Printf("After Set Value is %f", x)
}
// 依然会报错! 这是因为 &x 是地址了,所以它的类型就变了,通过上述注释部分看到变成了 *float64
// 正常的赋值,如果是地址的话,如下例中对 *y 赋值(这里 * 的意思就是往这个地址里面赋值)
var y *float64 = new(float64)
*y = 10.12
fmt.Printf("y = %v", *y)

// 同样,在反射里面也可以取地址,但是需要通过 Elem() 取地址
func main() {
    var x float64 = 3.14                        // x 是值类型
    v := reflect.ValueOf(&x)                    // 需要使用指针(避免传递变量的副本)
    fmt.Printf("type of v is %v\n", v.Type())   // type of v is *float64
    v.Elem().SetFloat(6.28)                     // 获取指针指向的变量,然后再赋值
    fmt.Printf("After set x is %v", x)          // After set x is 6.28
}

// ------------------------------------------------------------------------------------------ 修改结构体中字段的值

type Student struct {
	Name  string
	Sex   int
	Age   int
	Score float32
}

func main() {

	s := Student{
		Name:  "BigOrange",
		Sex:   1,
		Age:   10,
		Score: 80.1,
	}

	fmt.Printf("Name:%v, Sex:%v,Age:%v,Score:%v \n", s.Name, s.Sex, s.Age, s.Score)
	// Name:BigOrange, Sex:1,Age:10,Score:80.1

	v := reflect.ValueOf(&s)                     // 这里传的是地址
	v.Elem().Field(0).SetString("ChangeName")    // 对结构体中的字段进行赋值操作
	v.Elem().FieldByName("Score").SetFloat(99.9) // ...

	fmt.Printf("Name:%v, Sex:%v,Age:%v,Score:%v \n", s.Name, s.Sex, s.Age, s.Score)
	// Name:ChangeName, Sex:1,Age:10,Score:99.9

}

获取结构体中字段的信息


// 判断一个变量是不是结构体并获取结构体中的字段
// 通过 NumField() 获取结构体中的字段数量(从而实现遍历)
// 通过 Field() 获取结构体中字段的信息

type Student struct {
	Name  string
	Sex   int
	Age   int
	Score float32
}

func main() {

	var s Student = Student{
		Name:  "BigOrange",
		Sex:   1,
		Age:   10,
		Score: 80.1,
	}

	v := reflect.ValueOf(s) // func reflect.ValueOf(i any) reflect.Value
	t := v.Type()           // func (reflect.Value).Type() reflect.Type

	// 返回表示其底层数据类型的一个常量值
	kind := t.Kind() // func (reflect.Type).Kind() reflect.Kind
	switch kind {
	case reflect.Int64: // ...
		fmt.Printf("s is int64\n")
	case reflect.Float32: // ...
		fmt.Printf("s is int64\n")
	case reflect.Struct: // 如果是结构体类型则遍历其中所有的字段
		fmt.Printf("s is struct\n")
		fmt.Printf("field number of is %d\n", v.NumField())

		for i := 0; i < v.NumField(); i++ { // 获取结构体中字段的数量
			// 通过 Field() 取得按下标索引的字段信息,返回 reflect.Value 结构体类型的值
			field := v.Field(i) // func (reflect.Value).Field(i int) reflect.Value
			fmt.Printf("name:%s type:%v value: %v\n",
				t.Field(i).Name,     // 字段名称
				field.Type().Kind(), // 字段类型
				field.Interface(),   // 字段的值,也可以用 reflect.Value.Interface().(type) 的方式还原成最原始的值~
			)
		}

	default:
		fmt.Printf("default\n")
	}
}

/* 
s is struct
field number of is 4
name:Name type:string value: BigOrange
name:Sex type:int value: 1
name:Age type:int value: 10
name:Score type:float32 value: 80.1

--------------------------------------------------------------------------------------------- Tips
打印字段名时使用的是 t.Field(i).Name,因为 Name 是静态的,所以属于类型的信息

打印值的时候,这里将 field.Interface() 实际上相当于 ValueOf 的反操作
可参考这篇文章 https://www.cnblogs.com/baicaiyoudu/archive/2016/09/25/5905766.html,所以才能把值打印出来

如果 Student 中的 Name 字段变为 name(私有)则会报错(不能反射私有变量)
"panic: reflect.Value.Interface: cannot return value obtained from unexported field or method"
*/

获取结构体中方法的信息


type Student struct {
    Name  string
    Sex   int
    Age   int
    Score float32
}

func (s *Student) SetName(name string) {
    fmt.Printf("有参数方法,通过反射进行调用")
    s.Name = name
}

func (s *Student) PrintStudent() {
    fmt.Printf("无参数方法,通过反射进行调用")
}

// ------------------------------------------------------------------------------------------ 

func main() {

    s := Student{
        Name:  "BigOrange",
        Sex:   1,
        Age:   10,
        Score: 80.1,
    }

    v := reflect.ValueOf(&s)
    t := v.Type()
    fmt.Printf("struct student have %d methods\n", t.NumMethod())

    // 获得结构体的方法数量,然后遍历并通过 Method() 获取结构体方法的具体信息
    for i := 0; i < t.NumMethod(); i++ {
        method := t.Method(i)
        fmt.Printf("struct %d method, name:%s type: %v\n", i, method.Name, method.Type)
    }

    // 调用方法
    if method.Name == "SetName" {
        method.Func.Call([]reflect.Value{reflect.ValueOf(&s), reflect.ValueOf("1")})
    }
}

/* 
struct student have 2 methods
struct 0 method, name:PrintStudent type: func(*main.Student)
struct 1 method, name:SetName type: func(*main.Student, string)
有参数方法,通过反射进行调用

从输出中可以看到能够获取方法的名称及其签名,并且这个方法的输出顺序是按字母排列的
从输出中可以看到一个有趣的现象: 结构体的方法其实也是通过函数实现的
例如结构体方法 func(s Student) SetName(name string) 反射之后的结果是 func(main.Student, string),因此实际上是把 Student 当参数了 ...
*/

获取结构体中标签的信息


// 有时在类型上定义一些 tag,例如使用 json 和数据库时 ...
// Field() 方法返回的 StructField 结构体对象中保存着 Tag 信息,并且可以通过其 Get() 方法获取 Tag 信息

type Student struct {
    Name string `json:"jsName" db:"dbName"`
}

func main() {

	s := Student{
		Name: "BigOrange",
	}

	v := reflect.ValueOf(&s)    // func reflect.ValueOf(i any) reflect.Value
	t := v.Type()               // func (reflect.Value).Type() reflect.Type
	field0 := t.Elem().Field(0) // func (reflect.Type).Field(i int) reflect.StructField

	fmt.Printf("tag json=%s\n", field0.Tag.Get("json")) // tag json=jsName
	fmt.Printf("tag db=%s\n", field0.Tag.Get("db"))     // tag db=dbName
}

// ------------------------------------------------------------------------------------------- 获取结构体的 tag 信息

type resume struct {
	Name string `json:"name" doc:"wangyu"`
}

func findDocOnTag(stru interface{}) map[string]string {
	t := reflect.TypeOf(stru).Elem()
	doc := make(map[string]string)

	for i := 0; i < t.NumField(); i++ {
		doc[t.Field(i).Tag.Get("json")] = t.Field(i).Tag.Get("doc")
	}

	return doc
}

func main() {
	var stru resume
	doc := findDocOnTag(&stru)
	fmt.Printf("name字段为: %s\n", doc["name"]) // name字段为: wangyu
}

标签:reflect,示例,type,fmt,Golang,func,Printf,Type
From: https://www.cnblogs.com/MoonGlow/p/17553111.html

相关文章

  • diff 与 patch 命令的示例
    diff#diff以逐行的方式,比较文本文件的异同处,特别是比较两个版本不同的文件,如果指定要比较目录,则比较目录中相同文件名的文件,但不会比较其中子目录#diff的输出描述两个文件的不同,告诉用户怎样改变第一个文件之后与第二个文件保持一致(它是以"行"为单位进行比较的)##-r对比......
  • 设计模式-桥接模式在Java中的使用示例
    场景桥接模式情境引入假如我们需要大中小3种型号的画笔,能够绘制12种不同的颜色,如果使用蜡笔,需要准备3×12=36支,但如果使用毛笔的话,只需要提供3种型号的毛笔,外加12个颜料盒即可,涉及到的对象个数仅为3+12=15,远小于36,却能实现与36支蜡笔同样的功能。如果增加一种新型号的画......
  • goland打开配置golang工程
    有一个golang工程,没有go.mod,用goland打开,配置编译,会提示没有go.mod,但是增加了go.mod,又提示工程目录下引用的包找不到。去掉go.mod先把go.mod关闭把工程目录加入GOPATH......
  • mongodb 入门 和 php示例
    内容太多了,感觉不好写,就写点入门的吧,其他参考参考_MonogDB中文网(mongodb.net.cn)虽然内容是机器翻译的,但也还好,基本能看. 相关概念: database数据库collection集合,相当于数据库表document文档,相当于数据记录行 dockerrun-d--namemongo-p27017:27......
  • Golang 刷题记录
    刷了大概50道题,我个人的结论:在中等难度题中,使用Golang的效率完全是不输于C++的,特别是在Golang没有C++这么完善的STL库的情况下,只借助container包里的三种数据结构,大部分题时间、空间复杂度都可以媲美C++。当然这个hard题是啥情况我这个蒟蒻就不知道咯。再刷洛谷......
  • 设计模式-建造者模式在Java中使用示例
    场景建造者模式复杂对象的组装与创建没有人买车会只买一个轮胎或者方向盘,大家买的都是一辆包含轮胎、方向盘和发动机等多个部件的完整汽车。如何将这些部件组装成一辆完整的汽车并返回给用户,这是建造者模式需要解决的问题。建造者模式又称为生成器模式,它是一种较为复杂、使用......
  • rabbitmq php 代码示例
    交换机类型direct:直连交换机,根据路由键投递到与绑定键匹配的队列。fanout:扇形交换机,采用广播模式,投递到所有与之绑定的队列。topic :主题交换机,对路由键与绑定键进行模式匹配后再投递到相应的队列。headers:头交换机,不处理路由键,而是根据发送的消息内容中的heade......
  • Golang学习笔记-数据类型
    目录整型有符号整型无符号整型栗子浮点型栗子布尔型栗子字符型栗子字符串型栗子字符串<-->其他类型转换数组栗子切片创建切片通过数组的方式创建切片读取/修改切片元素追加元素-append合并多个切片删除元素字典栗子字典读、写、删除指针栗子方法栗子结构体结构体继承接口栗子类型......
  • golang的list数据结构demo
    packagemainimport"container/list"funcmain(){varmylistlist.List//放在尾部mylist.PushBack("go")mylist.PushBack("grpc")mylist.PushBack("mysql")//头部放数据mylist.PushFront("gi......
  • golang channel Synchronization
    在Go语言中,通道(channel)是一个很重要的并发同步机制,可以用来在不同的goroutine之间发送和接收数据。通道实现了一个先进先出(FIFO)的数据结构,所以可以确保数据的接收顺序与发送顺序一致。此外,通道的发送和接收操作都是原子的,这意味着你不需要额外的锁来同步数据访问。这里有几......