Go 语言中的反射,一篇文章带你学会!

反射引入

前置

有时我们需要写一个函数,这个函数有能力统一处理各种值类型,而这些类型可能无法共享 同一个接口,也可能布局未知,也有可能这个类型在我们设计函数时还不存在,这个时候我们就可以用到反射。

问题一:空接口可以存储任意类型的变量,那我们如何知道这个空接口保存数据的类型是什么? 值是什么呢?

  • 可以使用类型断言
  • 可以使用反射实现,也就是在程序运行时动态的获取一个变量的类型信息和值信息。

问题二:在之前我们家分享的 Gorm 文章中,这个框架就用到的反射技术。

问题三:当我们把结构体序列化成 json 字符串,自定义结构体 Tag 标签的时候就用到了反射。

package test1

import "encoding/json"

type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func main() {
	u := User{
		ID:   1,
		Name: "ypb",
		Age:  18,
	}
	s, _ := json.Marshal(u)
	jsonStr := string(s)
	println(jsonStr)
}

引入

在我们日常的编程实践中,我们经常会遇到一些需要在运行时动态处理数据和类型的场景。这时候,Go 语言的反射功能就派上了用场。

反射(Reflection)是计算机科学中的一个重要概念,它允许程序在运行时检查变量和值,获取它们的类型信息,并且能够修改它们。通过反射,我们可以编写灵活的代码,这些代码能够处理各种不同类型的值,而不必在编译时就知道这些值的具体类型。

功能

  • 反射可以在程序运行期间动态的获取变量的各种信息,比如变量的类型、类别。
  • 如果是结构体,通过反射还可以获取结构体本身的信息,比如结构体的字段、结构体的方法、结构体的 tag
  • 通过反射,可以修改变量的值,可以调用关联的方法。

Go 语言中的变量是分为两部分的:

  • 类型信息:预先定义好的元信息。
  • 值信息:程序运行过程中可动态变化的。

在 Go 的反射机制中,任何接口值都由是一个具体类型具体类型的值两部分组成的。

在 Go 中,反射的相关功能由内置的 reflect 包提供,任意接口值在反射中都可以理解为由 reflect.Typereflect.Value 两部分组成,并 且 reflect 包提供了 reflect.TypeOfreflect.ValueOf 两个重要函数来获取任意对象的 ValueType

在接下来的文章中,将介绍 Go 语言的反射机制,包括它的基本概念,如何在 Go 语言中使用反射,以及一些常见的使用场景和注意事项。我希望通过这篇文章,你能够理解反射的工作原理,学习如何在你自己的代码中使用反射,以及如何避免反射的一些常见陷阱。

reflect.TypeOf()

reflect.TypeOf() 是 Go 语言中 reflect 包提供的一个函数,它返回一个 reflect.Type 类型的值,这个值代表了其参数的动态类型。

这个函数可以接收任何类型的参数,包括基础类型(如 intstring等),结构体,接口,函数等。如果参数是一个接口值,reflect.TypeOf() 会返回接口值的动态类型,而不是接口的类型。

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var i int = 10
	var s string = "hello"
	var b bool = true
	var f float64 = 1.23
	var p *int = &i
	var m map[string]int = map[string]int{"a": 1, "b": 2}
	fmt.Println(reflect.TypeOf(i)) // int
	fmt.Println(reflect.TypeOf(s)) // string
	fmt.Println(reflect.TypeOf(b)) // bool 
	fmt.Println(reflect.TypeOf(f)) // float64
	fmt.Println(reflect.TypeOf(p)) // *int
	fmt.Println(reflect.TypeOf(m)) // map[string]int
}

Type Name & Type Kind

在反射中关于类型还划分为两种:类型(Type)和种类(Kind)。

因为在 Go 语言中我们可以使用 type 关键字构造很多自定义类型,而种类(Kind)就是指底层的类型。

  • Type Name:这是类型的名称,它通常是在声明类型时指定的。例如,当我们声明一个自定义的结构体类型时,我们会给它一个名字:
type MyStruct struct {
    ID int
    Name string
}

在这个例子中,MyStruct 就是这个类型的 Type Name。如果一个类型是通过类型别名或者是内置类型,那么它的 Type Name 就是那个别名或者内置类型的名称。

  • Type Kind:这是类型的种类,它描述了这个类型的基本分类。在 Go 语言中,有许多种不同的 Type Kind,比如intfloat64structpointerslice等等。Type Kind 并不关心类型的具体名称,只关心它是什么种类的类型。

例如,对于上面的 MyStruct 类型,它的 Type Kindstruct ,因为它是一个结构体类型。对于一个 int 类型的变量,它的Type Kind就是int

