127. Go反射基本原理
文章目录
- 反射基础 - go 的 interface 是怎么存储的?
- iface 和 eface 的结构体定义(runtime/iface.go):
- _type 是什么?
- itab 是什么?
- 反射对象 - reflect.Type 和 reflect.Value
- 反射三大定律
- Elem 方法
- reflect.Value 的 Elem 方法
- reflect.Type 的 Elem 方法
- Interface 方法
- Kind
- addressable
- 获取类型信息 - reflect.Type
- 通用的 Type 方法
- 某些类型特定的 Type 方法
- 创建 reflect.Type 的方式
- 获取值信息 - reflect.Value
- reflect.Value 的方法
- 创建 reflect.Value 的方式
- 总结
反射是这样一种机制,它是可以让我们在程序运行时(runtime
)访问、检测和修改对象本身状态或行为的一种能力。 比如,从一个变量推断出其类型信息、以及存储的数据的一些信息,又或者获取一个对象有什么方法可以调用等。 反射经常用在一些需要同时处理不同类型变量的地方,比如序列化、反序列化、ORM
等等,如标准库里面的 json.Marshal
。
反射基础 - go 的 interface 是怎么存储的?
在正式开始讲解反射之前,我们有必要了解一下 go
里的接口(interface
)是怎么存储的。 在之前相关文章中我们学习过,interface{}
类型(不含有任何方法的接口)在底层实际上是eface
类型,而 含有方法的接口类型在底层实际上是 iface
类型。
iface 和 eface 的结构体定义(runtime/iface.go):
// 非空接口(如:io.Reader)
type iface struct {tab *itab // 方法表 与 类型信息data unsafe.Pointer // 指向变量本身的指针
}// 空接口(interface{})
type eface struct {_type *_type // 接口变量的类型data unsafe.Pointer // 指向变量本身的指针
}
go
底层的类型信息是使用 _type
结构体来存储的。
比如,我们有下面的代码:
package maintype Bird struct {name string
}func (b Bird) Fly() {
}type Flyable interface {Fly()
}func main() {bird := Bird{name: "b1"}var efc interface{} = bird // efc 是 efacevar ifc Flyable = bird // ifc 是 ifaceprintln(efc) // runtime.printefaceprintln(ifc) // runtime.printiface
}
在上面代码中,efc
是 eface
类型的变量,对应到 eface
结构体的话,_type
就是Bird
这个类型本身,而data
就是 &bird
这个指针:
类似的,ifc
是 iface
类型的变量,对应到iface
结构体的话,data
也是 &bird
这个指针:
_type 是什么?
在go
中,_type
是保存了变量类型的元数据的结构体,定义如下:
// _type 是 go 里面所有类型的一个抽象,里面包含 GC、反射、大小等需要的细节,
// 它也决定了 data 如何解释和操作。
// 里面包含了非常多信息:类型的大小、哈希、对齐及 kind 等信息
type _type struct {size uintptr // 数据类型共占用空间的大小ptrdata uintptr // 含有所有指针类型前缀大小hash uint32 // 类型 hash 值;避免在哈希表中计算tflag tflag // 额外类型信息标志align uint8 // 该类型变量对齐方式fieldAlign uint8 // 该类型结构体字段对齐方式kind uint8 // 类型编号// 用于比较此类型对象的函数equal func(unsafe.Pointer, unsafe.Pointer) bool// gc 相关数据gcdata *bytestr nameOff // 类型名字的偏移ptrToThis typeOff
}
这个 _type
结构体定义大家大致看看就好了,实际上,go
底层的类型表示也不是上面这个结构体这么简单。
itab 是什么?
我们从 iface
中可以看到,它包含了一个 *itab
类型的字段,我们看看这个 itab
的定义:
// 编译器已知的 itab 布局
type itab struct {inter *interfacetype // 接口类型_type *_typehash uint32_ [4]bytefun [1]uintptr // 变长数组. fun[0]==0 意味着 _type 没有实现 inter 这个接口
}// 接口类型
// 对应源代码:type xx interface {}
type interfacetype struct {typ _type // 类型信息pkgpath name // 包路径mhdr []imethod // 接口的方法列表
}
根据 interfacetype
我们可以得到关于接口所有方法的信息。同样的,通过_type
也可以获取结构体类型的所有方法信息。
从定义上,我们可以看到 itab
跟 *interfacetype
和 *_type
有关,但实际上有什么关系,从定义上其实不太能看得出来, 但是我们可以看它是怎么被使用的,现在,假设我们有如下代码:
// i 在底层是一个 interfacetype 类型
type i interface {A()C()
}// t 底层会用 _type 来表示
// t 里面有 A、B、C、D 方法
// 因为实现了 i 中的所有方法,所以 t 实现了接口 i
type t struct {}
func (t) A() {}
func (t) B() {}
func (t) C() {}
func (t) D() {}
下图描述了上面代码对应的 itab
生成的过程:
i
为接口类型,t
为结构体类型,将t
的实例对象赋值给i
接口类型后,itab
组成如下,其中inter
字段包含i
接口类型的信息,_type
字段包含t
结构体类型信息,fun
字段包含i
和t
拥有的方法的交集
,因为t
赋值给i
接口类型后,只能调用i
接口类型拥有的方法。
说明:
-
itab
里面的inter
是接口类型的指针(比如通过type Reader interface{}
这种形式定义的接口,记录的是这个类型本身的信息),这个接口类型本身定义了一系列的方法,如图中的i
包含了A、C
两个方法。 -
_type
是实际类型的指针,记录的是这个实际类型本身的信息,比如这个类型包含哪些方法。图中的i
实现了A、B、C、D
四个方法,因为实现了i
的所有方法,所以说t
实现了i
接口。 -
在底层做类型转换的时候,比如
t
转换为i
的时候(var v i = t{}
),会生成一个itab
- 如果
t
没有实现i
中的所有方法,那么生成的itab
中不包含任何方法。 - 如果
t
实现了i
中的所有方法,那么生成的itab
中包含了i
中的所有方法指针,但是实际指向的方法是实际类型的方法(也就是指向的是t
中的方法地址)
- 如果
-
mhdr
(interfacetype
结构体中的一个字段)就是itab
中的方法表,里面的方法名就是接口的所有方法名,这个方法表中保存了实际类型(t)
中同名方法的函数地址,通过这个地址就可以调用实际类型的方法了。
所以,我们有如下结论:
itab
实际上定义了interfacetype
和_type
之间方法的交集。作用是什么呢?就是用来判断一个结构体是否实现某个接口的。itab
包含了接口的所有方法,这里面的方法是实际类型的子集。itab
里面的方法列表包含了实际类型的方法指针(也就是实际类型的方法的地址),通过这个地址可以对实际类型进行方法的调用。itab
在实际类型没有实现接口的所有方法的时候,生成失败(失败的意思是,生成的itab
里面的方法列表是空的,在底层实现上是用fun[0] = 0
来表示)。
一个 interface{}
中实际上既包含了变量的类型信息,也包含了类型的数据。而reflect.TypeOf
和 reflect.ValueOf
函数都会先将实参转为interface{}
,正因为如此,我们才可以通过反射来获取到变量的类型信息,以及变量的数据信息。
反射对象 - reflect.Type 和 reflect.Value
知道了 interface{}
的内存结构之后,我们就可以开始讲解反射了。反射的核心是两个对象,分别是 reflect.Type接口
和 reflect.Value结构体
。 它们分别代表了 go
语言中的类型和值。我们可以通过 reflect.TypeOf
和reflect.ValueOf
来获取到一个变量的类型和值。
var a = 1
t := reflect.TypeOf(a)var b = "hello"
t1 := reflect.ValueOf(b)
我们去看一下 TypeOf
和 ValueOf
的源码会发现,这两个方法都接收一个 interface{}
类型的参数,然后返回一个reflect.Type
和 reflect.Value
类型的值。这也就是为什么我们可以通过reflect.TypeOf
和reflect.ValueOf
来获取到一个变量的类型和值的原因。
反射三大定律
在go
官方博客中关于反射的文章 laws-of-reflection
中,提到了三条反射定律:
- 反射可以将
interface
类型变量转换成反射对象。通常使用通过reflect.TypeOf
和reflect.ValueOf
实现。 - 反射可以将反射对象还原成
interface
对象。通常使用reflect.Value.Interface()
实现。 - 如果要修改反射对象,那么反射对象必须是可设置的(
CanSet
)。
关于这三条定律,官方博客已经有了比较完整的阐述,感兴趣的可以去看一下官方博客的文章。这里简单阐述一下:
反射可以将 interface 类型变量转换成反射对象。
其实也就是上面的 reflect.Type
和 reflect.Value
,我们可以通过 reflect.TypeOf
和 reflect.ValueOf
来获取到一个变量的反射类型和反射值。
var a = 1
typeOfA := reflect.TypeOf(a)
valueOfA := reflect.ValueOf(a)
反射可以将反射对象还原成 interface 对象。
我们可以通过 reflect.Value.Interface
来获取到反射对象的interface
对象,也就是传递给 reflect.ValueOf
的那个变量本身。 不过返回值类型是 interface{}
,所以我们需要进行类型断言。
i := valueOfA.Interface()
fmt.Println(i.(int))
如果要修改反射对象,那么反射对象必须是可设置的(CanSet)。
我们可以通过 reflect.Value.CanSet
来判断一个反射对象是否是可设置的。如果是可设置的,我们就可以通过 reflect.Value.Set
来修改反射对象的值。 这其实也是非常常见的使用反射的一个场景,通过反射来修改变量的值。
var x float64 = 3.4
v := reflect.ValueOf(&x)
fmt.Println("settability of v:", v.CanSet()) // false
fmt.Println("settability of v:", v.Elem().CanSet()) // true
那什么情况下一个反射对象是可设置的呢?前提是这个反射对象是一个指针,然后这个指针指向的是一个可设置的变量。 在我们传递一个值给reflect.ValueOf
的时候,如果这个值只是一个普通的变量,那么reflect.ValueOf
会返回一个不可设置的反射对象。 因为这个值实际上被拷贝了一份,我们如果通过反射修改这个值,那么实际上是修改的这个拷贝的值,而不是原来的值。 所以go
语言在这里做了一个限制,如果我们传递进reflect.ValueOf
的变量是一个普通的变量,那么在我们设置反射对象的值的时候,会报错。 所以在上面这个例子中,我们传递了 x
的指针变量作为参数。这样,运行时就可以找到 x
本身,而不是x
的拷贝,所以就可以修改 x
的值了。
但同时我们也注意到了,在上面这个例子中,v.CanSet()
返回的是 false
,而 v.Elem().CanSet()
返回的是 true
。 这是因为,v
是一个指针,而v.Elem()
是指针指向的值,对于这个指针本身,我们修改它是没有意义的,我们可以设想一下, 如果我们修改了指针变量(也就是修改了指针变量指向的地址),那会发生什么呢?那样我们的指针变量就不是指向x
了, 而是指向了其他的变量,这样就不符合我们的预期了。所以 v.CanSet()
返回的是 false
。
而 v.Elem().CanSet()
返回的是 true
。这是因为 v.Elem()
才是 x
本身,通过 v.Elem()
修改 x
的值是没有问题的。
Elem 方法
Elem
方法的作用是什么呢?在回答这个问题之前,我们需要明确一点:reflect.Value 和 reflect.Type 这两个反射对象都有 Elem 方法,既然是不同的对象,那么它们的作用自然是不一样的。
reflect.Value 的 Elem 方法
reflect.Value
的 Elem
方法的作用是获取指针指向的值,或者获取接口的动态值。也就是说,能调用 Elem
方法的反射对象,必须是一个指针或者一个接口。 在使用其他类型的 reflect.Value
来调用 Elem
方法的时候,会 panic
:
var a = 1
// panic: reflect: call of reflect.Value.Elem on int Value
reflect.ValueOf(a).Elem()// 不报错
var b = &a
reflect.ValueOf(b).Elem()
对于指针很好理解,其实作用类似解引用。而对于接口,还是要回到 interface
的结构本身,因为接口里包含了类型和数据本身,所以 Elem
方法就是获取接口的数据部分(也就是 iface
或 eface
中的 data
字段)。
指针类型:
接口类型:
reflect.Type 的 Elem 方法
reflect.Type
的 Elem
方法的作用是获取数组、chan、map、指针、切片
关联元素的类型信息,也就是说,对于reflect.Type
来说, 能调用Elem
方法的反射对象,必须是数组、chan
、map
、指针、切片中的一种,其他类型的 reflect.Type
调用 Elem
方法会 panic
。
示例:
t1 := reflect.TypeOf([3]int{1, 2, 3}) // 数组 [3]int
fmt.Println(t1.String()) // [3]int
fmt.Println(t1.Elem().String()) // int
需要注意的是,如果我们要获取 map
类型key
的类型信息,需要使用 Key
方法,而不是 Elem
方法。
m := make(map[string]string)
t1 := reflect.TypeOf(m)
fmt.Println(t1.Key().String()) // string
Interface 方法
这也是非常常用的一个方法,reflect.Value
的 Interface
方法的作用是获取反射对象的动态值。 也就是说,如果反射对象是一个指针,那么 Interface
方法会返回指针指向的值。
简单来说,如果 var i interface{} = x
,那么 reflect.ValueOf(x).Interface()
就是 i
本身,只不过其类型是 interface{}
类型。
Kind
说到反射,不得不提的另外一个话题就是 go
的类型系统,对于开发者来说,我们可以基于基本类型来定义各种新的类型,如:
// Kind 是 int
type myIny int
// Kind 是 Struct
type Person struct {Name stringAge int
}
但是不管我们定义了多少种类型,在 go
看来都是下面的基本类型中的一个:
type Kind uintconst (Invalid Kind = iotaBoolIntInt8Int16Int32Int64UintUint8Uint16Uint32Uint64UintptrFloat32Float64Complex64Complex128ArrayChanFuncInterfaceMapPointerSliceStringStructUnsafePointer
)
也就是说,我们定义的类型在 go
的类型系统中都是基本类型的一种,这个基本类型就是 Kind
。 也正因为如此,我们可以通过有限的 reflect.Type
的 Kind
来进行类型判断。 也就是说,我们在通过反射来判断变量的类型的时候,只需要枚举 Kind
中的类型,然后通过 reflect.Type
的 Kind
方法来判断即可。
Type
表示的是反射对象的类型(Type
对象是某一个 Kind
,通过 Kind()
方法可以获取 Type
的 Kind
,基本类型的种类),Kind
表示的是 go
底层类型系统中的类型。
比如下面的例子:
func display(path string, v reflect.Value) {switch v.Kind() {case reflect.Invalid:fmt.Printf("%s = invalid\n", path)case reflect.Slice, reflect.Array:for i := 0; i < v.Len(); i++ {display(fmt.Sprintf("%s[%d]", path, i), v.Index(i))}case reflect.Struct:for i := 0; i < v.NumField(); i++ {fieldPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name)display(fieldPath, v.Field(i))}case reflect.Map:for _, key := range v.MapKeys() {display(fmt.Sprintf("%s[%s]", path, formatAny(key)), v.MapIndex(key))}case reflect.Pointer:if v.IsNil() {fmt.Printf("%s = nil\n", path)} else {display(fmt.Sprintf("(*%s)", path), v.Elem())}case reflect.Interface:if v.IsNil() {fmt.Printf("%s = nil\n", path)} else {fmt.Printf("%s.type = %s\n", path, v.Elem().Type())display(path+".value", v.Elem())}default:fmt.Printf("%s = %s\n", path, formatAny(v))}
}
我们在开发的时候非常常用的结构体,在go
的类型系统中,通通都是Struct
这个种类的。
addressable
go
反射中最后一个很重要的话题是 addressable
。在 go
的反射系统中有两个关于寻址的方法:CanAddr
和 CanSet
。
CanAddr
方法的作用是判断反射对象是否可以寻址,也就是说,如果 CanAddr
返回 true
,那么我们就可以通过 Addr
方法来获取反射对象的地址。 如果 CanAddr
返回 false
,那么我们就不能通过Addr
方法来获取反射对象的地址。对于这种情况,我们就无法通过反射对象来修改变量的值。
但是,CanAddr
是true
并不是说 reflect.Value
一定就能修改变量的值了。reflect.Value
还有一个方法 CanSet
,只有 CanSet
返回 true
,我们才能通过反射对象来修改变量的值。
那么CanAddr
背后的含义是什么呢?它意味着我们传递给 reflect.ValueOf
的变量是不是可以寻址的。也就是说,我们的反射值对象拿到的是不是变量本身,而不是变量的副本。如果我们是通过 &v
这种方式来创建反射对象的,那么 CanAddr
就会返回 true
, 反之,如果我们是通过 v
这种方式来创建反射对象的,那么 CanAddr
就会返回 false
。
获取类型信息 - reflect.Type
reflect.Type
是一个接口,它代表了一个类型。我们可以通过 reflect.TypeOf
来获取一个类型的reflect.Type
对象。 我们使用 reflect.Type 的目的通常是为了获取类型的信息,比如类型是什么、类型的名称、类型的字段、类型的方法等等
。 又或者最常见的场景:结构体中的 json
的 tag
,它是没有语义的,它的作用就是为了在序列化的时候,生成我们想要的字段名。 而这个 tag
就是需要通过反射来获取的。
通用的 Type 方法
在 go
的反射系统中,是使用reflect.Type
这个接口来获取类型信息的。reflect.Type
这个接口有很多方法,下面这些方法是所有的类型通用的方法:
// Type 是 Go 类型的表示。
//
// 并非所有方法都适用于所有类型。
// 在调用 kind 具体方法之前,先使用 Kind 方法找出类型的种类。因为调用一个方法如果类型不匹配会导致 panic
//
// Type 类型值是可以比较的,比如用 == 操作符。所以它可以用做 map 的 key
// 如果两个 Type 值代表相同的类型,那么它们一定是相等的。
type Type interface {// Align 返回该类型在内存中分配时,以字节数为单位的字节数Align() int// FieldAlign 返回该类型在结构中作为字段使用时,以字节数为单位的字节数FieldAlign() int// Method 这个方法返回类型方法集中的第 i 个方法。// 如果 i 不在[0, NumMethod()]范围内,就会 panic。// 对于非接口类型 T 或 *T,返回的 Method 的 Type 和 Func 字段描述了一个函数,// 其第一个参数是接收者,并且只能访问导出的方法。// 对于一个接口类型,返回的 Method 的 Type 字段给出的是方法签名,没有接收者,Func字段为nil。// 方法是按字典序顺序排列的。Method(int) Method// MethodByName 返回类型的方法集中具有该名称的方法和一个指示是否找到该方法的布尔值。// 对于非接口类型 T 或 *T,返回的 Method 的 Type 和 Func 字段描述了一个函数,// 其第一个参数是接收者。// 对于一个接口类型,返回的 Method 的 Type 字段给出的是方法签名,没有接收者,Func字段为nil。MethodByName(string) (Method, bool)// NumMethod 返回使用 Method 可以访问的方法数量。// 对于非接口类型,它返回导出方法的数量。// 对于接口类型,它返回导出和未导出方法的数量。NumMethod() int// Name 返回定义类型在其包中的类型名称。// 对于其他(未定义的)类型,它返回空字符串。Name() string// PkgPath 返回一个定义类型的包的路径,也就是导入路径,导入路径是唯一标识包的类型,如 "encoding/base64"。// 如果类型是预先声明的(string, error)或者没有定义(*T, struct{}, []int,或 A,其中 A 是一个非定义类型的别名),包的路径将是空字符串。PkgPath() string// Size 返回存储给定类型的值所需的字节数。它类似于 unsafe.Sizeof.Size() uintptr// String 返回该类型的字符串表示。// 字符串表示法可以使用缩短的包名。// (例如,使用 base64 而不是 "encoding/base64")并且它并不能保证类型之间是唯一的。如果是为了测试类型标识,应该直接比较类型 Type。String() string// Kind 返回该类型的具体种类。Kind() Kind// Implements 表示该类型是否实现了接口类型 u。Implements(u Type) bool// AssignableTo 表示该类型的值是否可以分配给类型 u。AssignableTo(u Type) bool// ConvertibleTo 表示该类型的值是否可转换为 u 类型。ConvertibleTo(u Type) bool// Comparable 表示该类型的值是否具有可比性。Comparable() bool
}
某些类型特定的 Type 方法
下面是某些类型特定的方法,对于这些方法,如果我们使用的类型不对,则会 panic
:
type Type interface {// Bits 以 bits 为单位返回类型的大小。// 如果类型的 Kind 不属于:sized 或者 unsized Int, Uint, Float, 或者 Complex,会 panic。Bits() int// ChanDir 返回一个通道类型的方向。// 如果类型的 Kind 不是 Chan,会 panic。ChanDir() ChanDir// IsVariadic 表示一个函数类型的最终输入参数是否为一个 "..." 可变参数。如果是,t.In(t.NumIn() - 1) 返回参数的隐式实际类型 []T.// 更具体的,如果 t 代表 func(x int, y ... float64),那么:// t.NumIn() == 2// t.In(0)是 "int" 的 reflect.Type 反射类型。// t.In(1)是 "[]float64" 的 reflect.Type 反射类型。// t.IsVariadic() == true// 如果类型的 Kind 不是 Func,IsVariadic 会 panicIsVariadic() bool// Elem 返回一个 type 的元素类型。// 如果类型的 Kind 不是 Array、Chan、Map、Ptr 或 Slice,就会 panicElem() Type// Field 返回一个结构类型的第 i 个字段。// 如果类型的 Kind 不是 Struct,就会 panic。// 如果 i 不在 [0, NumField()) 范围内也会 panic。Field(i int) StructField// FieldByIndex 返回索引序列对应的嵌套字段。它相当于对每一个 index 调用 Field。// 如果类型的 Kind 不是 Struct,就会 panic。FieldByIndex(index []int) StructField// FieldByName 返回给定名称的结构字段和一个表示是否找到该字段的布尔值。FieldByName(name string) (StructField, bool)// FieldByNameFunc 返回一个能满足 match 函数的带有名称的 field 字段。布尔值表示是否找到。FieldByNameFunc(match func(string) bool) (StructField, bool)// In 返回函数类型的第 i 个输入参数的类型。// 如果类型的 Kind 不是 Func 类型会 panic。// 如果 i 不在 [0, NumIn()) 的范围内,会 panic。In(i int) Type// Key 返回一个 map 类型的 key 类型。// 如果类型的 Kind 不是 Map,会 panic。Key() Type// Len 返回一个数组类型的长度。// 如果类型的 Kind 不是 Array,会 panic。Len() int// NumField 返回一个结构类型的字段数目。// 如果类型的 Kind 不是 Struct,会 panic。NumField() int// NumIn 返回一个函数类型的输入参数数。// 如果类型的 Kind 不是Func.NumIn(),会 panic。NumIn() int// NumOut 返回一个函数类型的输出参数数。// 如果类型的 Kind 不是 Func.NumOut(),会 panic。NumOut() int// Out 返回一个函数类型的第 i 个输出参数的类型。// 如果类型的 Kind 不是 Func,会 panic。// 如果 i 不在 [0, NumOut()) 的范围内,会 panic。Out(i int) Type
}
创建 reflect.Type 的方式
我们可以通过下面的方式来获取变量的类型信息,即以下方法的返回类型都是reflect.Type
:
获取值信息 - reflect.Value
reflect.Value
是一个结构体,它代表了一个值。 我们使用 reflect.Value
可以实现一些接收多种类型参数的函数,又或者可以让我们在运行时针对值的一些信息来进行修改。 常常用在接收interface{}
类型参数的方法中,因为参数是接口类型,所以我们可以通过 reflect.ValueOf
来获取到参数的值信息。 值信息不仅包含具体的数据,还包含类型、大小、结构体字段、方法等等。
同时,我们可以对这些获取到的反射值进行修改。这也是反射的一个重要用途。
reflect.Value 的方法
reflect.Value
这个Struct
同样有很多方法:具体可以分为以下几类:
- 设置值的方法:
Set*:Set、SetBool、SetBytes、SetCap、SetComplex、SetFloat、SetInt、SetLen、SetMapIndex、SetPointer、SetString、SetUint
。通过这类方法,我们可以修改反射值的内容,前提是这个反射值得是合适的类型。CanSet
返回true
才能调用这类方法 - 获取值的方法:
Interface、InterfaceData、Bool、Bytes、Complex、Float、Int、String、Uint
。通过这类方法,我们可以获取反射值的内容。前提是这个反射值是合适的类型,比如我们不能通过complex
反射值来调用Int
方法(我们可以通过Kind
来判断类型)。 map
类型的方法:MapIndex、MapKeys、MapRange、MapSet
。chan
类型的方法:Close、Recv、Send、TryRecv、TrySend
。slice
类型的方法:Len、Cap、Index、Slice、Slice3
。struct
类型的方法:NumField、NumMethod、Field、FieldByIndex、FieldByName、FieldByNameFunc
。reflect.Type也基本有这些方法。- 判断是否可以设置为某一类型:·CanConvert、CanComplex、CanFloat、CanInt、CanInterface、CanUint·。
- 方法类型的方法:
Method、MethodByName、Call、CallSlice
。 - 判断值是否有效:
IsValid
。 - 判断值是否是
nil
:IsNil
。 - 判断值是否是零值:
IsZero
。 - 判断值能否容纳下某一类型的值:
Overflow、OverflowComplex、OverflowFloat、OverflowInt、OverflowUint
。 - 反射值指针相关的方法:
Addr
(CanAddr
为true
才能调用)、UnsafeAddr、Pointer、UnsafePointer
。 - 获取类型信息:
Type、Kind
。注:reflect.Value
有Type
方法可以获取到reflect.Type
类型,包含结构体字段类型信息,但是该方式获取到的类型信息没有reflect.StructField
的,比如reflect.StructField
有Tag
方法获取tag
信息,reflect.Type
则没有Tag()方法,所以在遍历结构体的字段类型和值时,尤其是需要Tag
信息时,一般是如下模式
val := reflect.ValueOf(v) // v是结构体typ := val.Type() // 等价 reflect.TypeOf(v)for i := 0; i < val.NumField(); i++ { // 也可以换成typ.Numfield()// 获取到字段对应的Value,即使再使用fieldVal.Type方法获取到字段对应的reflect.Type//也只能拿到字段名、类型,路径等信息,不包含Tag信息,因为Tag信息是结构体特有的fieldVal := val.Field(i)// 返回reflect.StructField类型,包含字段的类型信息,如字段名、Tag,类型,路径,是否匿名等fieldType := typ.Field(i)//进行相应的后续处理//如 xxxTag := fieldType.Tag.Get("xxx")}
- 获取指向元素的值:
Elem
。 - 类型转换:
Convert
。 Len
也适用于slice、array、chan、map、string
类型的反射值。
创建 reflect.Value 的方式
我们可以通过下面的方式来获取变量的值信息,即以下方法的返回类型都是reflect.Value
::
总结
reflect
包提供了反射机制,可以在运行时获取变量的类型信息、值信息、方法信息等等。go
中的interface{}
实际上包含了两个指针,一个指向类型信息,一个指向值信息。正因如此,我们可以在运行时通过interface{}
来获取变量的类型信息、值信息。reflect.Type
代表一个类型,reflect.Value
代表一个值。通过reflect.Type
可以获取类型信息,通过reflect.Value
可以获取值信息。- 反射三定律:
- 反射可以将
interface
类型变量转换成反射对象。 - 反射可以将反射对象还原成
interface
对象。 - 如果要修改反射对象,那么反射对象必须是可设置的(
CanSet
)。
- 反射可以将
reflect.Value
和reflect.Type
里面都有Elem
方法,但是它们的作用不一样:reflect.Type
的Elem
方法返回的是元素类型,只适用于array、chan、map、pointer
和slice
类型的reflect.Type
。reflect.Value
的Elem
方法返回的是值,只适用于接口或指针类型的reflect.Value
。- 通过
reflect.Value
的Interface
方法可以获取到反射对象的原始变量,但是是interface{}
类型的。 Type
和Kind
都表示类型,但是Type
是类型的反射对象,Kind
是go
类型系统中最基本的一些类型,比如int、string、struct
等等。- 如果我们想通过
reflect.Value
来修改变量的值,那么reflect.Value
必须是可设置的(CanSet
)。同时如果想要CanSet
为true
,那么我们的变量必须是可寻址的。 - 我们有很多方法可以创建
reflect.Type
和reflect.Value
,我们需要根据具体的场景来选择合适的方法。 reflect.Type
和reflect.Value
里面,都有一部分方法是通用的,也有一部分只适用于特定的类型。如果我们想要调用那些适用于特定类型的方法,那么我们必须先判断reflect.Type
或reflect.Value
的类型(这里说的是Kind
),然后再调用。
相关文章:

