反射引入
前置
有时我们需要写一个函数,这个函数有能力统一处理各种值类型,而这些类型可能无法共享 同一个接口,也可能布局未知,也有可能这个类型在我们设计函数时还不存在,这个时候我们就可以用到反射。
问题一:空接口可以存储任意类型的变量,那我们如何知道这个空接口保存数据的类型是什么? 值是什么呢?
- 可以使用类型断言
- 可以使用反射实现,也就是在程序运行时动态的获取一个变量的类型信息和值信息。
问题二:在之前我们家分享的 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.Type
和 reflect.Value
两部分组成,并 且 reflect
包提供了 reflect.TypeOf
和 reflect.ValueOf
两个重要函数来获取任意对象的 Value
和 Type
。
在接下来的文章中,将介绍 Go 语言的反射机制,包括它的基本概念,如何在 Go 语言中使用反射,以及一些常见的使用场景和注意事项。我希望通过这篇文章,你能够理解反射的工作原理,学习如何在你自己的代码中使用反射,以及如何避免反射的一些常见陷阱。
reflect.TypeOf()
reflect.TypeOf()
是 Go 语言中 reflect
包提供的一个函数,它返回一个 reflect.Type
类型的值,这个值代表了其参数的动态类型。
这个函数可以接收任何类型的参数,包括基础类型(如 int
,string
等),结构体,接口,函数等。如果参数是一个接口值,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
,比如int
、float64
、struct
、pointer
、slice
等等。Type Kind
并不关心类型的具体名称,只关心它是什么种类的类型。
例如,对于上面的 MyStruct
类型,它的 Type Kind
是 struct
,因为它是一个结构体类型。对于一个 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.Value
的 SetXXX
方法来设置值,其中 XXX
是相应的类型。比如 SetInt
、SetFloat
、SetString
等。重要的一点是,只有当 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的封装原则,应当谨慎使用。
总的来说,虽然反射是一个强大的工具,但是在使用时需要考虑到其性能开销、可读性、错误处理等问题。并且,反射应当作为最后的手段来使用,只有在没有其他方法可以达到同样目的的时候,才应该考虑使用反射。