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
}

在上面代码中,efceface 类型的变量,对应到 eface 结构体的话,_type 就是Bird这个类型本身,而data就是 &bird 这个指针:
在这里插入图片描述

类似的,ifciface 类型的变量,对应到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字段包含it拥有的方法的交集,因为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.Typereflect.Value 类型的值。这也就是为什么我们可以通过reflect.TypeOfreflect.ValueOf来获取到一个变量的类型和值的原因。

反射三大定律

go官方博客中关于反射的文章 laws-of-reflection 中,提到了三条反射定律:

  • 反射可以将 interface 类型变量转换成反射对象。通常使用通过reflect.TypeOfreflect.ValueOf实现。
  • 反射可以将反射对象还原成 interface 对象。通常使用reflect.Value.Interface()实现。
  • 如果要修改反射对象,那么反射对象必须是可设置的(CanSet)。

关于这三条定律,官方博客已经有了比较完整的阐述,感兴趣的可以去看一下官方博客的文章。这里简单阐述一下:

反射可以将 interface 类型变量转换成反射对象。
其实也就是上面的 reflect.Typereflect.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.ValueElem 方法的作用是获取指针指向的值,或者获取接口的动态值。也就是说,能调用 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 方法就是获取接口的数据部分(也就是 ifaceeface 中的 data 字段)。

指针类型:

在这里插入图片描述

接口类型:

在这里插入图片描述

reflect.Type 的 Elem 方法

reflect.TypeElem 方法的作用是获取数组、chan、map、指针、切片关联元素的类型信息,也就是说,对于reflect.Type来说, 能调用Elem方法的反射对象,必须是数组、chanmap、指针、切片中的一种,其他类型的 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.ValueInterface 方法的作用是获取反射对象的动态值。 也就是说,如果反射对象是一个指针,那么 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.TypeKind 来进行类型判断。 也就是说,我们在通过反射来判断变量的类型的时候,只需要枚举 Kind 中的类型,然后通过 reflect.TypeKind 方法来判断即可。

Type 表示的是反射对象的类型(Type 对象是某一个 Kind,通过 Kind() 方法可以获取 TypeKind,基本类型的种类),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 的反射系统中有两个关于寻址的方法:CanAddrCanSet

CanAddr 方法的作用是判断反射对象是否可以寻址,也就是说,如果 CanAddr 返回 true,那么我们就可以通过 Addr 方法来获取反射对象的地址。 如果 CanAddr 返回 false,那么我们就不能通过Addr方法来获取反射对象的地址。对于这种情况,我们就无法通过反射对象来修改变量的值。

但是,CanAddrtrue并不是说 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 的目的通常是为了获取类型的信息,比如类型是什么、类型的名称、类型的字段、类型的方法等等。 又或者最常见的场景:结构体中的 jsontag,它是没有语义的,它的作用就是为了在序列化的时候,生成我们想要的字段名。 而这个 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、FieldByNameFuncreflect.Type也基本有这些方法。
  • 判断是否可以设置为某一类型:·CanConvert、CanComplex、CanFloat、CanInt、CanInterface、CanUint·。
  • 方法类型的方法:Method、MethodByName、Call、CallSlice
  • 判断值是否有效:IsValid
  • 判断值是否是 nilIsNil
  • 判断值是否是零值:IsZero
  • 判断值能否容纳下某一类型的值:Overflow、OverflowComplex、OverflowFloat、OverflowInt、OverflowUint
  • 反射值指针相关的方法:AddrCanAddr true 才能调用)、UnsafeAddr、Pointer、UnsafePointer
  • 获取类型信息:Type、Kind注:reflect.ValueType方法可以获取到reflect.Type类型,包含结构体字段类型信息,但是该方式获取到的类型信息没有reflect.StructField的,比如reflect.StructFieldTag方法获取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.TypeElem方法返回的是元素类型,只适用于 array、chan、map、pointer slice 类型的 reflect.Type
  • reflect.ValueElem 方法返回的是值,只适用于接口或指针类型的 reflect.Value
  • 通过 reflect.Value Interface 方法可以获取到反射对象的原始变量,但是是 interface{} 类型的。
  • Type Kind 都表示类型,但是Type是类型的反射对象,Kind go 类型系统中最基本的一些类型,比如int、string、struct等等。
  • 如果我们想通过reflect.Value来修改变量的值,那么reflect.Value必须是可设置的(CanSet)。同时如果想要 CanSet true,那么我们的变量必须是可寻址的。
  • 我们有很多方法可以创建 reflect.Typereflect.Value,我们需要根据具体的场景来选择合适的方法。
  • reflect.Type reflect.Value里面,都有一部分方法是通用的,也有一部分只适用于特定的类型。如果我们想要调用那些适用于特定类型的方法,那么我们必须先判断 reflect.Typereflect.Value 的类型(这里说的是 Kind),然后再调用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/397770.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mysql中select的执行流程