127. Go反射基本原理
文章目录 反射基础 - go 的 interface 是怎么存储的?iface 和 eface 的结构体定义(runtime/iface.go):_type 是什么?itab 是什么? 反射对象 - reflect.Type 和 reflect.Value反射三大定律Elem 方法reflect.…...

提高PDF电子书的分辨率
解决方法出处 1. 安装ImageMagick brew install imagemagick brew install ghostscript2. 按流程进行 convert -density 600 your_pdf_filename.pdf output-%02d.jpg convert output*.jpg -normalize -threshold 80% final-%02d.jpg convert final*.jpg my_new_highcontras…...
Spring Cloud全解析:注册中心之zookeeper注册中心
zookeeper注册中心 使用zookeeper作为注册中心就不需要像eureka一样,在写一个eureka-server的服务了,因为zookeeper本身就是一个服务端,只需要编写需要进行服务注册的客户端即可 依赖 <!-- zookeeper 注册中心 --> <dependency&g…...

解决戴尔台式电脑休眠后无法唤醒问题
近期发现有少量戴尔的台式机会有休眠后无法唤醒的问题,具体现象就是电脑在休眠后,电源指示灯以呼吸的频率闪烁,无论怎么点鼠标和键盘都没有反应,并且按开机按钮也没法唤醒,只能是长按开机键强制关机再重启才行…...

