go反射之FieldByName、MethodByName什么时候加*

前言

首先要明白,Go中结构体方法的定义方式有两种,包括指针方法和值方法。

  • 如果一个方法的接收者的类型是其所属类型的指针类型(并非该类型本身),则该方法称为一个指针方法。
  • 如果一个方法的接收者类型就是其所属的类型本身,则该方法称为做值方法。

示例:

//指针方法
func (e *Employee) UpdateAge(newVal int){
	e.Age=newVal
}
//值方法
func (e Employee) UpdateAge(newVal int){
	e.Age=newVal
}

那么指针方法和值方法的区别在哪呢?

  • (e Employee)的定义是实例成员的拷贝,而不是成员本身;在实例对应方法被调用时,实例的成员会进行复制
  • (s *Student)的定义是指针的拷贝,只创建了指针的副本,若操作指针的话,操作的也是实例本身;同时该方法避免了大量数据的内存拷贝(一般情况下,推荐使用这种方式)。

示例:

func (e *Employee) UpdateAge(newVal int){
	e.Age=newVal
}
func TestMethod(t *testing.T){
	e:=Employee{"1","Amy",20}
	e.UpdateAge(10)
	fmt.Println(e.GetAge())
}
//output:
10
func (e Employee) UpdateAge(newVal int){
	e.Age=newVal
}
func TestMethod(t *testing.T){
	e:=Employee{"1","Amy",20}
	e.UpdateAge(10)
	fmt.Println(e.GetAge())
}
//output:
20

显而易见,e *Employee的方式操作了指针,指针指向了统一的对象,操作也即同一个对象;而e Employee的方式copy了一次对象,只对局部的对象进行了修改。

  • 为什么e *Employee可以直接.Age,而不是类似与C++的e->Age或者( * e).Age呢?

    其实Go在发送e为一个指针类型,同时其在访问某个字段时,就会将该表达式视为(*e).Age。

反射

FieldByName

在反射中,如果不稍加注意,*的加与不加就经常会导致panic,同样用以上的Employee举个栗子:

e:=&Employee{"1","Amy",20}
reflect.ValueOf(e).FieldByName("Name")

运行后报错“call of reflect.Value.FieldByName on ptr Value”:FieldByName不可操作指针类型

进入源码:

// FieldByName returns the struct field with the given name.
// It returns the zero Value if no field was found.
// It panics if v's Kind is not struct.
func (v Value) FieldByName(name string) Value {
	v.mustBe(Struct)
	if f, ok := v.typ.FieldByName(name); ok {
		return v.FieldByIndex(f.Index)
	}
	return Value{}
}
func (t *rtype) FieldByName(name string) (StructField, bool) {
	if t.Kind() != Struct {
		panic("reflect: FieldByName of non-struct type " + t.String())
	}
	tt := (*structType)(unsafe.Pointer(t))
	return tt.FieldByName(name)
}

在FieldByName中,对v的类型进行的判断,规定传入的为Struct的类型,否则将报错。

fmt.Println(reflect.ValueOf(e).Kind())
//output:
ptr

当将示例改成如下时:

e:=&Employee{"1","Amy",20}
reflect.ValueOf(*e).FieldByName("Name")

将成功运行获取Name的值,参考以上,打印reflect.ValueOf(*e)的Kind类型

fmt.Println(reflect.ValueOf(*e).Kind())
//output:
struct

MethodByName

UpdateAge如下:

func (e *Employee) UpdateAge(newVal int){
	e.Age=newVal
}
e:=&Employee{"1","Amy",20}
reflect.ValueOf(e).MethodByName("UpdateAge").
		Call([]reflect.Value{reflect.ValueOf(1)})
fmt.Println("New Age:",e.Age)
//output:
New Age: 1

以上代码是合理的结果,通过反射设置e的Age;

设置修改下代码,如下:

e:=&Employee{"1","Amy",20}
reflect.ValueOf(*e).MethodByName("UpdateAge").
		Call([]reflect.Value{reflect.ValueOf(1)})
fmt.Println("New Age:",e.Age)

将e改成*e;

运行后报错“reflect: call of reflect.Value.Call on zero Value [recovered]”,进入源码探索下:

MethodByName:

func (v Value) MethodByName(name string) Value {
	if v.typ == nil {
		panic(&ValueError{"reflect.Value.MethodByName", Invalid})
	}
	if v.flag&flagMethod != 0 {
		return Value{}
	}
	m, ok := v.typ.MethodByName(name)
	if !ok {
		return Value{}
	}
	return v.Method(m.Index)
}

v.typ.MethodByName:

func (t *rtype) MethodByName(name string) (m Method, ok bool) {
	if t.Kind() == Interface {
		tt := (*interfaceType)(unsafe.Pointer(t))
		return tt.MethodByName(name)
	}
	ut := t.uncommon()
	if ut == nil {
		return Method{}, false
	}
	// TODO(mdempsky): Binary search.
	for i, p := range ut.exportedMethods() {
		if t.nameOff(p.name).name() == name {
			return t.Method(i), true
		}
	}
	return Method{}, false
}

先查看下uncommon方法:

func (t *rtype) uncommon() *uncommonType {
	if t.tflag&tflagUncommon == 0 {
		return nil
	}
	switch t.Kind() {
	case Struct:
		return &(*structTypeUncommon)(unsafe.Pointer(t)).u
	case Ptr:
		type u struct {
			ptrType
			u uncommonType
		}
		return &(*u)(unsafe.Pointer(t)).u
	....
}

其对t的类型进行了判断并做相应的返回处理;

exportedMethods:

func (t *uncommonType) exportedMethods() []method {
   if t.xcount == 0 {
   	return nil
   }
   return (*[1 << 16]method)(add(unsafe.Pointer(t), uintptr(t.moff), "t.xcount > 0"))[:t.xcount:t.xcount]
}

在这里插入图片描述

发现mcount和xcount均为0;

mcount  uint16  // number of methods
xcount  uint16  // number of exported methods

我们试着将*e改为e,即:

e:=&Employee{"1","Amy",20}
reflect.ValueOf(e).MethodByName("UpdateAge").
		Call([]reflect.Value{reflect.ValueOf(1)})

在这里插入图片描述

在此种方式在,发现mcount和xounct均为1,即方法数量均为1。

这不禁让我们很疑惑,为何如此?

首先,在上面也讲到,在传入*e时,其实我们传入的是struct,在传入e时,传入的为ptr指针类型,而对于Go来说,有这样一条规则:一个指针类型拥有以它以及以它的基底类型为接收者类型的所有方法,而它的基底类型却只拥有以它本身为接收者类型的方法。

指针类型与基底类型其实是相对的概念:指针类型由指针加上某个基底类型组成,基底类型如上面所说的Employee,其指针类型就是*Employee。

回到上面的规则,也就是说*Employee拥有UpdateAge,而Employee没有此方法。

  • 也就是说,如果我们新增一个GetAge方法

    func (e Employee) GetAge() int{
    	return e.Age
    }
    

    这个时候*Employee拥有UpdateAge和GetAge,而Employee只拥有GetAge。

  • 为什么我们定义一个Employee的对象e1,可以直接e1.UpdateAge呢?

    如果Go语言发现调用的UpdateAge方法是e1的指针方法,则其会将表达式视为(&e1).GrUpdateAgeow()。

我们可以进行测试,将UpdateAge方法修改成如下:

func (e Employee) UpdateAge(newVal int){
	e.Age=newVal
}

此时无论是*Employee还是Employee,都拥有了此方法,再次运行代码:

e:=&Employee{"1","Amy",20}
reflect.ValueOf(*e).MethodByName("UpdateAge").
		Call([]reflect.Value{reflect.ValueOf(1)})
fmt.Println("New Age:",e.Age)
//output:
New Age: 20

发现Age为20,并未改变,这在前言已经说过,值传递中的修改只是修改了拷贝的副本,并未修改对象本身。

至此,大功告成,茅塞顿开。

总结

由于校招拿到offer的企业后台技术栈主要为Go,最近经历从Java转Golang的一个过程,以下是学习了几天后的小感悟吧:首先在Java中由于少了指针这一Part,在Java中通过引用地址的值传递会比较直接,无需顾虑太多,而对于Golang而言,其确实方便,关键字又少,虽说存在指针类型,但实际上Go开发团队暗中帮助我们屏蔽了很多底层的细节,如以上提到的(*e).Age、(&e1).GrUpdateAgeow()等表达式的速记法,会让开发者避免了很多bug,这其实也给我们在探索底层实现时稍稍加了点挑战吧,也算有利有弊。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章