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)
}

在这个例子中,DogCat 类型都实现了 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 iface struct {
	_type *_type
	data 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 + iota
	kindInt
	kindInt8
	kindInt16
	kindInt32
	kindInt64
	kindUint
	kindUint8
	kindUint16
	kindUint32
	kindUint64
	kindUintptr
	kindFloat32
	kindFloat64
	kindComplex64
	kindComplex128
	kindArray
	kindChan
	kindFunc
	kindInterface
	kindMap
	kindPtr
	kindSlice
	kindString
	kindStruct
	kindUnsafePointer

	kindDirectIface = 1 << 5
	kindGCProg      = 1 << 6
	kindMask        = (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 uint8

type eface struct {
	_type *_type
	data  unsafe.Pointer
}

type _type struct {
	size       uintptr
	ptrdata    uintptr // size of memory prefix holding all pointers
	hash       uint32
	tflag      tflag
	align      uint8
	fieldAlign uint8
	kind       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    *byte
	str       nameOff
	ptrToThis typeOff
}

func TestEFace(t *testing.T) {

	var i interface{} = 3.14
	e := (*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  *itab
	data 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]byte
	fun   [1]uintptr // 是一个函数指针,它指向的是具体类型的函数方法
}

itab 结构体是非空接口的核心组成部分,占 32 字节,着重看下 inter_type 属性。

type imethod struct {
	name nameOff
	ityp typeOff
}

type interfacetype struct {
	typ     _type // 类型元信息
	pkgpath name  // 包路径和描述信息等等
	mhdr    []imethod // 方法
}

inter 存储的是非空接口自己类型相关数据,因为 Golang 中函数方法是以包为单位隔离的。所以 interfacetype 除了保存 _type 还需要保存包路径等描述信息。mhdr 存的是各个 interface 函数方法在段内的偏移值 offset,知道偏移值以后才方便调用。

_type 在上一节空接口已详细介绍过,存储的是接口类型的元信息,这里就不展开。

这里还要说下 tabfun 属性,存储的是指向实现非空接口类型的方法数组。

type iface struct {
	tab  *itab
	data unsafe.Pointer
}

type itab struct {
	inter *interfacetype
	_type *_type
	hash  uint32 // copy of _type.hash. Used for type switches.
	_     [4]byte
	fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

type imethod struct {
	name nameOff
	ityp typeOff
}

type interfacetype struct {
	typ     _type
	pkgpath name
	mhdr    []imethod
}

type name struct {
	bytes *byte
}

type _type struct {
	size       uintptr
	ptrdata    uintptr // size of memory prefix holding all pointers
	hash       uint32
	tflag      tflag
	align      uint8
	fieldAlign uint8
	kind       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    *byte
	str       nameOff
	ptrToThis 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 = s

	noEmpty := (*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 *Student
	return stu
}

func TestNil(t *testing.T) {

	stu := live()
	if stu == nil {
		fmt.Println("nil")
	} else {
		fmt.Println("not nil")
	}

}

看完空接口这一节,上面输出应该就简单了吧,输出 not nil。至于原因嘛,在运行时,非空接口只是 datanil,但是 tab 可不为 nil

这里再做个展开,简单说说动态派发。

Gointerface 可以动态派发方法,实现类似面向对象语言中的多态的特性。

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{} 的别名,anyGolang 1.18 中引入,表示任意类型。

type any = interface{}

TypeOf 函数实现很简单,只是将一个 interface{} 变量转换成了内部的 emptyInterface 表示,然后从中获取相应的类型信息。里面有个强制转换,把 any 转成了 emptyInterface。下面来看看 emptyInterface 的数据结构:

// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
	typ  *rtype
	word 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       uintptr
	ptrdata    uintptr // number of bytes in the type that can contain pointers
	hash       uint32  // hash of type; avoids computation in hash tables
	tflag      tflag   // extra type information flags
	align      uint8   // alignment of variable with this type
	fieldAlign uint8   // alignment of struct field with this type
	kind       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) bool
	gcdata    *byte   // garbage collection data
	str       nameOff // string form
	ptrToThis 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,对应的 kindstruct
  • 第二组输出中
    • ifbinterface 类型变量,绑定具体的类型对象实例,所以返回的是绑定的具体类型 Forest,对应的 kindstruct
    • ifcinterface 类型变量,且没有绑定任何具体的类型对象实例,所以返回的是本身的类型 Forest,对应的 kindinterface

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 bool
	x 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.typ
	if t == nil {
		return Value{}
	}
	f := flag(t.Kind())
	if ifaceIndir(t) {
		f |= flagIndir
	}
	return Value{t, e.word, f}
}

ValueOf 实现也很简单,若 inil 返回零值 Value{}。否则就先调用 escapes(i) 确保值逃逸到堆上,然后调用 reflect.unpackEface ,先把 i interface{} 转换成 emptyInterface,然后将具体类型和指针包装成 reflect.Value 结构体后返回。

这里对内存逃逸做个简单的说明。在 Golang 中,内存逃逸通常发生在以下情况:

1.变量的地址被返回给调用者。
2.变量的地址被赋值给全局变量或者其他包级变量。
3.变量的地址被传递给不透明的函数或方法(例如接口方法)。

上面的 dump 是个全局变量,当 x 赋值给全部变量的 x 属性时就会发生逃逸。其实,在变量 i 传给函数 escapes 时就已经发生了逃逸(见上述第三种情况),笔者在本机环境中(Golang 版本为 1.19.12)测试也是如此:

package _0240619

import "testing"

func escapes(x any) {
	if dummy.b {
		dummy.x = x
	}
}

var dummy struct {
	b bool
	x any
}

func TestEscapes(t *testing.T) {
	i := 11
	escapes(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 Pike2011 年写的 The Laws of Reflection,提出了反射三大定律

  1. 反射可以从接口值到反射对象
  2. 反射可以从反射对象中获得接口值
  3. 要修改反射对象,其值必须可设置

2.3.1 反射可以从接口值得到反射对象

反射第一定律即通过一个接口值,我们可以获取其对应的反射对象。可以通过 reflect.TypeOfreflect.ValueOf 函数来实现。因为二者的入参都是 interface{},传参的时候会发生类型转换。

func TestReflectFirstLaw(t *testing.T) {
	var x float64 = 3.4
	
	tp := reflect.TypeOf(x) // 得到类型信息 reflect.Type
	v := reflect.ValueOf(x) // 得到值信息 reflect.Value
	
	fmt.Println("type:", tp) // 输出 float64
	fmt.Println("value:", v) // 3.4

}

2.3.2 反射可以从反射对象中获得接口值

即通过一个反射对象,我们可以还原出其对应的接口值。可以通过 reflect.ValueInterface 方法来实现。

不过调用 reflect.Value.Interface 方法只能获得 interface{} 类型的变量,如果想要将其还原成最原始的状态还需要经过如下所示的显式类型转换:

func TestReflectSecondLaw(t *testing.T) {
	var x float64 = 3.4
	v := reflect.ValueOf(x)
	y := v.Interface().(float64) // 将反射值转回为接口值
	fmt.Println(y) // 3.4
}

仔细看,发现第一定律和第二定律是互为逆向的过程:

  • 从接口值得到反射对象:
    • 从基本类型到接口类型的类型转换(这里是隐式转换);
    • 从接口类型到反射对象的转换;
  • 从反射对象的到接口值:
    • 反射对象转换成接口类型;
    • 通过显式类型转换变成原始类型;

2.3.3 要修改反射对象,其值必须可设置

如果我们想要更新一个 reflect.Value,那么它持有的值一定是可以被更新的。

func TestReflectThirdLaw(t *testing.T) {
	f := 3.14
	v := 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.14
	v := reflect.ValueOf(f)
	if !v.CanSet() {
		v = reflect.ValueOf(&f)
		v = v.Elem()
	}
	v.SetFloat(6)
	fmt.Println(v)}

2.4 实例

首先通过一个综合的示例,来把上面列举的 TypeOfValueOf 给串起来。

type Monster struct {
	Name  string `json:"name"`
	Age   int    `json:"monster_age"`
	Score float32
	Sex   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 = name
	m.Age = age
	m.Score = score
	m.Sex = sex
}

func ProcStruct(a interface{}) {

	typ := reflect.TypeOf(a)
	val := reflect.ValueOf(a)
	valMethod := val

	if 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.Value
	params = 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 string
	Age  int
	Addr 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" + name
	method := 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 := 99
	v := reflect.ValueOf(num)

	i := v.Interface().(int)
	fmt.Println(i)

}

最后,来个校验参数的例子

type Params struct {
	Name *string
	Age  *int
	Addr *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

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部