MySQL运维-分库分表
介绍 问题分析 拆分策略 垂直拆分 水平拆分 实现技术 Mycat概述 介绍 概念介绍 Mycat配置 schema.xml schema标签 schema标签(table) datanode标签 datahost标签 rule.xml sever.xml system标签 user标签 Mycat分片 分片规则-范围 分片规则-取模 分…...

AGX orin硬件设计
AGX orin简介 从硬件组成来说,AGX orin可以分为核心板和扩展板。 核心板 核心板就是英伟达原装板卡,如下图所示: 核心板分为32G内存版本和64内存版本,两个版本除去内存不同之外,CPU也略有差异。核心板通过…...

AI大模型开发——2.深度学习基础(1)
学习大模型开发之前,我们需要有足够的储备知识,类似于基础的python语法相信大家也都是十分熟悉了。所以笔者也是考虑了几天决定先给大家补充一些深度学习知识。 首先问大家一个问题,学习大模型之前为什么要先学习深度学习知识呢? …...

go语言day22 gin-vue-admin全栈项目的依赖安装
flipped-aurora/gin-vue-admin: 🚀ViteVue3Gin的开发基础平台,支持TS和JS混用。它集成了JWT鉴权、权限管理、动态路由、显隐可控组件、分页封装、多点登录拦截、资源权限、上传下载、代码生成器【可AI辅助】、表单生成器和可配置的导入导出等开发必备功能…...
PHP之docker学习笔记
Docker学习笔记 前言: 之前学过一遍忘了 那就再来一遍没啥好说的就是可以直接构建一个环境 然后方便部署官网 http://www.docker.com仓库 https://hub.docker.comDocker的基本组成 镜像 容器 仓库 安装与卸载 卸载 sudo yum remove docker \docker-client \dock…...
基于树莓派4B与STM32的UART串口通信实验(代码开源)
在现代嵌入式系统中,树莓派和STM32的结合使用已成为一种流行趋势,它们各自承担不同的角色,实现优势互补。树莓派以其强大的计算能力处理复杂算法,而STM32则以其高效的控制能力执行实际的硬件操作。本文将详细介绍如何实现基于树莓…...