基础类型;

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var i int = 10
	var s string = "hello"
	var b bool = true
	var f float64 = 1.23
	var p *int = &i
	var m map[string]int = map[string]int{"a": 1, "b": 2}

	reflectPrintType(i) //TypeOf: int  Name: int Kind: int
	reflectPrintType(s) //TypeOf: string  Name: string Kind: string
	reflectPrintType(b) //TypeOf: bool  Name: bool Kind: bool
	reflectPrintType(f) //TypeOf: float64  Name: float64 Kind: float64
	reflectPrintType(p) //TypeOf: *int  Name:  Kind: ptr
	reflectPrintType(m) //TypeOf: map[string]int  Name:  Kind: map
}

func reflectPrintType(v interface{}) {
	t := reflect.TypeOf(v)
	fmt.Printf("TypeOf: %v  Name: %v Kind: %v\n", t, t.Name(), t.Kind())
}

自定义类型:

package main

import (
	"fmt"
	"reflect"
)

type MyInt int

type User struct {
	Name string
	Age  int
	Sex  string
}

func main() {
	var n MyInt = 10
	reflectPrintType(n)  //TypeOf: main.MyInt  Name: MyInt Kind: int
	var u User = User{"Tom", 18, "male"}
	reflectPrintType(u) //TypeOf: main.User  Name: User Kind: struct
	var num = [3]int{1, 2, 3}
	reflectPrintType(num) //TypeOf: [3]int  Name:  Kind: array
}

func reflectPrintType(v interface{}) {
	t := reflect.TypeOf(v)
	fmt.Printf("TypeOf: %v  Name: %v Kind: %v\n", t, t.Name(), t.Kind())
}

所有的 Kind 类型:

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
	Pointer
	Slice
	String
	Struct
	UnsafePointer
)

reflect.ValueOf()

reflect.ValueOf() 是 Go 语言中的一个反射函数,它返回一个表示其参数的 reflect.Value。这个 reflect.Value 可以让你在运行时检查它代表的值的类型,读取它的内容,甚至修改它(如果它是可寻址的)。

reflect.Value 类型提供的获取原始值的方法如下:

方法说明
Int() int64将值以 int 类型返回,所有有符号整型均可以此方式返回
Uint() uint64将值以 uint 类型返回,所有无符号整型均可以此方式返回
Float() float64将值以双精度(float64)类型返回,所有浮点数(float32、float64)均 可以此方式返回
Bool() bool将值以 bool 类型返回
String() string将值以字符串类型返回

获取原始值1

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var a int64 = 10
	fmt.Printf("add befor: %v\n", a)
	reflectValue(a)
	fmt.Printf("add after: %v\n", a)
}

func reflectValue(v interface{}) {
	value := reflect.ValueOf(v)
	var add = value.Int() + 6
	fmt.Println(add)
}

获取原始值2

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var a int64 = 10
	reflectValue2(a)
	var s string = "hello"
	reflectValue2(s)
	var b struct {
		Name string
		Age  int
	}
	reflectValue2(b)
}

func reflectValue2(v interface{}) {
	value := reflect.ValueOf(v)
	k := value.Kind()
	switch k {
	case reflect.Int64:
		// value.Int()
		fmt.Println("type is int64", value.Int())
	case reflect.Float64:
		fmt.Println("type is float64", value.Float())
	case reflect.String:
		fmt.Println("type is string", value.String())
	case reflect.Bool:
		fmt.Println("type is bool", value.Bool())
	default:
		fmt.Println("type is illegal")
	}
}

设置值

在 Go 语言的反射中,你可以使用 reflect.ValueSetXXX 方法来设置值,其中 XXX 是相应的类型。比如 SetIntSetFloatSetString 等。重要的一点是,只有当 reflect.Value 可寻址并且可被修改时,才可以设置其值。

想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。而反射中使用专有的 Elem() 方法来获取指针对应的值。

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var a int64 = 10
	reflectSetValue(a)
	fmt.Println(a)
	var b *int64 = &a
	reflectSetValue2(b)
	fmt.Println(a)
}

func reflectSetValue(v interface{}) {
	value := reflect.ValueOf(v)
	if value.Kind() == reflect.Int {
		value.SetInt(value.Int() + 6)
	}
}

func reflectSetValue2(v interface{}) {
	value := reflect.ValueOf(v)
	// 在反射中通过 Elem()来获取指针指向的值
	if value.Elem().Kind() == reflect.Int {
		value.Elem().SetInt(value.Elem().Int() + 6)
	}
}

如果 reflect.Value 不可寻址或者不可被修改(比如它是一个常量或者是未导出的字段),那么调用 SetXXX 方法会导致程序崩溃。所以在调用 SetXXX 方法之前,你需要使用 CanSet 方法来检查 reflect.Value 是否可以被设置。

