参考文章:
https://zhuanlan.zhihu.com/p/34288219
学习golang有一段时间了,但对框架中的反射代码仍然不是很清楚,因此花时间学习了下。同Java相比,golang中多出了“指针”这一概念,在学习的时候需要稍加注意。
golang中有“指针”的概念,但是和C/C++比起来,又有少许的不同,比如有:
C语言中的指针可以做+/-操作,典型的就是指向数组的指针。golang中不允许这种操作。
golang不允许指针类型的直接转换。
此时就需要用到unsafe.Pointer和uintptr两个类。
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定义在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的一个用途示例:
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}
预定义一个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)
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")
}
结果就是如期打印出了字符串。
使用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和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
可见有以下两点:
刚才获取的是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
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)