Golang反射初步体会

参考文章:

https://zhuanlan.zhihu.com/p/34288219

 

 

    学习golang有一段时间了,但对框架中的反射代码仍然不是很清楚,因此花时间学习了下。同Java相比,golang中多出了“指针”这一概念,在学习的时候需要稍加注意。

一、unsafe.Pointer和uintptr

    golang中有“指针”的概念,但是和C/C++比起来,又有少许的不同,比如有:

C语言中的指针可以做+/-操作,典型的就是指向数组的指针。golang中不允许这种操作。

golang不允许指针类型的直接转换。

    此时就需要用到unsafe.Pointer和uintptr两个类。

unsafe.Pointer

    Pointer定义在unsafe包下unsafe.go文件中,其定义很简单:

type Pointer *ArbitraryType

    ArbitraryType类型是一个占位符一般的存在,没有任何意义,如同他的字面意思一样,可以代替任何类型。这样,可以把Pointer理解成为C语言中的(void *)类型。

    golang中的指针转换都要依赖于Pointer类,如下:

	a := new(int32)
	p := unsafe.Pointer(a)
	f := (*float32)(p)

	*f = 1.024
	fmt.Println(*a)

    打印出来的结果是:

1065554543

    也就是拿到了a的地址,把a使用的内存使用float32 1.024重写,再当做int32打印出来,得到的结果。这段代码如果没有第二行,直接把int32*转换成float32*,编译将报错。

    Pointer类的注释中描述了类的使用方法:

  • 任意的指针类型可以转换成Pointer

  • Pointer可以转换成任意的指针类型

  • uintptr可以转换成Pointer

  • Pointer可以转换成uintptr

    更详细的使用方法,可以参考Pointer类的注释。

uintptr

    uintptr定义在builtin包的builtin.go文件中。从注释看,uintptr就是一个整型值,但是足够大,可以保存任意指针的地址。

// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr

    若一个对象仅能通过一个uintptr类型的指针访问到,却不再有其他引用时,这个对象会被回收。换句话说,uintptr又不像是一个指针,它不算在对象的引用计数中,也不能阻止对象被回收。

    uintptr的一个用途示例:

  1. 将对象s的地址转换成Pointer类型,转换为uintptr类型
  2. 使用unsafe.Offsetof函数得到字段Address相对于对象头的偏移量,加到s的地址上,得到s.Address的地址
  3. 将s.Address转换成Pointer,再转换成*string类型
  4. 使用指针,修改s.Address的值
type Student struct {
	Name string
	Address string
	Birthday time.Time
	Age int16
	mark string
}

	s := Student{
		Name:     "aaaa",
		Address:  "oooaddr",
		Birthday: time.Now(),
		Age:      18,
		mark:     "no more info",
	}

	fmt.Printf("%+v\n", s)
	p := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.Address))
	addrPtr := (*string)(p)
	*addrPtr = "newaddr"
	fmt.Printf("%+v\n", s)

    从输出可见,对象的Address字段的值被成功修改。

{Name:aaaa Address:oooaddr Birthday:2019-10-05 16:43:33.561129 +0800 CST m=+0.000136296 Age:18 mark:no more info}
{Name:aaaa Address:newaddr Birthday:2019-10-05 16:43:33.561129 +0800 CST m=+0.000136296 Age:18 mark:no more info}

 

二、Type,得到Type的所有字段、方法

    预定义一个Student类:

type Student struct {
	Name string
	Address string	`json:"hahahaxxxxx" proto:"ememem090909"`
	Birthday time.Time
	Age int16
	mark []string
	pp *string
}

func (s Student) ToString() string {
	return fmt.Sprintf("Student:%+v", s)
}

func (s *Student) ForPointer() int64 {
	return time.Now().Unix()
}

    使用reflect.TypeOf方法,即可以获得对象的type信息。

	sentence := "instance for string pointer"
	s := Student{
		Name:     "aaaa",
		Address:  "oooaddr",
		Birthday: time.Now(),
		Age:      18,
		mark:     []string{"no more info", "that's end"},
		pp:       &sentence,
	}

	t := reflect.TypeOf(s)

Kind

    type上的Kind方法,可以知道该type是哪一“种类”的对象。Kind只是一个枚举值,但是type的很多方法都要先判断Kind。比如,Elem()方法,获得指针、切片、数、通道等的“元素”的type,如果不是这几种type,调用Elem()方法时会抛出panic。

    Kind枚举的定义如下,大体分为:所有自带的基础类型(int16,int32……)、指针、集合类型(数组、切片、映射)、通道、函数、结构……

// A Kind represents the specific kind of type that a Type represents.
// The zero Kind is not a valid kind.
type Kind uint

const (
	Invalid Kind = iota
	Bool
	Int
	Int8
	Int16
	Int32
	Int64
	Uint
	Uint8
	Uint16
	Uint32
	Uint64
	Uintptr
	Float32
	Float64
	Complex64
	Complex128
	Array
	Chan
	Func
	Interface
	Map
	Ptr
	Slice
	String
	Struct
	UnsafePointer
)

    使用的示例如下:

	k := t.Kind()
	if k == reflect.Struct {
		fmt.Println("kind of s is struct")
	}

    结果就是如期打印出了字符串。