【云服务器系列】基于华为云OBS实现Picgo和Typora的完美融合
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...

IIC协议
一、IIC协议 1.1 IIC协议概述 IIC全称Inter-Integrated Circuit (集成电路总线) 是由PHILIPS(飞利浦)公司在80年代开发的两线式串行总线,用于连接微控制器及其外围设备。IIC属于半双工同步通信方式 特点 简单性和有效性。 由于接口直接在组件之上,…...

如何在linux系统上部署nginx
1)首先去 nginx.org/download 官网下载你所需要的版本 我这里是下载的 nginx-1-23-3.tar.gz 2)然后执行 yum -y install lrzsz 安装文件上传软件 执行 rz 选择你下载nginx的位置进行上传 yum -y install lrzsz 3)执行 tar -zxvf nginx-1.23…...
香港网站服务器抵御恶意攻击的一些措施
香港网站服务器因为在互联网中扮演着重要的角色,因此也在面临着网络中各种恶意攻击的威胁,为了确保香港网站服务器的安全和稳定运行,可以通过安全措施来进行防御,本文就来分享一些香港网站服务器来抵御恶意攻击的关键措施。 一、网…...

实战:docker部署filesite.io完美解决家庭相册需求-2024.8.10(测试成功)
https://wiki.onedayxyy.cn/docs/filesite.io-photot-install-full...

