说说 golang 中的接口和反射
1、接口
1.1 类型
Golang 中的接口是一组方法的签名,是实现多态和反射的基础。
type 接口名 interface {method1(参数列表) 返回值列表method2(参数列表) 返回值列表
}
不同于 Java 语言,使用 implements 关键字显示的实现接口。Golang 接口的实现都是隐式的,只需要实现了接口类型中的所有方法就实现了接口。
func (t 自定义类型) method1(参数列表) (返回值列表) {//方法实现
}func (t 自定义类型) method2(参数列表) (返回值列表) {//方法实现
}
这里来看个示例
type Speaker interface {Speak() string
}type Dog struct{}func (d *Dog) Speak() string {return "Woof!"
}type Cat struct{}func (c *Cat) Speak() string {return "Meow!"
}func PrintSpeak(s Speaker) {fmt.Println(s.Speak())
}func TestSpeaker(t *testing.T) {dog := &Dog{}cat := &Cat{}PrintSpeak(dog)PrintSpeak(cat)
}
在这个例子中,Dog 和 Cat 类型都实现了 Speaker 接口的 Speak 方法。PrintSpeak 函数接受一个 Speaker 接口类型的参数,因此可以接受任何实现了 Speaker 接口的类型。这展示了多态性的强大之处:不同的类型可以通过相同的接口进行交互。
这里可以再展开下,利用接口多态的特点,服务可以对外提供一个元数据接口,依据传参的不同,返回不同的数据模板。比如,type 字段来做区分,rider 代表返回骑手的相关信息,order 代表返回订单的相关信息,delivery 代表返回运单的相关。服务内部就可以依据 type 的不同,来分别封装实现。
另外,Golang 接口不能包含任何变量,且允许为空。空接口 interface{} 没有任何方法,所以所有类型都实现了空接口。
func TestGeneric(t *testing.T) {var values []interface{}values = append(values, 42)values = append(values, "hello")values = append(values, 3.14)for _, value := range values {fmt.Println(value)}
}
上述代码展示了如何使用空接口 (interface{}) 来存储不同类型的值,并通过循环遍历这些值进行打印,仔细看是不是像泛型?
因为接口在定义一组方法时没有对实现的接收者做限制,所以在这一节的最后,来探讨下结构体实现接口和结构体指针实现接口的不同。
- 示例一
type Animal interface {Say()
}type Cat struct{}// 接收者类型为结构体指针
func (c *Cat) Say() {fmt.Println("miu")
}func TestStruct(t *testing.T) {var animal Animal = &Cat{}animal.Say()
}
程序正常运行。
- 示例二
type Animal interface {Say()
}type Cat struct{}// 接收者类型为结构体
func (c Cat) Say() {fmt.Println("miu")
}func TestStruct(t *testing.T) {var animal Animal = &Cat{}animal.Say()
}
程序正常运行。
- 示例三
type Animal interface {Say()
}type Cat struct{}// 接收者类型为结构体
func (c Cat) Say() {fmt.Println("miu")
}func TestStruct(t *testing.T) {var animal Animal = Cat{}animal.Say()
}
程序正常运行。
- 示例四
type Animal interface {Say()
}type Cat struct{}// 接收者类型为结构体指针
func (c *Cat) Say() {fmt.Println("miu")
}func TestStruct(t *testing.T) {var animal Animal = Cat{}animal.Say()
}
运行失败,输出
cannot use Cat{} (value of type Cat) as type Animal in variable declaration:Cat does not implement Animal (Say method has pointer receiver)
编译器提示 Cat 没有实现 Animal 接口,Say 方法接受的是指针。
针对上述四个示例汇总如下
| 结构体实现接口 | 结构体指针实现接口 | |
|---|---|---|
| 结构体初始化变量 | 通过 | 不通过 |
| 结构体指针初始化变量 | 通过 | 通过 |
为什么会出现这种情况呢?我们知道 Golang 中传递参数都是值传递
- 对于
&Cat{}来说,这意味着拷贝一个新的&Cat{}指针,不过这个指针与原来的指针指向一个相同并且唯一的结构体,所以编译器可以隐式的对变量解引用(dereference)获取指针指向的结构体 - 而对于
Cat{}来说,这意味着Say方法会接受一个全新的Cat{},因为方法的参数是*Cat,编译器不会无中生有创建一个新的指针;即使编译器可以创建新指针,这个指针指向的也不是最初调用该方法的结构体;
这里可以看出,当接受者为结构体时,那么在方法调用的时候需要传值,拷贝参数,这里会有性能损失,因此建议在实际项目中,接受者使用结构体指针来实现。
1.2 数据结构
golang版本1.19.12
Golang 中有两种略微不同的接口,一种是带有一组方法的接口,另一种是不带任何方法的接口,下面就分别介绍下其底层实现。
1.2.1 空接口
空接口的实现如下
type EmptyInterface {}
其底层的数据结构如下
// runtime/runtime2.go
type eface struct {_type *_typedata unsafe.Pointer
}
eface 的结构体由两个属性构成,一个是类型信息 _type,一个是数据信息 data,占 16 个字节。
_type属性,存放的是类型、方法等信息。
// Needs to be in sync with ../cmd/link/internal/ld/decodesym.go:/^func.commonsize,
// ../cmd/compile/internal/reflectdata/reflect.go:/^func.dcommontype and
// ../reflect/type.go:/^type.rtype.
// ../internal/reflectlite/type.go:/^type.rtype.
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 // 比较两个形参对应对象的类型是否相等gcdata *byte // GC 类型的数据str nameOff // 类型名称字符串在二进制文件段中的偏移量ptrToThis typeOff // 类型元信息指针在二进制文件段中的偏移量
}
其中可以关注下 kind,这个字段描述的是如何解析基础类型。在 Golang 中,基础类型是一个枚举常量,有 26 个基础类型,如下。枚举值通过 kindMask 取出特殊标记位。
// runtime/typekind.go
const (kindBool = 1 + iotakindIntkindInt8kindInt16kindInt32kindInt64kindUintkindUint8kindUint16kindUint32kindUint64kindUintptrkindFloat32kindFloat64kindComplex64kindComplex128kindArraykindChankindFunckindInterfacekindMapkindPtrkindSlicekindStringkindStructkindUnsafePointerkindDirectIface = 1 << 5kindGCProg = 1 << 6kindMask = (1 << 5) - 1
)
这里再做个简单的展开, kindMask 的值为 31,对应的二进制为 00011111,也就是低五位都为 1,再看下 Golang 中有 26 个基础类型,也就是都比 kindMask 值要小,这时利用位与运算的特性(如果两个对应位都是 1,则结果位为 1,否则为 0),二者做与运算可以获取对应类型的种类信息。
data属性,指向原始数据的指针,是一个unsafe.Pointer类型
下面用个示例演示空接口 eface 数据到底是如何存储的。
type nameOff int32 // offset to a name
type typeOff int32 // offset to an *rtype
type tflag uint8type eface struct {_type *_typedata unsafe.Pointer
}type _type struct {size uintptrptrdata uintptr // size of memory prefix holding all pointershash uint32tflag tflagalign uint8fieldAlign uint8kind uint8// function for comparing objects of this type// (ptr to object A, ptr to object B) -> ==?equal func(unsafe.Pointer, unsafe.Pointer) bool// gcdata stores the GC type data for the garbage collector.// If the KindGCProg bit is set in kind, gcdata is a GC program.// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.gcdata *bytestr nameOffptrToThis typeOff
}func TestEFace(t *testing.T) {var i interface{} = 3.14e := (*eface)(unsafe.Pointer(&i))fmt.Println(e)fmt.Println(e._type)fmt.Println(e.data)}
打印输出一目了然。
&{0x1049dc640 0x1049ce618}
&{8 0 2472095124 7 8 8 14 0x1048bafd0 0x1049ce678 7052 34048}
0x1049ce618
从上述 eface 结构的两个属性可以推断出,Golang 的任意类型都可以转换成 interface{}。
1.2.2 非空接口
空接口的实现如下
type NoEmptyInterface {Say()
}
其底层的数据结构如下
type iface struct {tab *itabdata unsafe.Pointer
}
iface 结构体也是占 16 个字节,这里的 data 属性和空接口的 eface 里的 data 作用相同,这里就不做赘述。下面来看看 tab 属性。
// layout of Itab known to compilers
// allocated in non-garbage-collected memory
// Needs to be in sync with
// ../cmd/compile/internal/reflectdata/reflect.go:/^func.WriteTabs.
type itab struct {inter *interfacetype // 存的是 interface 自己的静态类型_type *_type // 存的是 interface 对应具体对象的类型hash uint32 // 是对 _type.hash 的拷贝_ [4]bytefun [1]uintptr // 是一个函数指针,它指向的是具体类型的函数方法
}
itab 结构体是非空接口的核心组成部分,占 32 字节,着重看下 inter 和 _type 属性。
type imethod struct {name nameOffityp typeOff
}type interfacetype struct {typ _type // 类型元信息pkgpath name // 包路径和描述信息等等mhdr []imethod // 方法
}
inter 存储的是非空接口自己类型相关数据,因为 Golang 中函数方法是以包为单位隔离的。所以 interfacetype 除了保存 _type 还需要保存包路径等描述信息。mhdr 存的是各个 interface 函数方法在段内的偏移值 offset,知道偏移值以后才方便调用。
_type 在上一节空接口已详细介绍过,存储的是接口类型的元信息,这里就不展开。
这里还要说下 tab 中 fun 属性,存储的是指向实现非空接口类型的方法数组。
type iface struct {tab *itabdata unsafe.Pointer
}type itab struct {inter *interfacetype_type *_typehash uint32 // copy of _type.hash. Used for type switches._ [4]bytefun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}type imethod struct {name nameOffityp typeOff
}type interfacetype struct {typ _typepkgpath namemhdr []imethod
}type name struct {bytes *byte
}type _type struct {size uintptrptrdata uintptr // size of memory prefix holding all pointershash uint32tflag tflagalign uint8fieldAlign uint8kind uint8// function for comparing objects of this type// (ptr to object A, ptr to object B) -> ==?equal func(unsafe.Pointer, unsafe.Pointer) bool// gcdata stores the GC type data for the garbage collector.// If the KindGCProg bit is set in kind, gcdata is a GC program.// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.gcdata *bytestr nameOffptrToThis typeOff
}type People interface{Say()
}type Student struct {}func (s *Student) Say() {fmt.Println("hello world")
}func TestNoEmptyInterface(t *testing.T) {s := &Student{}var i People = snoEmpty := (*iface)(unsafe.Pointer(&i))fmt.Println(noEmpty)fmt.Println(noEmpty.tab)fmt.Println(noEmpty.tab.inter)fmt.Println(noEmpty.tab._type)fmt.Println(noEmpty.tab.fun)fmt.Println(noEmpty.data)}
打开 Goland 的调试模式,可以很清晰的看到非空接口内部字段都是如何存储的。
type Student struct{}func (stu *Student) Show() {}func live() People {var stu *Studentreturn stu
}func TestNil(t *testing.T) {stu := live()if stu == nil {fmt.Println("nil")} else {fmt.Println("not nil")}}
看完这一节,上面输出应该就简单了吧,输出 not nil。至于原因嘛,在运行时,非空接口只是 data 为 nil,但是 tab 可不为 nil。
这里再做个展开,简单说说动态派发。
Go 中 interface 可以动态派发方法,实现类似面向对象语言中的多态的特性。
func TestDynamic(t *testing.T) {var stu People = &Student{}stu.Show()
}
这里为了正常实现 stu.Show() 方法调用,需要构建 iface 结构,之后再依据 *tab 里面存的 fun 指针做一次寻址,接着才能调用。
func TestDynamic(t *testing.T) {var stu := &Student{}stu.Show()
}
这里直接调用结构体的方法,少了构建 iface 结构以及寻址的时间,性能应该比动态派发要好。不过,指针实现的动态派发造成的性能损失非常小,相对于一些复杂逻辑的处理函数,这点性能损失几乎可以忽略不计。
2、反射
Golang 中的反射是用标准库中的 reflect 包实现,reflect 包实现了 runtime (运行时)的反射能力,能够让程序操作不同的对象。
reflect 包中有两个非常重要的函数:
reflect.TypeOf能获取类型信息;reflect.ValueOf能获取数据的运行时表示;
2.1 TypeOf
先来看看 TypeOf 函数
// reflect/type.go
// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i any) Type {eface := *(*emptyInterface)(unsafe.Pointer(&i))return toType(eface.typ)
}// toType converts from a *rtype to a Type that can be returned
// to the client of package reflect. In gc, the only concern is that
// a nil *rtype must be replaced by a nil Type, but in gccgo this
// function takes care of ensuring that multiple *rtype for the same
// type are coalesced into a single Type.
func toType(t *rtype) Type {if t == nil {return nil}return t
}
其中入参 i 的类型 any 就是 interface{} 的别名,any 在 Golang 1.18 中引入,表示任意类型。
type any = interface{}
TypeOf 函数实现很简单,只是将一个 interface{} 变量转换成了内部的 emptyInterface 表示,然后从中获取相应的类型信息。里面有个强制转换,把 any 转成了 emptyInterface。下面来看看 emptyInterface 的数据结构:
// emptyInterface is the header for an interface{} value.
type emptyInterface struct {typ *rtypeword unsafe.Pointer
}// rtype is the common implementation of most values.
// It is embedded in other struct types.
//
// rtype must be kept in sync with ../runtime/type.go:/^type._type.
type rtype struct {size uintptrptrdata uintptr // number of bytes in the type that can contain pointershash uint32 // hash of type; avoids computation in hash tablestflag tflag // extra type information flagsalign uint8 // alignment of variable with this typefieldAlign uint8 // alignment of struct field with this typekind uint8 // enumeration for C// function for comparing objects of this type// (ptr to object A, ptr to object B) -> ==?equal func(unsafe.Pointer, unsafe.Pointer) boolgcdata *byte // garbage collection datastr nameOff // string formptrToThis typeOff // type for pointer to this type, may be zero
}
仔细看看 emptyInterface 结构,发现其属性和上一章节的空接口是一模一样的,只是名称改了下, rtype 用于表示变量的类型, word 指向内部封装的数据。
从 TypeOf 参数 i any 中可以看出,入参是两种类型
- 一种是具体类型变量,
TypeOf()返回的具体类型信息 - 一种是
interface类型变量- 如果
i绑定了具体类型对象实例,返回的是i绑定具体类型的动态类型信息; - 如果
i没有绑定任何具体的类型对象实例,返回的是接口自身的静态类型信息。
- 如果
type Forest interface{}type Tree struct{}func TestBasicType(t *testing.T) {ifa := &Tree{}rfa := reflect.TypeOf(ifa)fmt.Println("第一组输出:", rfa.Elem().Name(), rfa.Elem().Kind().String())var ifb Forest = &Tree{}ifc := new(Forest)rfb := reflect.TypeOf(ifb)rfc := reflect.TypeOf(ifc)fmt.Println("第二组输出:", rfb.Elem().Name(), rfb.Elem().Kind().String())fmt.Println("第二组输出:", rfc.Elem().Name(), rfc.Elem().Kind().String())
}
输出
第一组输出: Tree struct
第二组输出: Tree struct
第二组输出: Forest interface
- 第一组输出中
ifa是具体的类型,所以返回本身的类型Tree,对应的kind为struct。 - 第二组输出中
ifb是interface类型变量,绑定具体的类型对象实例,所以返回的是绑定的具体类型Forest,对应的kind为struct。ifc是interface类型变量,且没有绑定任何具体的类型对象实例,所以返回的是本身的类型Forest,对应的kind为interface。
2.2 ValueOf
再来看看 ValueOf 函数
// reflect/type.go
// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero Value.
func ValueOf(i any) Value {if i == nil {return Value{}}// TODO: Maybe allow contents of a Value to live on the stack.// For now we make the contents always escape to the heap. It// makes life easier in a few places (see chanrecv/mapassign// comment below).escapes(i)return unpackEface(i)
}// Dummy annotation marking that the value x escapes,
// for use in cases where the reflect code is so clever that
// the compiler cannot follow.
func escapes(x any) {if dummy.b {dummy.x = x}
}var dummy struct {b boolx any
}// unpackEface converts the empty interface i to a Value.
func unpackEface(i any) Value {e := (*emptyInterface)(unsafe.Pointer(&i))// NOTE: don't read e.word until we know whether it is really a pointer or not.t := e.typif t == nil {return Value{}}f := flag(t.Kind())if ifaceIndir(t) {f |= flagIndir}return Value{t, e.word, f}
}
ValueOf 实现也很简单,若 i 为 nil 返回零值 Value{}。否则就先调用 escapes(i) 确保值逃逸到堆上,然后调用 reflect.unpackEface ,先把 i interface{} 转换成 emptyInterface,然后将具体类型和指针包装成 reflect.Value 结构体后返回。
这里对内存逃逸做个简单的说明。在 Golang 中,内存逃逸通常发生在以下情况:
1.变量的地址被返回给调用者。
2.变量的地址被赋值给全局变量或者其他包级变量。
3.变量的地址被传递给不透明的函数或方法(例如接口方法)。
上面的 dump 是个全局变量,当 x 赋值给全部变量的 x 属性时就会发生逃逸。其实,在变量 i 传给函数 escapes 时就已经发生了逃逸(见上述第三种情况),笔者在本机环境中(Golang 版本为 1.19.12)测试也是如此:
package _0240619import "testing"func escapes(x any) {if dummy.b {dummy.x = x}
}var dummy struct {b boolx any
}func TestEscapes(t *testing.T) {i := 11escapes(i)
}
执行 go tool compile -m escapes_test.go ,输出如下
escapes_test.go:5:6: can inline escapes
escapes_test.go:16:6: can inline TestEscapes
escapes_test.go:19:9: inlining call to escapes
escapes_test.go:5:14: leaking param: x
escapes_test.go:16:18: t does not escape
escapes_test.go:19:9: i escapes to heap
这里的 19 行就是 escapes(i),也就是传参的时候由于此函数入参是个 interface{} 因此发生了内存逃逸。
那什么时候仍然需要 escapes 函数?如果在某些特定情况下,编译器无法自动判定变量需要逃逸到堆上,而又需要强制变量逃逸,那么 escapes 函数仍然是有用的。它可以明确告诉编译器这个变量需要在堆上分配。
那为什么要在 ValueOf 中把变量的内存逃逸到堆上呢?源码上写的原因是 这样标记是为了防止反射代码写的过于高级,以至于编译器跟不上了。
这里我理解为 ValueOf 通常用于反射操作,在反射中我们经常需要确保被反射的对象在堆上,以便反射包能够安全地访问和修改这些对象。例如:
- 反射可能需要长时间持有对象的引用。
- 反射可能会传递对象到其他地方(例如,作为返回值)。
- 反射可能会在不同的
Goroutine之间传递对象。
2.3 反射三定律
Golang 开派祖师之一 Rob Pike 于 2011 年写的 The Laws of Reflection,提出了反射三大定律
- 反射可以从接口值到反射对象
- 反射可以从反射对象中获得接口值
- 要修改反射对象,其值必须可设置
2.3.1 反射可以从接口值得到反射对象
反射第一定律即通过一个接口值,我们可以获取其对应的反射对象。可以通过 reflect.TypeOf和 reflect.ValueOf 函数来实现。因为二者的入参都是 interface{},传参的时候会发生类型转换。
func TestReflectFirstLaw(t *testing.T) {var x float64 = 3.4tp := reflect.TypeOf(x) // 得到类型信息 reflect.Typev := reflect.ValueOf(x) // 得到值信息 reflect.Valuefmt.Println("type:", tp) // 输出 float64fmt.Println("value:", v) // 3.4}
2.3.2 反射可以从反射对象中获得接口值
即通过一个反射对象,我们可以还原出其对应的接口值。可以通过 reflect.Value 的 Interface 方法来实现。
不过调用 reflect.Value.Interface 方法只能获得 interface{} 类型的变量,如果想要将其还原成最原始的状态还需要经过如下所示的显式类型转换:
func TestReflectSecondLaw(t *testing.T) {var x float64 = 3.4v := reflect.ValueOf(x)y := v.Interface().(float64) // 将反射值转回为接口值fmt.Println(y) // 3.4
}
仔细看,发现第一定律和第二定律是互为逆向的过程:
- 从接口值得到反射对象:
- 从基本类型到接口类型的类型转换(这里是
隐式转换); - 从接口类型到反射对象的转换;
- 从基本类型到接口类型的类型转换(这里是
- 从反射对象的到接口值:
- 反射对象转换成接口类型;
- 通过
显式类型转换变成原始类型;
2.3.3 要修改反射对象,其值必须可设置
如果我们想要更新一个 reflect.Value,那么它持有的值一定是可以被更新的。
func TestReflectThirdLaw(t *testing.T) {f := 3.14v := reflect.ValueOf(f)v.SetFloat(6.52)
}
运行后报如下错误
panic: reflect: reflect.Value.SetFloat using unaddressable value [recovered]panic: reflect: reflect.Value.SetFloat using unaddressable value
这里给的提示信息是使用了不可寻址(unaddressable)的 Value。
我们知道 Golang 的函数的参数都是值传递的,这里 reflect.ValueOf(f) 传值后,会拷贝一个 f 的副本,与原来的 f 就没关系了,此时再改 f 的值就会报不可寻址。
修复也简单,传指针即可,虽然是值拷贝,但是指向的都是同一块内存地址。
func TestReflectThirdLaw(t *testing.T) {f := 3.14v := reflect.ValueOf(f)if !v.CanSet() {v = reflect.ValueOf(&f)v = v.Elem()}v.SetFloat(6)fmt.Println(v)}
2.4 实例
首先通过一个综合的示例,来把上面列举的 TypeOf 和 ValueOf 给串起来。
type Monster struct {Name string `json:"name"`Age int `json:"monster_age"`Score float32Sex string
}func (m *Monster) Print() {fmt.Println("---start---")fmt.Println(m)fmt.Println("---end---")
}func (m *Monster) GetSum(n1, n2 int) int {return n1 + n2
}func (m *Monster) Set(name string, age int, score float32, sex string) {m.Name = namem.Age = agem.Score = scorem.Sex = sex
}func ProcStruct(a interface{}) {typ := reflect.TypeOf(a)val := reflect.ValueOf(a)valMethod := valif typ.Kind() == reflect.Ptr {typ = typ.Elem()}if typ.Kind() != reflect.Struct {fmt.Println("expect struct")return}if val.Kind() == reflect.Ptr {val = val.Elem()}num := val.NumField()fmt.Printf("struct has %d field\n", num)for i := 0; i < num; i++ {fmt.Printf("Field %d: 属性为 = %v\n", i, typ.Field(i).Name)fmt.Printf("Field %d: 值为 = %v\n", i, val.Field(i))tagVal := typ.Field(i).Tag.Get("json")if tagVal != "" {fmt.Printf("Field %d: tag 为 %v\n", i, tagVal)}}numOfMethod := valMethod.NumMethod()fmt.Printf("struct has %d methods\n", numOfMethod)valMethod.Method(1).Call(nil)var params []reflect.Valueparams = append(params, reflect.ValueOf(10))params = append(params, reflect.ValueOf(40))res := valMethod.Method(0).Call(params)fmt.Println("res = ", res[0].Int())}func TestReflectDemo(t *testing.T) {monster := &Monster{Name: "Cat",Age: 400,Score: 30.8,Sex: "10",}ProcStruct(monster)}
打印输出
struct has 4 field
Field 0: 属性为 = Name
Field 0: 值为 = Cat
Field 0: tag 为 name
Field 1: 属性为 = Age
Field 1: 值为 = 400
Field 1: tag 为 monster_age
Field 2: 属性为 = Score
Field 2: 值为 = 30.8
Field 3: 属性为 = Sex
Field 3: 值为 = 10
struct has 3 methods
---start---
&{Cat 400 30.8 10}
---end---
res = 50
这里需要关注下:
NumField(),获取结构体字段的数量NumMethod, 获取reflect.Value可以访问的方法数量,这包括值接收者和指针接收者的方法- 值接收器:方法定义在值类型上。例如:
func (m Monster) Print() - 指针接收器:方法定义在指针类型上。例如:
func (m *Monster) Print()
- 值接收器:方法定义在值类型上。例如:
来看看下面这段代码
type Foo struct {Name string
}func (f *Foo) Method1() {fmt.Println("Method1 called")
}func TestNum(t *testing.T) {f := &Foo{}v := reflect.ValueOf(f)fmt.Println(v)fmt.Println(v.Elem())fmt.Println(v.Elem().NumField())fmt.Println(v.NumMethod())
}
打印输出
&{}
{}
1
1
再结合上面列出的关注点,就很清楚二者要获取数量的注意点了。
NumField(),针对的是结构体本身的字段数量,若接收器(receiver)是结构体指针,需要解引用指针,获取指针指向的reflect.Value,也就是结构体NumMethod(),用于获取某个reflect.Value所代表的类型的方法数量,即使类型为指针,也无需使用Elem()进行解引用。
其次,再来个依据反射动态调用方法
type DynamicStruct struct {Name stringAge intAddr string
}func (d *DynamicStruct) PrintName(ctx context.Context) error {fmt.Println(d.Name)return nil
}func (d *DynamicStruct) PrintAge(ctx context.Context) error {fmt.Println(d.Age)return nil
}func (d *DynamicStruct) PrintAddr(ctx context.Context) error {fmt.Println(d.Addr)return nil
}func TestDynamic(t *testing.T) {dynamic := &DynamicStruct{Name: "molaifeng",Age: 18,Addr: "beijing",}ctx := context.Background(){method, exits := dynamic.GetMethod("Name")if !exits {return}_ = method(ctx)}{method, exits := dynamic.GetMethod("hobby")if !exits {return}_ = method(ctx)}}func (d *DynamicStruct) GetMethod(name string) (func(ctx context.Context) error, bool) {methodName := "Print" + namemethod := reflect.ValueOf(d).MethodByName(methodName)if !method.IsValid() {fmt.Println(fmt.Printf("%v not exist", methodName))return nil, false}return method.Interface().(func(ctx context.Context) error), true
}
打印输出
molaifeng
Printhobby not exist20 <nil>
这里主要用到了 MethodByName 获取反射对象的方法,有了前面例子打底,这里就不做过多介绍。另外一个就是 Interface() 方法了,这个方法可以获取反射对象,但是呢,要还原成原本的类型,还需显示的转换,于是就有下面这个
method.Interface().(func(ctx context.Context) error), true
其实这个很好理解,看看具体 *DynamicStruct 具体绑定的方法,返回值是不是就是上面括号里的。
再来个简化版的
func TestInt(t *testing.T) {num := 99v := reflect.ValueOf(num)i := v.Interface().(int)fmt.Println(i)}
最后,来个校验参数的例子
type Params struct {Name *stringAge *intAddr *string
}func TestValidParam(t *testing.T) {requiredParam := map[string]bool{"Name": true,"Age": true,}params := &Params{}err := CheckParams(params, requiredParam)if err != nil {fmt.Println(err)return}}func CheckParams(params *Params, requiredParam map[string]bool) error {val := reflect.ValueOf(params).Elem()for fieldName := range requiredParam {field := val.FieldByName(fieldName)if isFieldNil(field) {return fmt.Errorf("%v is required,actual nil", fieldName)}}return nil}func isFieldNil(v reflect.Value) bool {k := v.Kind()switch k {case reflect.Slice, reflect.Map, reflect.Ptr:return v.IsNil()default:return false}
}
打印如下
Name is required,actual nil
相关文章:
说说 golang 中的接口和反射
1、接口 1.1 类型 Golang 中的接口是一组方法的签名,是实现多态和反射的基础。 type 接口名 interface {method1(参数列表) 返回值列表method2(参数列表) 返回值列表 }不同于 Java 语言,使用 implements 关键字显示的实现接口。Golang 接口的实现都是…...
小程序注册
【 一 】小程序注册 微信公众平台 https://mp.weixin.qq.com/ https://mp.weixin.qq.com/注册 邮箱激活 小程序账户注册 微信小程序配置 微信小程序开发流程 添加项目成员 【 二 】云服务 lass 基础设施服务(组装机) 你买了一大堆的电脑配件&#x…...
工作记录2
1. 要实现y轴超出部分滚动的效果,可以这样写 <div style"max-height: 384px; overflow-y: auto;"> </div> 2. 当后端接口还没好的时候,可以自己模拟一下接口返回的数据 export const getCommodityDetail (id) > Promise.re…...
linux挂载硬盘(解决linux不显示硬盘问题)
目录 1.查看系统有几块硬盘2.查看挂载情况3.格式化硬盘4.创建挂载目录用于挂载硬盘5.将硬盘挂载到指定的挂载目录6.随系统自启动挂载查看配置文件,看是否已经把这条命令加入配置 帮同门解决挂载失败问题记录 参考视频:只要6步!Linux系统下挂载…...
运输标签扫描仪可简化运输和接收任务
Dynamic Web TWAIN 是一个专为Web应用程序设计的TWAIN扫描识别控件。你只需在TWAIN接口写几行代码,就可以用兼容TWAIN的扫描仪扫描文档或从数码相机/采集卡中获取图像。然后用户可以编辑图像并将图像保存为多种格式,用户可保存图像到远程数据库或者Share…...
Stable Diffusion 3 大模型文生图实践
windows教程2024年最新Stable Diffusion本地化部署详细攻略,手把手教程(建议收藏!!)_stable diffusion 本地部署-CSDN博客 linux本地安装教程 1.前期准备工作 1)创建conda环境 conda create --name stable3 python3.10 2)下…...
Linux grep技巧 删除含有指定关键词的行,创建新文件
一. 需求 ⏹有如下文件,现要求 删除含有xuecheng关键字的行删除含有192.168.1.1关键字的行也就是说,最终只会留下127.0.0.1 license.sublimehq.com 127.0.0.1 www.xuecheng.com 127.0.0.1 img.xuecheng.com 192.168.1.1 www.test.com 127.0.0.1 video…...
ChatMoney还能写剧本杀?
本文由 ChatMoney团队出品 近年来,剧本杀作为一种新兴社交游戏,收到了越来越多人的喜爱,它不仅需要玩家们发挥自身演技,还需运用逻辑思维推理,分析所获得的线索,找出案件真凶。然而你是否想过,你…...
优化系统小工具
一款利用VB6编写的系统优化小工具,系统优化、桌面优化、清理垃圾、查找文件等功能。 下载:https://download.csdn.net/download/ty5858/89432367...
调幅信号AM的原理与matlab实现
平台:matlab r2021b 本文知识内容摘自《软件无线电原理和应用》 调幅就是使载波的振幅随调制信号的变化规律而变化。用音频信号进行调幅时,其数学表达式可以写为: 式中,为调制音频信号,为调制指数,它的范围在(0&…...
[MySql]两阶段提交
文章目录 什么是binlog使用binlog进行恢复的流程 什么是redolog缓冲池redologredolog结构 两阶段提交 什么是binlog binlog是二进制格式的文件,用于记录用户对数据库的修改,可以作用于主从复制过程中数据同步以及基于时间点的恢复(PITR&…...
掌握rpc、grpc并探究内在本质
文章目录 rpc是什么?又如何实现服务通信?理解rpcRPC的通信过程通信协议的选择小结RPC VS Restful net_rpc实践案例net/rpc包介绍创建服务端创建client 看看net_rpc的通信调度实现的内部原理明确目标基于自己实现的角度分析我会怎么做代码分析 grpc介绍与…...
构造,析构,垃圾回收
构造函数 基本概念 在实例化对象时 会调用的用于初始化的函数 如果不写,默认存在一个无参构造函数 构造函数的写法 1.没有返回值 2.函数名和类名必须相同 3.没有特殊需求时,一般都是public的 4.构造函数可以被重载 5.this代表当前调用该函数的对…...
杂记 | 搭建反向代理防止OpenAI API被封禁(对于此次收到邮件提示7月9日后将被屏蔽的解决参考)
文章目录 重要声明(免责)01 OpenAI封禁API的情况02 解决方案及原理2.1 原因分析2.2 解决方案2.3 步骤概述 03 操作步骤3.1 购买一个海外服务器3.2 申请一个域名3.3 将域名指向代理服务器3.4 在代理服务器上安装nginx3.5 配置反向代理 重要声明࿰…...
利用ref实现防抖
结合vue的customRef function debounceRef(value,time1000){ let t return customRef((track,trigger)>{ return { get(){ track() return value; } set(val){ clearTimeout(t) tsetTimeout(()>{ trigger() valueval },time) } } }) }...
SAP ABAP 之OOALV
文章目录 前言一、案例介绍/笔者需求二、SE24 查看类 a.基本属性 Properties b.接口 Interfaces c.友元 Friends d.属性 Attributes e.方法 Methods f.事件 Events g.局部类型 Types …...
构建实用的Flutter文件列表:从简到繁的完美演进
前言:为什么我们需要文件列表? 在现代科技发展迅速的时代,我们的电脑、手机、平板等设备里积累了大量的文件,这些文件可能是我们的照片、文档、音频、视频等等。然而,当文件数量增多时,我们如何快速地找到…...
spring使用@PostConstruct踩得坑
情况说明: 在一个抽象类中使用PostConstruct注解方法init用于初始化操作。然后每个实现类在初始化时都会调用PostConstruct注解的init方法执行初始化操作。如下代码: public abstract class AbstractClass {/*** 存放各实例.*/public static final Map&…...
【Mac】XnViewMP for Mac(图片浏览查看器)及同类型软件介绍
软件介绍 XnViewMP 是一款多功能、跨平台的图像查看和管理软件,适用于 macOS、Windows 和 Linux 系统。它是经典 XnView 软件的增强版本,更加现代化且功能更强大。XnViewMP 支持数百种图像格式,并提供多种图像处理工具,使其成为摄…...
win10修改远程桌面端口,Windows 10下修改远程桌面端口及服务器关闭445端口的操作指南
Windows 10下修改远程桌面端口及服务器关闭445端口的操作指南 一、修改Windows 10远程桌面端口 在Windows 10系统中,远程桌面连接默认使用3389端口。为了安全起见,建议修改此端口以减少潜在的安全风险。以下是修改远程桌面端口的步骤: 1. 打…...
【杂谈】-递归进化:人工智能的自我改进与监管挑战
递归进化:人工智能的自我改进与监管挑战 文章目录 递归进化:人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管?3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...
K8S认证|CKS题库+答案| 11. AppArmor
目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作: 1)、切换集群 2)、切换节点 3)、切换到 apparmor 的目录 4)、执行 apparmor 策略模块 5)、修改 pod 文件 6)、…...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...
Docker 运行 Kafka 带 SASL 认证教程
Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明:server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...
VTK如何让部分单位不可见
最近遇到一个需求,需要让一个vtkDataSet中的部分单元不可见,查阅了一些资料大概有以下几种方式 1.通过颜色映射表来进行,是最正规的做法 vtkNew<vtkLookupTable> lut; //值为0不显示,主要是最后一个参数,透明度…...
leetcodeSQL解题:3564. 季节性销售分析
leetcodeSQL解题:3564. 季节性销售分析 题目: 表:sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...
《基于Apache Flink的流处理》笔记
思维导图 1-3 章 4-7章 8-11 章 参考资料 源码: https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...
大学生职业发展与就业创业指导教学评价
这里是引用 作为软工2203/2204班的学生,我们非常感谢您在《大学生职业发展与就业创业指导》课程中的悉心教导。这门课程对我们即将面临实习和就业的工科学生来说至关重要,而您认真负责的教学态度,让课程的每一部分都充满了实用价值。 尤其让我…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
SQL慢可能是触发了ring buffer
简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...
