因收到Google相关通知,网站将会择期关闭。相关通知内容 15 运行时反射:字符串和结构体之间如何转换? 我们在开发中会接触很多字符串和结构体之间的转换,尤其是在调用 API 的时候,你需要把 API 返回的 JSON 字符串转换为 struct 结构体,便于操作。那么一个 JSON 字符串是如何转换为 struct 结构体的呢?这就需要用到反射的知识,这节课我会基于字符串和结构体之间的转换,一步步地为你揭开 Go 语言运行时反射的面纱。 反射是什么? 和 Java 语言一样,Go 语言也有运行时反射,这为我们提供了一种可以在运行时操作任意类型对象的能力。比如查看一个接口变量的具体类型、看看一个结构体有多少字段、修改某个字段的值等。 Go 语言是静态编译类语言,比如在定义一个变量的时候,已经知道了它是什么类型,那么为什么还需要反射呢?这是因为有些事情只有在运行时才知道。比如你定义了一个函数,它有一个interface{}类型的参数,这也就意味着调用者可以传递任何类型的参数给这个函数。在这种情况下,如果你想知道调用者传递的是什么类型的参数,就需要用到反射。如果你想知道一个结构体有哪些字段和方法,也需要反射。 还是以我常用的函数 fmt.Println 为例,如下所示: src/fmt/print.go func Println(a ...interface{}) (n int, err error) { return Fprintln(os.Stdout, a...) } 例子中 fmt.Println 的源代码有一个可变参数,类型为 interface{},这意味着你可以传递零个或者多个任意类型参数给它,都能被正确打印。 reflect.Value 和 reflect.Type 在 Go 语言的反射定义中,任何接口都由两部分组成:接口的具体类型,以及具体类型对应的值。比如 var i int = 3,因为 interface{} 可以表示任何类型,所以变量 i 可以转为 interface{}。你可以把变量 i 当成一个接口,那么这个变量在 Go 反射中的表示就是 。其中 Value 为变量的值,即 3,而 Type 为变量的类型,即 int。 小提示:interface{} 是空接口,可以表示任何类型,也就是说你可以把任何类型转换为空接口,它通常用于反射、类型断言,以减少重复代码,简化编程。 在 Go 反射中,标准库为我们提供了两种类型 reflect.Value 和 reflect.Type 来分别表示变量的值和类型,并且提供了两个函数 reflect.ValueOf 和 reflect.TypeOf 分别获取任意对象的 reflect.Value 和 reflect.Type。 我用下面的代码进行演示: ch15/main.go func main() { i:=3 iv:=reflect.ValueOf(i) it:=reflect.TypeOf(i) fmt.Println(iv,it)//3 int } 代码定义了一个 int 类型的变量 i,它的值为 3,然后通过 reflect.ValueOf 和 reflect.TypeOf 函数就可以获得变量 i 对应的 reflect.Value 和 reflect.Type。通过 fmt.Println 函数打印后,可以看到结果是 3 int,这也可以证明 reflect.Value 表示的是变量的值,reflect.Type 表示的是变量的类型。 reflect.Value reflect.Value 可以通过函数 reflect.ValueOf 获得,下面我将为你介绍它的结构和用法。 结构体定义 在 Go 语言中,reflect.Value 被定义为一个 struct 结构体,它的定义如下面的代码所示: type Value struct { typ *rtype ptr unsafe.Pointer flag } 我们发现 reflect.Value 结构体的字段都是私有的,也就是说,我们只能使用 reflect.Value 的方法。现在看看它有哪些常用方法,如下所示: //针对具体类型的系列方法 //以下是用于获取对应的值 Bool Bytes Complex Float Int String Uint CanSet //是否可以修改对应的值 以下是用于修改对应的值 Set SetBool SetBytes SetComplex SetFloat SetInt SetString Elem //获取指针指向的值,一般用于修改对应的值 //以下Field系列方法用于获取struct类型中的字段 Field FieldByIndex FieldByName FieldByNameFunc Interface //获取对应的原始类型 IsNil //值是否为nil IsZero //值是否是零值 Kind //获取对应的类型类别,比如Array、Slice、Map等 //获取对应的方法 Method MethodByName NumField //获取struct类型中字段的数量 NumMethod//类型上方法集的数量 Type//获取对应的reflect.Type 看着比较多,其实就三类:一类用于获取和修改对应的值;一类和 struct 类型的字段有关,用于获取对应的字段;一类和类型上的方法集有关,用于获取对应的方法。 下面我通过几个例子讲解如何使用它们。 获取原始类型 在上面的例子中,我通过 reflect.ValueOf 函数把任意类型的对象转为一个 reflect.Value,而如果想逆向转回来也可以,reflect.Value 为我们提供了 Inteface 方法,如下面的代码所示: ch15/main.go func main() { i:=3 //int to reflect.Value iv:=reflect.ValueOf(i) //reflect.Value to int i1:=iv.Interface().(int) fmt.Println(i1) } 这是 reflect.Value 和 int 类型互转,换成其他类型也可以。 修改对应的值 已经定义的变量可以通过反射在运行时修改,比如上面的示例 i=3,修改为 4,如下所示: ch15/main.go func main() { i:=3 ipv:=reflect.ValueOf(&i) ipv.Elem().SetInt(4) fmt.Println(i) } 这样就通过反射修改了一个变量。因为 reflect.ValueOf 函数返回的是一份值的拷贝,所以我们要传入变量的指针才可以。 因为传递的是一个指针,所以需要调用 Elem 方法找到这个指针指向的值,这样才能修改。 最后我们就可以使用 SetInt 方法修改值了。 要修改一个变量的值,有几个关键点:传递指针(可寻址),通过 Elem 方法获取指向的值,才可以保证值可以被修改,reflect.Value 为我们提供了 CanSet 方法判断是否可以修改该变量。 那么如何修改 struct 结构体字段的值呢?参考变量的修改方式,可总结出以下步骤: 传递一个 struct 结构体的指针,获取对应的 reflect.Value; 通过 Elem 方法获取指针指向的值; 通过 Field 方法获取要修改的字段; 通过 Set 系列方法修改成对应的值。 运行下面的代码,你会发现变量 p 中的 Name 字段已经被修改为张三了。 ch15/main.go func main() { p:=person{Name: "飞雪无情",Age: 20} ppv:=reflect.ValueOf(&p) ppv.Elem().Field(0).SetString("张三") fmt.Println(p) } type person struct { Name string Age int } 最后再来总结一下通过反射修改一个值的规则。 可被寻址,通俗地讲就是要向 reflect.ValueOf 函数传递一个指针作为参数。 如果要修改 struct 结构体字段值的话,该字段需要是可导出的,而不是私有的,也就是该字段的首字母为大写。 记得使用 Elem 方法获得指针指向的值,这样才能调用 Set 系列方法进行修改。 记住以上规则,你就可以在程序运行时通过反射修改一个变量或字段的值。 获取对应的底层类型 底层类型是什么意思呢?其实对应的主要是基础类型,比如接口、结构体、指针……因为我们可以通过 type 关键字声明很多新的类型。比如在上面的例子中,变量 p 的实际类型是 person,但是 person 对应的底层类型是 struct 这个结构体类型,而 &p 对应的则是指针类型。我们来通过下面的代码进行验证: ch15/main.go func main() { p:=person{Name: "飞雪无情",Age: 20} ppv:=reflect.ValueOf(&p) fmt.Println(ppv.Kind()) pv:=reflect.ValueOf(p) fmt.Println(pv.Kind()) } 运行以上代码,可以看到如下打印输出: ptr struct Kind 方法返回一个 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 ) 从以上源代码定义的 Kind 常量列表可以看到,已经包含了 Go 语言的所有底层类型。 reflect.Type reflect.Value 可以用于与值有关的操作中,而如果是和变量类型本身有关的操作,则最好使用 reflect.Type,比如要获取结构体对应的字段名称或方法。 要反射获取一个变量的 reflect.Type,可以通过函数 reflect.TypeOf。 接口定义 和 reflect.Value 不同,reflect.Type 是一个接口,而不是一个结构体,所以也只能使用它的方法。 以下是我列出来的 reflect.Type 接口常用的方法。从这个列表来看,大部分都和 reflect.Value 的方法功能相同。 type Type interface { Implements(u Type) bool AssignableTo(u Type) bool ConvertibleTo(u Type) bool Comparable() bool //以下这些方法和Value结构体的功能相同 Kind() Kind Method(int) Method MethodByName(string) (Method, bool) NumMethod() int Elem() Type Field(i int) StructField FieldByIndex(index []int) StructField FieldByName(name string) (StructField, bool) FieldByNameFunc(match func(string) bool) (StructField, bool) NumField() int } 其中几个特有的方法如下: Implements 方法用于判断是否实现了接口 u; AssignableTo 方法用于判断是否可以赋值给类型 u,其实就是是否可以使用 =,即赋值运算符; ConvertibleTo 方法用于判断是否可以转换成类型 u,其实就是是否可以进行类型转换; Comparable 方法用于判断该类型是否是可比较的,其实就是是否可以使用关系运算符进行比较。 我同样会通过一些示例来讲解 reflect.Type 的使用。 遍历结构体的字段和方法 我还是采用上面示例中的 person 结构体进行演示,不过需要修改一下,为它增加一个方法 String,如下所示: func (p person) String() string{ return fmt.Sprintf("Name is %s,Age is %d",p.Name,p.Age) } 新增一个 String 方法,返回对应的字符串信息,这样 person 这个 struct 结构体也实现了 fmt.Stringer 接口。 你可以通过 NumField 方法获取结构体字段的数量,然后使用 for 循环,通过 Field 方法就可以遍历结构体的字段,并打印出字段名称。同理,遍历结构体的方法也是同样的思路,代码也类似,如下所示: ch15/main.go func main() { p:=person{Name: "飞雪无情",Age: 20} pt:=reflect.TypeOf(p) //遍历person的字段 for i:=0;i