美团到店面经
redis中大key引起的问题 1、阻塞请求 Big Key对应的value较大,我们对其进行读写的时候,需要耗费较长的时间,这样就可能阻塞后续的请求处理。Redis的核心线程是单线程,单线程中请求任务的处理是串行的,前面的任务完不成…...

【CSS入门】第五课 - font字体
这一节,我们说一说font这个字体。做网页开发,网页中几乎不可能没有文字的,为了使网页更漂亮,用户体验更好。人们可算是绞尽脑汁,其中一部分就是在字体上下的大功夫。 接下来,我们学习一下,font…...

STM32-门电路-储存器-寄存器-STM32f1-MCU-GPIO-总线-keil5-点led-寄存器编程
1、门电路 门电路组成简单加法器: 二进制对电路的影响: 0和1代表无和有; 以下图例,演示与门:左1右1输出1; 电平标准:使用不同的电压表示数字0和1; 高电平:1࿱…...

【动态规划算法题记录】343. 整数拆分 | 96.不同的二叉搜索树
整数拆分 题目🔗 题目描述 给定一个正整数 n ,将其拆分为 k个正整数的和(k > 2),并使这些整数的乘积最大化。 返回你可以获得的最大乘积 。 思路分析 dp数组含义:dp[i]表示整数i拆分后的最大乘积。…...