package main

import (
	"fmt"
	"reflect"
)

func main() {
	x := 2
	v := reflect.ValueOf(&x)
	if v.CanSet() {
		v.Elem().SetInt(10)
	} else {
		fmt.Println("can not set")
	}
	fmt.Println(x)
}
/*
  程序输出:
    can not set
    2
*/

结构体反射

常用的方法

  • TypeOf():返回一个reflect.Type,表示其参数的动态类型。我们可以通过它来获取结构体的名称(Name()方法)或者种类(Kind()方法)。例如:
type MyStruct struct {
    Field1 int
    Field2 string
}

s := MyStruct{10, "hello"}
t := reflect.TypeOf(s)
fmt.Println(t.Name())  // 输出"MyStruct"
fmt.Println(t.Kind())  // 输出"struct"
  • ValueOf():返回一个reflect.Value,表示其参数的动态值。我们可以通过它来获取或者设置结构体字段的值。例如:
s := MyStruct{10, "hello"}
v := reflect.ValueOf(s)
fmt.Println(v.Field(0).Int())  // 输出"10"
fmt.Println(v.Field(1).String())  // 输出"hello"
  • NumField():返回结构体的字段数量。例如:
s := MyStruct{10, "hello"}
v := reflect.ValueOf(s)
fmt.Println(v.NumField())  // 输出"2"
  • Field(i int):返回结构体的第 i 个字段的 reflect.Value 。例如:
s := MyStruct{10, "hello"}
v := reflect.ValueOf(s)
fmt.Println(v.Field(0).Int())  // 输出"10"
  • FieldByName(name string):返回结构体的名为name的字段的reflect.Value。例如:
s := MyStruct{10, "hello"}
v := reflect.ValueOf(s)
fmt.Println(v.FieldByName("Field1").Int())  // 输出"10"
  • CanSet():检查reflect.Value是否可以被设置。例如:
s := MyStruct{10, "hello"}
v := reflect.ValueOf(s)
fmt.Println(v.Field(0).CanSet())  // 输出"false"  
  • NumMethod() int:这个方法返回结构体的方法数量。例如:
type MyStruct struct{}
func (s MyStruct) Hello() {}
func (s MyStruct) World() {}

s := MyStruct{}
t := reflect.TypeOf(s)
fmt.Println(t.NumMethod())  // 输出"2"
  • Method(i int) reflect.Method:这个方法返回结构体的第i个方法,假设有m个方法,方法的索引范围是[0, m),如果i不在这个范围内,则会引发panic。返回的reflect.Method类型包含了方法的名称、类型等信息。例如:
type MyStruct struct{}
func (s MyStruct) Hello() {}

s := MyStruct{}
t := reflect.TypeOf(s)
method := t.Method(0)
fmt.Println(method.Name)      // 输出"Hello"
fmt.Println(method.Type)      // 输出"func(main.MyStruct)"
  • MethodByName(name string) (reflect.Method, bool):这个方法返回结构体的名为name的方法,如果没有找到这个方法,就返回零值。返回的reflect.Method类型包含了方法的名称、类型等信息。第二个返回值表示是否找到了这个方法。例如:
type MyStruct struct{}
func (s MyStruct) Hello() {}

s := MyStruct{}
t := reflect.TypeOf(s)
method, found := t.MethodByName("Hello")
if found {
    fmt.Println(method.Name)  // 输出"Hello"
    fmt.Println(method.Type)  // 输出"func(main.MyStruct)"
}

StructField

StructField 类型用来描述结构体中的一个字段的信息。

// A StructField describes a single field in a struct.
type StructField struct {
	// Name is the field name.
	Name string

	// PkgPath is the package path that qualifies a lower case (unexported)
	// field name. It is empty for upper case (exported) field names.
	// See https://golang.org/ref/spec#Uniqueness_of_identifiers
	PkgPath string

	Type      Type      // field type
	Tag       StructTag // field tag string
	Offset    uintptr   // offset within struct, in bytes
	Index     []int     // index sequence for Type.FieldByIndex
	Anonymous bool      // is an embedded field
}

示例演示

使用反射得到一个结构体数据之后可以通过索引依次获取其字段信息,也可以通过字段名去获取指定的字段信息。

package main

import (
	"fmt"
	"reflect"
)

// User 用户信息
type User struct {
	Name    string   `json:"name"`
	Age     int      `json:"age"`
	emails  []string `json:"emails"`
	address string   `json:"address"`
}

// GetUser 获取用户信息
func (u User) GetUser() (string, int, []string, string) {
	return u.Name, u.Age, u.emails, u.address
}