目录 引言 SELECT查询语句的重要性 ​编辑引言部分重写示例&#xff1a; MySQL架构概览 MySQL架构概述 Server层的核心功能模块 知识点图文结合示例&#xff1a; 连接器的作用 连接器的职责 连接器职责 查询缓存的工作原理 查询缓存的概念 查询缓存的工作机制 查询…

虚幻引擎 C++ 实现平面阴影

1、平面阴影介绍 平面阴影是一种相对简单的渲染阴影的方式&#xff0c;可以理解为对一个模型渲染两次&#xff0c;一次是渲染模型本身&#xff0c;另一次是渲染模型的投影。渲染投影可以看作是将模型的顶点变换到地面的投影空间再渲染&#xff0c;可以理解为渲染了一个“压扁”…

Linux内核编程(十二)热插拔

本文目录 一、知识点1. 热插拔概念2. 热插拔机制3. Netlink机制 二、内核发送uevent事件到用户空间1. kobject发送uevent事件2. udevadm命令查看★示例代码&#xff1a;★优化&#xff1a;完善kset_uevent_ops&#xff08;热插拔事件结构体&#xff09; 三、用户空间使用Netlin…

Dubbo源码深度解析(四)

接上篇博客《Dubbo源码深度解析(三)》&#xff0c;上篇博文&#xff0c;主要讲的是DubboBootstrap#start()方法中调用到的其他方法&#xff0c;以及讲到ServiceConfig#export()方法的调用链路。其中讲到最核心的方法为ServiceConfig#doExportUrlsFor1Protocol()&#xff0c;还没…

CentOS7 配置 nginx 和 php 方案

配置方案 一、安装软件二、编写配置文件&#xff0c;连接PHP三、引用文件四、测试 鉴于网上教程错综复杂&#xff0c;写下一这篇文章 本教程只需要三步即可 一、安装软件 yum install -y nginx php php-fpm二、编写配置文件&#xff0c;连接PHP 一般情况下在安装完 nginx 后…

python-质因数分解(赛氪OJ)

[题目描述] 已知正整数 n 是两个不同的质数的乘积&#xff0c;试求出两者中较大的那个质数。输入格式&#xff1a; 输入一个正整数 n。输出格式&#xff1a; 输出一个正整数 p&#xff0c;即较大的那个质数。样例 #1样例输入 #1 21样例输出 #1 7提示&#xff1a; 1≤n≤2109 来…

无字母数字的绕过方法

php代码 <?phpif(isset($_GET[code])){$code $_GET[code];if(strlen($code)>35){die("Long.");}if(preg_match("/[A-Za-z0-9_$]/",$code)){die("NO.");}eval($code);}else{highlight_file(__FILE__);} 题目的限制&#xff1a; webshell…

书籍分享:【矩阵力量】豆瓣评分高达9.6,看完感叹《矩阵论》又白学了

书籍分享&#xff1a;【矩阵力量】豆瓣评分高达9.6&#xff0c;看完感叹《矩阵论》又白学了 《矩阵力量》简要介绍书籍下载链接 《矩阵力量》简要介绍 《矩阵力量》是姜伟生精心编写的线性代数的深度理解之作&#xff0c;作者将抽象的线性代数概念用通俗易懂的语言和大量生动形…

Windows下,C# 通过FastDDS高效通信

目录 1、安装FastDDS 库2、使用IDL定义自己的数据格式3、生成DLL3.1 托管 &#xff08;Managed&#xff09;模式3.2 非托管 &#xff08;Unmanaged&#xff09;模式 -- 可用于Unity 代码示例 eprosima Fast DDS is a C implementation of the DDS (Data Distribution Service) …

vscode用快捷键一键生成vue模板