Field

    使用NumField方法得到字段的数量,再通过Field方法得到第X个字段。当然,也有FieldByName方法,根据字段的名称得到字段。

    Student的最后两个字段分别是[]string,*string类型,因此可以使用Elem方法,获得其“元素”的类型。

    json序列化时在字段后面反引号(``)中的内容称作Tag,可以使用反射解析出来(相当于Java中字段的注解)。代码片段的最后两行演示了如何获取其中内容。

	fieldNum := t.NumField()
	for i := 0; i < fieldNum; i++ {
		field := t.Field(i)
		fmt.Printf("%+v\n", field)

		fieldKind := field.Type.Kind()
		if fieldKind == reflect.Slice || fieldKind == reflect.Ptr {
			elementType := field.Type.Elem()
			fmt.Printf("element type: %+v\n", elementType)
		}
	}

	field, _ := t.FieldByName("Address")
	tag := field.Tag
	fmt.Printf("json: %s, proto: %s\n\n", tag.Get("json"), tag.Get("proto"))

    输出:

{Name:Name PkgPath: Type:string Tag: Offset:0 Index:[0] Anonymous:false}
{Name:Address PkgPath: Type:string Tag:json:"hahahaxxxxx" proto:"ememem090909" Offset:16 Index:[1] Anonymous:false}
{Name:Birthday PkgPath: Type:time.Time Tag: Offset:32 Index:[2] Anonymous:false}
{Name:Age PkgPath: Type:int16 Tag: Offset:56 Index:[3] Anonymous:false}
{Name:mark PkgPath:main Type:[]string Tag: Offset:64 Index:[4] Anonymous:false}
element type: string
{Name:pp PkgPath:main Type:*string Tag: Offset:88 Index:[5] Anonymous:false}
element type: string
json: hahahaxxxxx, proto: ememem090909

 

Method

    Method和Field的获取方法非常类似,通过NumMethod方法获得数量,再通过Method方法或者MethodByName方法,获得方法的引用。

    Method相比Field,没有Tag,但是有参数、返回值的概念,通过NumIn、NumOut方法获得其数量,通过In、Out方法获取第X个参数/返回值的Type。

	fmt.Println("Student methods:")
	methodNum := t.NumMethod()
	for i := 0; i < methodNum; i++ {
		method := t.Method(i)
		fmt.Printf("%+v\n", method)
		mt := method.Type

		inNum := mt.NumIn()
		for a := 0; a < inNum; a++ {
			fmt.Printf("in[%d]: %+v\n", a, mt.In(a))
		}

		outNum := mt.NumOut()
		for a := 0; a < outNum; a++ {
			fmt.Printf("out[%d]: %+v\n", a, mt.Out(a))
		}
	}

    输出为:

Student methods:
{Name:ToString PkgPath: Type:func(main.Student) string Func: Index:0}
in[0]: main.Student
out[0]: string

    可见有以下两点:

  1. 这是Student类型的方法,方法定义中没有参数,但实际上是把类型的对象,当做了首个参数
  2. *Student类型的方法,不是Student类型的方法

    刚才获取的是Student类型的方法。如果在获取type的时候使用*Student呢?结果是不一样的:

*Student methods:
{Name:ForPointer PkgPath: Type:func(*main.Student) int64 Func: Index:0}, NumIn: 1, NumOut: 1
in[0]: *main.Student
out[0]: int64
{Name:ToString PkgPath: Type:func(*main.Student) string Func: Index:1}, NumIn: 1, NumOut: 1
in[0]: *main.Student
out[0]: string
  1. Student类型的方法也是*Student类型的方法,反之就不一样
  2. 即使是Student类型的方法,此时首个参数也是*Student

三、Value,给value赋值

    Type中存储的是“类型”相关的信息,例如类型、字段、方法的属性等,要通过反射操作对象的值,需要使用value。

    这是一个使用的示例。先用ValueOf得到反射的Value,拿到字段,获取这个字段的值,并用SetXXX方法修改该字段的值。

	sentence := "instance for string pointer"
	s := Student{
		Name:     "aaaa",
		Address:  "oooaddr",
		Birthday: time.Now(),
		Age:      18,
		mark:     []string{"no more info", "that's end"},
		pp:       &sentence,
	}

	v := reflect.ValueOf(&s).Elem()
	//v := reflect.ValueOf(s)
	addrValue := v.FieldByName("Address")
	fmt.Printf("Old addr: %s\n", addrValue.String())
	if addrValue.CanSet() {
		addrValue.SetString("yesyes")
		fmt.Printf("%+v\n", s)
	} else {
		fmt.Println("Cannot set value")
	}

     输出结果:

Old addr: oooaddr
{Name:aaaa Address:yesyes Birthday:2019-10-06 16:05:12.43877 +0800 CST m=+0.000134583 Age:18 mark:[no more info that's end] pp:0xc000096040}

     注意v的初始化方式:先获取其指针的value,再通过Elem得到对象本身的value。如果像被注释的一行一样,直接获取s的value,则其字段不能够被赋值。

    调用方法的示例:先用MethodByName得到方法的value,再通过Call执行这个方法。参数、返回值都是以[]reflect.Value的形式传进去的。

	stringMethod := v.MethodByName("ToString")
	result := stringMethod.Call([]reflect.Value{})
	fmt.Printf("%+v\n", result)

 

你可能感兴趣的:(golang,反射)