网页上预览Excel文件
如何运行: 需要发布在服务器 如Tomcat 实例图片: 需要展示的文件: <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>excel预览</title><link rel"stylesheet" href"…...
<6>-MySQL表的增删查改
目录 一,create(创建表) 二,retrieve(查询表) 1,select列 2,where条件 三,update(更新表) 四,delete(删除表…...

【入坑系列】TiDB 强制索引在不同库下不生效问题
文章目录 背景SQL 优化情况线上SQL运行情况分析怀疑1:执行计划绑定问题?尝试:SHOW WARNINGS 查看警告探索 TiDB 的 USE_INDEX 写法Hint 不生效问题排查解决参考背景 项目中使用 TiDB 数据库,并对 SQL 进行优化了,添加了强制索引。 UAT 环境已经生效,但 PROD 环境强制索…...

前端导出带有合并单元格的列表
// 导出async function exportExcel(fileName "共识调整.xlsx") {// 所有数据const exportData await getAllMainData();// 表头内容let fitstTitleList [];const secondTitleList [];allColumns.value.forEach(column > {if (!column.children) {fitstTitleL…...
拉力测试cuda pytorch 把 4070显卡拉满
import torch import timedef stress_test_gpu(matrix_size16384, duration300):"""对GPU进行压力测试,通过持续的矩阵乘法来最大化GPU利用率参数:matrix_size: 矩阵维度大小,增大可提高计算复杂度duration: 测试持续时间(秒&…...

Unity | AmplifyShaderEditor插件基础(第七集:平面波动shader)
目录 一、👋🏻前言 二、😈sinx波动的基本原理 三、😈波动起来 1.sinx节点介绍 2.vertexPosition 3.集成Vector3 a.节点Append b.连起来 4.波动起来 a.波动的原理 b.时间节点 c.sinx的处理 四、🌊波动优化…...
基于matlab策略迭代和值迭代法的动态规划
经典的基于策略迭代和值迭代法的动态规划matlab代码,实现机器人的最优运输 Dynamic-Programming-master/Environment.pdf , 104724 Dynamic-Programming-master/README.md , 506 Dynamic-Programming-master/generalizedPolicyIteration.m , 1970 Dynamic-Programm…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...

安全突围:重塑内生安全体系:齐向东在2025年BCS大会的演讲
文章目录 前言第一部分:体系力量是突围之钥第一重困境是体系思想落地不畅。第二重困境是大小体系融合瓶颈。第三重困境是“小体系”运营梗阻。 第二部分:体系矛盾是突围之障一是数据孤岛的障碍。二是投入不足的障碍。三是新旧兼容难的障碍。 第三部分&am…...
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的----NTFS源代码分析--重要
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的 第一部分: 0: kd> g Breakpoint 9 hit Ntfs!ReadIndexBuffer: f7173886 55 push ebp 0: kd> kc # 00 Ntfs!ReadIndexBuffer 01 Ntfs!FindFirstIndexEntry 02 Ntfs!NtfsUpda…...

沙箱虚拟化技术虚拟机容器之间的关系详解
问题 沙箱、虚拟化、容器三者分开一一介绍的话我知道他们各自都是什么东西,但是如果把三者放在一起,它们之间到底什么关系?又有什么联系呢?我不是很明白!!! 就比如说: 沙箱&#…...