项目中有些代码模块是固定的&#xff0c;如下面的代码所示&#xff0c;为了不重复写这些相同的代码&#xff0c;我们可以使用快键键一键生成模板。 流程&#xff1a; 中文&#xff1a;首选项-> 用户代码片段 -> 输入框中输入vue,找到vue.json文件&#xff08;没有vue.j…

Vue-07.生命周期

生命周期&#xff1a; 生命周期&#xff1a;指一个对象从创建到销毁的全过程 生命周期的八个阶段&#xff1a;每触发一个阶段&#xff0c;就会自动执行一个生命周期方法&#xff08;钩子方法&#xff09; 状态 阶段周期 beforeCreated 创…

内部排序(插入、交换、选择)

一、排序的部分基本概念 1. 算法的稳定性 若待排序表中有两个元素 Ri 和 Rj &#xff0c;其对应的关键字相同即 keyi keyj&#xff0c;且在排序前 Ri 在 Rj 的前面&#xff0c;若使用某一排序算法排序后&#xff0c;Ri 仍然在 Rj 的前面&#xff0c;则称这个排序算法是稳定的…

【MySQL】详解数据库约束、聚合查询和联合查询

数据库约束 约束类型 数据库的约束类型主要包括以下几种&#xff1a; 主键约束&#xff08;Primary Key Constraint&#xff09;&#xff1a;确保表中的每一行都有唯一的标识&#xff0c;且不能为NULL。 外键约束&#xff08;Foreign Key Constraint&#xff09;&#xff1a…

5.ADC(模拟信号转数字信号)

理论 3个ADC控制器 转换&#xff1a;单次转换模式、 连续转换模式 转换时间 采样时间 12.5周期 当ADCCLK(时钟) 14MHz&#xff0c;采样时间为1.5周期&#xff0c;TcoNv(转换时间) 1.5 12.5 14 周期 1us 采样精度&#xff1a;12位/16位(212 4096) 实际电压值 (通道采…

Java面试题--JVM大厂篇之破解 JVM 性能瓶颈:实战优化策略大全

目录 引言: 正文: 1. 常见的JVM性能问题 频繁的GC导致应用暂停 内存泄漏导致的内存不足 线程争用导致的CPU利用率过高 类加载问题导致的启动时间过长 2. 优化策略大全 2.1 代码层面的优化 2.1.1 避免不必要的对象创建 2.1.2 优化数据结构的选择 2.1.3 使用并发工具…

Python爬虫:下载4K壁纸

&#x1f381;&#x1f381;创作不易&#xff0c;关注作者不迷路&#x1f380;&#x1f380; 目录 &#x1f338;完整代码 &#x1f338;分析 &#x1f381;基本思路 &#x1f381;需要的库 &#x1f381;提取图片的链接和标题 &#x1f453;寻找Cookie和User-Agent &…

突破•指针六

听说这是目录哦 数组和指针笔试题解析&#x1fae7;一维数组1&#x1f355;&#x1f355;&#x1f355;&#x1f355;&#x1f355;&#x1f355;&#x1f355; 字符数组1&#x1f354;&#x1f354;&#x1f354;&#x1f354;&#x1f354;&#x1f354;&#x1f354;2&#…

PCL 采样一致性模型介绍

采样一致性可以简单高效的检测出一些具有数学表达式的目标模型。PCL中的sample consensus模块中不仅包含各种的采样一致性估计方法,也包含一些已经编写好的数学模型,下面主要介绍一下PCL中的采样一致性模型。 1. 二维圆模型 pcl::SampleConsensusModelCircle2D< PointT …

AI学习记录 - 自注意力机制的计算流程图

画图不易&#xff0c;如果你从这个图当中得到灵感&#xff0c;大佬赏个赞 过段时间解释一下&#xff0c;为啥这样子计算&#xff0c;研究这个自注意力花了不少时间&#xff0c;网上很多讲概念&#xff0c;但是没有具体的流程图和计算方式总结…

Win11表情符号输入详细教程,一学就会!

在Win11电脑操作中&#xff0c;用户可以根据自己的需求&#xff0c;点击输入想要的表情符合。但许多新手用户不知道怎么操作才能输入&#xff1f;这时候用户按下快捷键&#xff0c;快速打开表情符号选择界面&#xff0c;然后选择需要的表情符号点击输入即可。以下系统之家小编给…