learn-tech/专栏/22讲通关Go语言-完/15运行时反射:字符串和结构体之间如何转换?.md
2024-10-15 23:13:09 +08:00

697 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到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<pt.NumField();i++{
fmt.Println("字段",pt.Field(i).Name)
}
//遍历person的方法
for i:=0;i<pt.NumMethod();i++{
fmt.Println("方法",pt.Method(i).Name)
}
}
运行这个代码可以看到如下结果
字段 Name
字段 Age
方法 String
这正好和我在结构体 person 中定义的一致说明遍历成功
小技巧你可以通过 FieldByName 方法获取指定的字段也可以通过 MethodByName 方法获取指定的方法这在需要获取某个特定的字段或者方法时非常高效而不是使用遍历
是否实现某接口
通过 reflect.Type 还可以判断是否实现了某接口我还是以 person 结构体为例判断它是否实现了接口 fmt.Stringer io.Writer如下面的代码所示
func main() {
p:=person{Name: "飞雪无情",Age: 20}
pt:=reflect.TypeOf(p)
stringerType:=reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
writerType:=reflect.TypeOf((*io.Writer)(nil)).Elem()
fmt.Println("是否实现了fmt.Stringer",pt.Implements(stringerType))
fmt.Println("是否实现了io.Writer",pt.Implements(writerType))
}
小提示尽可能通过类型断言的方式判断是否实现了某接口而不是通过反射
这个示例通过 Implements 方法来判断是否实现了 fmt.Stringer io.Writer 接口运行它你可以看到如下结果
是否实现了fmt.Stringer true
是否实现了io.Writer false
因为结构体 person 只实现了 fmt.Stringer 接口没有实现 io.Writer 接口所以和验证的结果一致
字符串和结构体互转
在字符串和结构体互转的场景中使用最多的就是 JSON struct 互转在这个小节中我会用 JSON struct 讲解 struct tag 这一功能的使用
JSON Struct 互转
Go 语言的标准库有一个 json 通过它可以把 JSON 字符串转为一个 struct 结构体也可以把一个 struct 结构体转为一个 json 字符串下面我还是以 person 这个结构体为例讲解 JSON struct 的相互转换如下面的代码所示
func main() {
p:=person{Name: "飞雪无情",Age: 20}
//struct to json
jsonB,err:=json.Marshal(p)
if err==nil {
fmt.Println(string(jsonB))
}
//json to struct
respJSON:="{\"Name\":\"李四\",\"Age\":40}"
json.Unmarshal([]byte(respJSON),&p)
fmt.Println(p)
}
这个示例是我使用 Go 语言提供的 json 标准包做的演示通过 json.Marshal 函数你可以把一个 struct 转为 JSON 字符串通过 json.Unmarshal 函数你可以把一个 JSON 字符串转为 struct
运行以上代码你会看到如下结果输出
{"Name":"飞雪无情","Age":20}
Name is 李四,Age is 40
仔细观察以上打印出的 JSON 字符串你会发现 JSON 字符串的 Key struct 结构体的字段名称一样比如示例中的 Name Age那么是否可以改变它们呢比如改成小写的 name age并且字段的名称还是大写的 Name Age当然可以要达到这个目的就需要用到 struct tag 的功能了
Struct Tag
顾名思义struct tag 是一个添加在 struct 字段上的标记使用它进行辅助可以完成一些额外的操作比如 json struct 互转在上面的示例中如果想把输出的 json 字符串的 Key 改为小写的 name age可以通过为 struct 字段添加 tag 的方式示例代码如下
type person struct {
Name string `json:"name"`
Age int `json:"age"`
}
struct 字段添加 tag 的方法很简单只需要在字段后面通过反引号把一个键值对包住即可比如以上示例中的 json:"name"。其中冒号前的 json 是一个 Key可以通过这个 Key 获取冒号后对应的 name
小提示json 作为 Key Go 语言自带的 json 包解析 JSON 的一种约定它会通过 json 这个 Key 找到对应的值用于 JSON Key
我们已经通过 struct tag 指定了可以使用 name age 作为 json Key代码就可以修改成如下所示
respJSON:="{\"name\":\"李四\",\"age\":40}"
没错JSON 字符串也可以使用小写的 name age 现在再运行这段代码你会看到如下结果
{"name":"张三","age":20}
Name is 李四,Age is 40
输出的 JSON 字符串的 Key 是小写的 name age并且小写的 name age JSON 字符串也可以转为 person 结构体
相信你已经发现struct tag 是整个 JSON struct 互转的关键这个 tag 就像是我们为 struct 字段起的别名那么 json 包是如何获得这个 tag 的呢这就需要反射了我们来看下面的代码
//遍历person字段中key为json的tag
for i:=0;i<pt.NumField();i++{
sf:=pt.Field(i)
fmt.Printf("字段%s上,json tag为%s\n",sf.Name,sf.Tag.Get("json"))
}
要想获得字段上的 tag就要先反射获得对应的字段我们可以通过 Field 方法做到该方法返回一个 StructField 结构体它有一个字段是 Tag存有字段的所有 tag示例中要获得 Key json tag所以只需要调用 sf.Tag.Get(“json”) 即可
结构体的字段可以有多个 tag用于不同的场景比如 json 转换bson 转换orm 解析等如果有多个 tag要使用空格分隔采用不同的 Key 可以获得不同的 tag如下面的代码所示
//遍历person字段中key为jsonbson的tag
for i:=0;i<pt.NumField();i++{
sf:=pt.Field(i)
fmt.Printf("字段%s上,json tag为%s\n",sf.Name,sf.Tag.Get("json"))
fmt.Printf("字段%s上,bson tag为%s\n",sf.Name,sf.Tag.Get("bson"))
}
type person struct {
Name string `json:"name" bson:"b_name"`
Age int `json:"age" bson:"b_name"`
}
运行代码你可以看到如下结果
字段Name上,key为json的tag为name
字段Name上,key为bson的tag为b_name
字段Age上,key为json的tag为age
字段Age上,key为bson的tag为b_name
可以看到通过不同的 Key使用 Get 方法就可以获得自定义的不同的 tag
实现 Struct JSON
相信你已经理解了什么是 struct tag下面我再通过一个 struct json 的例子演示它的使用
func main() {
p:=person{Name: "飞雪无情",Age: 20}
pv:=reflect.ValueOf(p)
pt:=reflect.TypeOf(p)
//自己实现的struct to json
jsonBuilder:=strings.Builder{}
jsonBuilder.WriteString("{")
num:=pt.NumField()
for i:=0;i<num;i++{
jsonTag:=pt.Field(i).Tag.Get("json") //获取json tag
jsonBuilder.WriteString("\""+jsonTag+"\"")
jsonBuilder.WriteString(":")
//获取字段的值
jsonBuilder.WriteString(fmt.Sprintf("\"%v\"",pv.Field(i)))
if i<num-1{
jsonBuilder.WriteString(",")
}
}
jsonBuilder.WriteString("}")
fmt.Println(jsonBuilder.String())//打印json字符串
}
这是一个比较简单的 struct json 示例但是已经可以很好地演示 struct 的使用在上述示例中自定义的 jsonBuilder 负责 json 字符串的拼接通过 for 循环把每一个字段拼接成 json 字符串运行以上代码你可以看到如下打印结果
{"name":"飞雪无情","age":"20"}
json 字符串的转换只是 struct tag 的一个应用场景你完全可以把 struct tag 当成结构体中字段的元数据配置使用它来做想做的任何事情比如 orm 映射xml 转换生成 swagger 文档等
反射定律
反射是计算机语言中程序检视其自身结构的一种方法它属于元编程的一种形式反射灵活强大但也存在不安全它可以绕过编译器的很多静态检查如果过多使用便会造成混乱为了帮助开发者更好地理解反射Go 语言的作者在博客上总结了反射的三大定律
任何接口值 interface{} 都可以反射出反射对象也就是 reflect.Value reflect.Type通过函数 reflect.ValueOf reflect.TypeOf 获得
反射对象也可以还原为 interface{} 变量也就是第 1 条定律的可逆性通过 reflect.Value 结构体的 Interface 方法获得
要修改反射的对象该值必须可设置也就是可寻址参考上节课修改变量的值那一节的内容理解
小提示任何类型的变量都可以转换为空接口 intferface{}所以第 1 条定律中函数 reflect.ValueOf reflect.TypeOf 的参数就是 interface{}表示可以把任何类型的变量转换为反射对象在第 2 条定律中reflect.Value 结构体的 Interface 方法返回的值也是 interface{}表示可以把反射对象还原为对应的类型变量
一旦你理解了这三大定律就可以更好地理解和使用 Go 语言反射
总结
在反射中reflect.Value 对应的是变量的值如果你需要进行和变量的值有关的操作应该优先使用 reflect.Value比如获取变量的值修改变量的值等reflect.Type 对应的是变量的类型如果你需要进行和变量的类型本身有关的操作应该优先使用 reflect.Type比如获取结构体内的字段类型拥有的方法集等
此外我要再次强调反射虽然很强大可以简化编程减少重复代码但是过度使用会让你的代码变得复杂混乱所以除非非常必要否则尽可能少地使用它们
这节课的作业是自己写代码运行通过反射调用结构体的方法
下节课我将介绍非类型安全让你既爱又恨的 unsafe”,记得来听课