// SetUser 设置用户信息
func (u User) SetUser(Name string, Age int, emails []string, address string) {
	u.Name = Name
	u.Age = Age
	u.emails = emails
	u.address = address
}

// PrintUser 打印用户信息
func (u User) PrintUser() {
	fmt.Println(u.Name, u.Age, u.emails, u.address)
}

func main() {
	u := User{
		Name:    "xiaoming",
		Age:     18,
		emails:  []string{"test@eamil.com", "test@qq.com"},
		address: "beijing",
	}

	//PrintStructField(u)
	//PrintStructFn(u)
	ReflectChangeStruct(&u)
}

// PrintStructField 打印结构体字段
func PrintStructField(v interface{}) {
	t := reflect.TypeOf(v)

	if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {
		fmt.Println("not struct")
		return
	}

	//1、通过类型变量里面的 Field 可以获取结构体的字段
	for i := 0; i < t.NumField(); i++ {
		fmt.Printf("field %d:  Name:%s Type:%s JsonTag:%s\n",
			i, t.Field(i).Name, t.Field(i).Type, t.Field(i).Tag.Get("json"))
	}

	//2、通过类型变量里面的 FieldByName 可以获取结构体的字段
	field, ok := t.FieldByName("Name")
	if ok {
		fmt.Println(field.Name)
	} else {
		fmt.Println("not found")
	}
}

// PrintStructFn 打印结构体方法
func PrintStructFn(v interface{}) {
	t := reflect.TypeOf(v)
	val := reflect.ValueOf(v)

	if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {
		fmt.Println("not struct")
		return
	}
	//1、通过类型变量里面的 Method 可以获取结构体的方法
	for i := 0; i < t.NumMethod(); i++ {
		fmt.Println(t.Method(i).Name)
	}
	//2、通过类型变量获取这个结构体有多少个方法
	fmt.Println(t.NumMethod())
	//3、执行方法 (注意需要使用值变量,并且要注意参数)
	val.MethodByName("PrintUser").Call(nil)
	//4、执行方法传入参数 (注意需要使用值变量,并且要注意参数)
	var params []reflect.Value // 参数
	params = append(params, reflect.ValueOf("xiaoming"))
	params = append(params, reflect.ValueOf(18))
	params = append(params, reflect.ValueOf([]string{"test@eamil.com", "test@qq.com"}))
	params = append(params, reflect.ValueOf("beijing"))
	fmt.Println(val.MethodByName("SetUser").Call(params))

	// 5、执行方法获取方法的值
	info := val.MethodByName("GetUser").Call(nil)
	fmt.Println(info)
}

// ReflectChangeStruct 通过反射修改结构体字段
func ReflectChangeStruct(v interface{}) {
	t := reflect.TypeOf(v)
	val := reflect.ValueOf(v)

	if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {
		fmt.Println("not struct")
		return
	}

	name := val.Elem().FieldByName("Name")
	name.SetString("xiaoyan") // 设置字段值

	age := val.Elem().FieldByName("Age")
	age.SetInt(20) // 设置字段值
}

注意事项

  • 性能开销:反射操作通常比普通操作更慢,因为反射涉及到很多动态类型检查。如果你的代码在性能关键的路径上,你可能需要避免使用反射。
  • 类型安全:反射可以让你做一些通常类型系统不允许的事情,比如访问私有字段或调用私有方法。这可能会导致一些难以预见的问题,并且可能会破坏类型系统的安全性。
  • 可读性和维护性:使用反射的代码通常更难理解和维护。如果你可以通过其他方式达到同样的目的,那么最好避免使用反射。
  • 错误处理:许多反射操作返回一个错误作为其结果的一部分,比如Value.Interface()Value.SetInt()。你需要确保在使用这些方法时正确地处理这些错误。
  • 可设置性:当使用反射修改一个值时,需要注意值是否可以被设置。例如,如果一个值的CanSet()方法返回false,那么你不能修改这个值。对于不可寻址的值或未导出的结构字段,CanSet()将返回false
  • 结构体的零值:当使用reflect.TypeOf()reflect.ValueOf()时,如果你的结构体有未导出的字段,你需要使用指向该结构体的指针,否则会得到零值。
  • 导出和未导出的字段和方法:反射可以访问未导出的字段和方法,但是这可能会违反Go的封装原则,应当谨慎使用。

总的来说,虽然反射是一个强大的工具,但是在使用时需要考虑到其性能开销、可读性、错误处理等问题。并且,反射应当作为最后的手段来使用,只有在没有其他方法可以达到同样目的的时候,才应该考虑使用反射。

  • 15
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值