Golang reflect使用指南
Go提供了各种变量、切片、结构体等等特性,我们可以非常方便的定义与使用它们。例如,当你想定义一个结构体的类型,只需要简单地定义:
type A struct {
Name string
}
然而,当需要处理处理动态数据结构时,我们无法在编译阶段就知道未知数据的结构,其中一个非常经典的使用情景就是对Json串的Marshal。此时,就该reflect
包出场了,它提供了在运行时创建、更新某种类型以及获取该类型的各种信息的能力,有了它,我们不仅能有效处理动态数据类型,还可以大大提高代码的复用性、可读性。
Type
在reflect包中,是用Type
来描述Go中某个对象的类型,并提供了一系列方法,来获取类型的相关信息,一般通过调用TypeOf
来获取一个任意变量的类型Type
。
例如,Name()
返回的就是该类型的具体名称,String()
返回类型的字符串表示。
值得注意的是Kind()
方法,它返回的是该类型的类别,这似乎有点拗口,但其实十分好理解,举个例子,type A struct{}
,它的类型是A而类别是struct。通常,在开始阶段,我们会先判断传入的interface的类别,从而避免panic。因为有些方法只适用于某种类别,随意使用的话代码很容易panic,例如NumField()
方法,只能用以获取Kind为结构体的字段数量。
还有一个方法Elem()
,返回Type的子元素的Type。举个例子,若Type为指针,那么Elem()
返回指针所指向的Type,若为切片,则Elem()
返回切片元素的类型Type。例如*[]int
,它的Elem()
方法返回[]int
的Type。而[]int
的Elem()
方法返回int
的Type。
import (
"fmt"
"reflect"
)
type A []int
func printInfo(t reflect.Type) {
fmt.Printf("Kind = %s\tName = %s\n", t.Kind(), t.Name())
}
func main() {
a := &A{}
printInfo(reflect.TypeOf(a))
printInfo(reflect.TypeOf(a).Elem())
printInfo(reflect.TypeOf(a).Elem().Elem())
}
输出如下:
Kind = ptr Name =
Kind = slice Name = A
Kind = int Name = int
Value
而Value
描述了在Go运行时某个对象的值,我们可以针对它进行增删改查之类的操作,一般通过ValueOf
方法来获取对象的Value
。
通常情况下,我们可以通过Set()
方法来修改变量的值。例如下述代码
var a = 1
val := reflect.ValueOf(&a)
val.Elem().Set(reflect.ValueOf(2))
fmt.Printf("a = %d", a)
输出:
a = 2
可以看到,变量a的值由1被修改为2了。
使用举例
动态初始化结构体
实际工作中,struct
通常用来表示某种数据结构(或对象),是十分简洁易懂的。然而,缺点也很明显,即其表达能力很有限,比如,你想指定某个字段的默认值,你不得不在构造函数中手动指定。这种方式虽然可行,但是不够优雅,可读性也很差。
type DS struct {
FieldOne string
}
func NewDS() *DS {
return &DS{
FieldOne: "something",
}
}
那么该如何优化呢?很简单,即利用字段的tag信息。例如,下述代码,我在tag中设置了默认值。
type DS struct {
FieldOne string `default:"something"`
}
然后,我使用一个初始化函数initStruct()
来读取tag并设置字段默认值。
func NewDS() *DS {
ds := &DS{}
initStruct(ds)
fmt.Printf("FieldOne = %s", ds.FieldOne)
return ds
}
func initStruct(v interface{}) error {
e := reflect.Indirect(reflect.ValueOf(v))
if e.Kind() != reflect.Struct {
return errors.New("v must be struct")
}
et, ev := e.Type(), e
for i := 0; i < et.NumField(); i++ {
field, val := et.Field(i), ev.Field(i)
defaultValue, ok := field.Tag.Lookup("default")
if !ok {
continue
}
switch field.Type.Kind() {
case reflect.String:
val.SetString(defaultValue)
case reflect.Int:
if x, err := strconv.ParseInt(defaultValue, 10, 64); err != nil {
val.SetInt(x)
}
// 针对不同Kind,将defaultValue转换为对应类型并赋值
...
}
}
return nil
}
至此,我们就可以既方便又优雅地给结构体设置默认值了,当然,你还可以在tag中设置其他动态属性来动态更改结构体。
动态创建Map
通常情况下,我们是通过make
来创建一个map,而有了reflect
包后,我们也可以通过reflet
包来动态地创建一个map。
这里,我们有个需求,需要将一个代表长方形的结构体转换为一个map,并且存在额外要求,例如浮点字段只保留两位小数且转换为字符串。
首先,定义一个名为Rectangle
的结构体来代表一个长方形
type Rectangle struct {
Name string
Unit string
Length float64
Width float64
}
然后,使用一个convert
函数,将其转换为map。
func convert(rectangle *Rectangle) (res map[string]string, err error) {
e := reflect.Indirect(reflect.ValueOf(rectangle))
if e.Kind() != reflect.Struct {
return nil, errors.New("v must be struct")
}
et, ev := e.Type(), e
var mapStringType = reflect.TypeOf(make(map[string]string))
mapReflect := reflect.MakeMap(mapStringType)
for i := 0; i < et.NumField(); i++ {
field, val := et.Field(i), ev.Field(i)
switch field.Type.Kind() {
case reflect.String:
mapReflect.SetMapIndex(reflect.ValueOf(field.Name), reflect.ValueOf(val.String()))
case reflect.Float64:
s := strconv.FormatFloat(val.Float(), 'f', 2, 64)
mapReflect.SetMapIndex(reflect.ValueOf(field.Name), reflect.ValueOf(s))
// other cases
...
}
}
return mapReflect.Interface().(map[string]string), nil
}
最后,我们可以打印出转换后的map。
func main() {
res, _ := convert(&Rectangle{
Name: "rec-1",
Unit: "cm",
Length: 12.121764,
Width: 5.989681,
})
fmt.Printf("res = %+v", res)
}
输出:
res = map[Length:12.12 Name:rec-1 Unit:cm Width:5.99]
总结
至此,对于reflect
的简单介绍已完毕,相信你已经有了一个大概的认知了。是不是觉得这个包很强大,想跃跃欲试呢?但是,在此之前,还是要提醒你要铭记以下注意点。
reflect
大多只能适用于动态数据类型的场景,且较为危险,因此能使用原生类型尽量使用原生类型。- 书写要小心,错误使用reflect很容易panic,你需要确保你的类型使用了正确的相关方法,并提前返回错误。
- 编程界没有银弹,因此
reflect
也不是万能,例如你无法动态创建结构体的方法。
本人才疏学浅,文章难免有些不足之处,非常欢迎大家评论指出。