在开发中会接触很多字符串和结构体之间的转换,尤其是在调用API的时候,需要把API返回的JSON字符串转换为struct结构体,便于操作。那么一个JSON字符串是如何转换为struct结构体的呢?这就需要用到反射的知识。
反射是什么?
和Java语言一样,Go语言也有运行时反射,这为我们提供了一种可以在运行时操作任意类型对象的能力。比如查看一个接口变量的具体类型、查看结构体有多少字段,修改某个字段的值等。 Go语言是静态编译类语言,比如定义了一个变量,已经知道是什么类型,为什么还需要反射呢? 因为有些事情只能运行时才知道。比如定义了一个函数,有一个**interface{}**类型的参数,即意味着调用者可以传任意类型的参数给这个函数。在这种情况下爱,如果想知道调用者传递的是什么类型的参数,就需要用到反射。如果想知道一个结构体有哪些字段和方法,也需要用到反射。
func PrintLn(a ...interface{}) (n int, err error){}
// fmt.Println() 源码中有可变参数,类型是interface{}, 即意味着可以传零个或多个任意类型的值给它,都不能被打印。
reflect.Value和reflect.Type
在Go语言反射定义中, 任何接口都由两部分组成:接口的具体类型,以及具体类型对应的值。
比如var i int =3 ,以为interface{} 可以表示任何类型,所以变量i可以转为interface{}。可以把变量当成一个接口,那么这个变量在Go反射中的表示就是<Value, Type> 。其中Value为变量的值,Type为变量的类型。
提示: interface{}是空接口,可以表示任意类型,即可以把任何类型都转换为接口,它通常用于反射、类型断言,减少重复代码,简化编程。
在Go反射中,标准库为我们提供了两种类型 reflect.Value 和reflect.Type来分别表示变量的值和类型,并且提供了两个函数reflect.ValueOf 和reflect.Type分别获取任意对象的reflect.Value和reflect.Type。
func main(){
i := 3
iv := reflect.ValueOf(i)
it := reflect.Typeof(i)
fmt.Println(iv, it) // 3 int
}
// 定义了int类型的i,值为3。
reflect.Value
reflect.Value可以通过reflect.ValueOf函数获得。其结构体定义如下:
type Value struct {
typ *rtype
ptr unsafe.Pointer
flag
}
// 不难发现,其结构体字段都是私有的,即我们只能用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
MethodName
NumField // 获取struct类型中字段的数量
NumMethod // 类型上方法集数量
Type // 获取对应的reflect.Type
// 看着比较多,其实就分三类:一类用于获取和修改对应的值;一类和struct类型的字段有关,用于获取对应的字段;一类和类型上的方法集有关,用于获取对应的方法。
获取原始类型
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类型互转,换成其他类型也可以。
修改对应的值
func main(){
i := 3
ipv := reflect.ValueOf(&i)
ipv.Elem().SetInt(4)
fmt.Println(i)
}
// 这样就通过反射修改了一个变量。因为reflect.ValueOf函数可以返回一份值得拷贝,所以要传入变量的指针怎才可以。因为传递的值指针,所以需要调用Elem方法找到指针指向的变量,这样才能修改。
要修改一个变量的值,有几个关键点:传递指针(可寻址),通过Elem方法获取指向的值,才可以保证值被修改,reflect.Value为我们提供了CanSet 方法判断是否可以修改变量。那么如何修改一个struct结构体字段的值呢?参考变量的修改,可总结出以下步骤:
- 传递一个struct结构体的指针,获取对应的reflec.Value
- 通过Elem方法获取指针指向的值
- 通过Field方法获取要修改的字段
- 通过Set方法修改成对应的值
func main() {
p := person{name:"zhangsan", age:18}
ppv := reflect.ValueOf(&p)
ppv.Elem().Field(0).SetString("lisi")
fmt.Println(p)
}
type person struct{
Name string
Age int
}
小结:通过反射修改一个值的规则,记住这些规则, 就可以在程序运行时通过反射修改一个变量或字段的值:
1. 可被寻址,通俗的讲就是要向reflect.ValueOf函数传递一个指针作为参数
2. 如果要修改struct结构体字段值的话,该字段需要是可导出的,而不是私有的,即该字段首字母大写
3. 记得使用Elem方法获得指针指向的值,这样才能调用Set系列方法进行修改。
获取对应的底层类型
底层类型是什么意思呢?其实对应的主要是基础类型,比如接口、结构体、指针……因为我们可以通过type关键字声明很多新类型。上述示例中p person 就是一个新类型,person对应的底层类型是struct这个结构体类型,而&p对应的是指针类型。
func main(){
p := person{Name:"zhangsan", Age:18}
ppv := reflect.ValueOf(&p)
fmt.Println(vvp.Kind())
pv := reflect.ValueOf(p)
fmt.Println(pv.Kind())
}
// 输出结果
ptr
struct
// kind 方法返回一个Kind类型的值,它是一个常量,有一下可供使用的值
type Kind uint
const{
Invalid Kin = iota
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintprt
Float32
Float64
Complex64
Complex128
Array
Chan
Func
Interfact
Map
Ptr
Slcie
Struct
UnsafePointer
}
// 从源码定义的Kind常量列表看,已经包含了Go语言所有底层类型
reflect.Type
reflect.Value可以用于与值有关的操作汇总,而如果是和变量类型有关的操作,则最好是使用reflect.Type,比如要获取结构体对应的字段名称或方法。要反射获取一个变量的reflect.Type,可以通过reflect.TypeOf。和reflect.Value不同,reflect.Type是一个接口,不是一个结构体,所以也只能使用它的方法。接口定义:
type Type interface{
Implements(u Type) bool
AssignableTo(u Type) bool
CovertibleTo(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
}
其中有几个特有的方法如下:
1. Implements 方法用于判断是否实现了接口u
2. AssignableTo 方法用于判断是否可以赋值给类型u,其实就是是否可以使用=, 即赋值运算符
3. ConvertibleTo 方法用于判断是否可以转换成类型u,其实就是是否可以进行类型转换
4. Comparable 方法用于判断该类型是否可比较的,其实就是是否可以使用关系运算符进行比较
遍历结构体的字段和方法
上述示例中的person结构体,增加一个String方法:
func (p person) String() string{
return fmt.Sprintf("Name is %s, Age is %d", p.Name, p.Age)
}
// 新增一个String方法,返回对应的字符串信息,这样person结构体也实现了fmt.Stringer接口
// 通过NumField 方法获取结构体字段的数量,通过for循环进行遍历结构体字段,同理通过NumMethod方法获取结构体的方法数量
func main() {
p := person{Name:"奔跑的蜗牛", Age:18}
pt := reflect.TypeOf(p)
// 遍历person 字段
for i:=0; i< pt.NumFiled(); i++{
fmt.Println("字段", pt.Field(i).Name)
}
// 遍历person 方法
for i := 0; i < NumMethod(); i++ {
fmt.Println("方法", pt.Method(i).Name)
}
}
小技巧: 通过FieldByName方法获取指定字段,也可以通过MethodName方法获取指定的方法,这咋急需要获取某个指定字段或方法时非常高效,而不是使用遍历
是否实现某接口
通过reflect.Type 还可以判断是否实现了某接口。还是以person结构体为例,判断是否实现了接口fmt.Stringer 和io.Writer ,如下:
func main(){
p := person{Name:"奔跑的蜗牛", Age:18}
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.Impements(writerType))
}
// 输出结果
是否实现了fmt.Stringer true
是否实现了io.Writer false
提示: 尽可能通过类型断言的方式来判断是否实现了某接口,而不是通过反射。
字符串和结构体互转
在字符串和结构体互转场景中,使用最多额是JSON和struct互转。
JSON和Struct互转
Go语言的标准库有一个json包,通过它可以把JSON字符串转为一个struct结构体,也可以把一个struct结构体转为一个json字符串。
func main() {
P := person{Name:"奔跑的蜗牛", Age :18}
// struct to json
jsonA, err := json.Marshal(p)
if err != nil {
fmt.Println(string(jsonA))
}
// json to struct
respJson := "{\"Name\":\"独臂阿童木\",\"Age\":19}"
json.Unmarshal([]byte(respJson), &p)
fmt.Println(p)
}
// 输出结果
{"Name":"奔跑的蜗牛", "Age":18}
Name is 独臂阿童木,Age is 19
// 这个示例是通过Go语言提供的json标准包做的演示。通过json.Marshal函数,可以把一个struct转为JSON字符串。通过json.Unmarshal函数,可以把一个JSON字符串转为struct
Struct Tag
上述示例中,JSON字符串的Key和struct结构体的字段名称一样,那么是否可以改变他们呢? 要达到这个目的就需要用到**struct tag **的功能了。顾名思义,strcut tag 是一个添加在struct字段上的标记,使用它进行辅助,可以完成等一些额外的操作,比如json和struct互转。如果想把json字符串的key改为小写,通过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 是整个JSON和struct互转的关键,这个tag就像是为struct字段起的别名,那么json包是如何获得这个tag的呢? 这就需要反射了。
type person struct{
Name string `json:"name" bson:"b_name"`
Age int `json:"age" bson:"b_age"`
}
// 遍历person字段中key为json的tag
for i := 0; i < pt.NumField(); i++ {
sf := pt.Field(i)
fmt.Println("字段%s上,json tag 为 %s\n", sf.Name, sf.Tag.Get("json"))
}
// 获得字段上的tag,先反射获得对应的字段,通过Field方法的道。该方法返回一个StructField结构体,它有一个字段是Tag,存有字段的所有Tag。只需要调用Tag.Get()方法即可
// 遍历person字段中key为json、bson的tag
for i := 0; i < pt.NumField(); i++ {
sf := pt.Field(i)
fmt.Println("字段%s上,json tag 为 %s\n", sf.Name, sf.Tag.Get("json"))
fmt.Println("字段%s上,bson tag 为 %s\n", sf.Name, sf.Tag.Get("bson"))
}
结构体的字段可以有多个tag,用于不同的场景,比如json转换、bson转换、orm解析等。如果有多个tag,要使用空格分割。采用不同的key可以获得不同的tag。
实现Struct转JSON
我们已经理解了什么是struct tag,我们在通过一个struct转json 的示例演示如何使用
func main(){
p := person{Name:"奔跑的蜗牛", Age:18}
pv := reflect.ValueOf(p)
pt := reflect.TypeOf(p)
// 自己实现struct to json
jsonBuilder := string.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使用。
json字符串的转换只是struct tag 的一个应用场景,完全可以把struct tag当成结构体中字段的元数据配置,使用它来做想做的任何事情。
反射定律
反射是计算机语言中程序检视其自身结构的一种方法,它属于元编程的一种形式。反射灵活、强大,但也存在不安全。它可以绕过编译器的很多静态检查,如果使用过多会造成混乱。为了帮助开发者更好的理解反射,Go语言的作者总结了反射三大定律, 理解了三大定律,就可以更好的理解Go语言反射:
1. 任何接口值interface{} 都可以反射出反射对象,也就是reflect.Value 和reflect.Type,通过函数reflect.ValueOf 和 reflect.TypeOf获取
2. 反射对象也可以还原为interface{}变量,也就是第一条定律的可逆性,通过reflect.Value结构体的Interface 方法获得
3. 要修改反射的对象,该值必须可设置,即可寻址,
提示: 任何类型的变量都可以转化为空接口interface{},所以第一条定律中函数reflect.ValueOf 和reflect.TypeOf 的参数就是interface{},表示可以白任何类型的变量转换为反射对象。在第二条定律中,reflect.Value 结构体的Interface方法返回的的值也是interface{},表示可以把反射对象还原为对应的类型变量。
总结:
在反射中,reflect.Value对应的是变量的值,如果需要进行和变量的值有关的操作,应该优先是哟和哪个reflect.Value,比如获取变量的值、修改变量的值等。reflect.Type 对应的是变量的类型,如果需要进行和变量的类型有关的操作,应该优先使用reflect.Type,比如获取结构体内的字段,类型拥有的方法集等。
强调:反射虽然强大,可以简化编程,减少重复代码,但是多度使用会让代码变的复杂混乱,所以除非非常必要,所以尽可能少使用它们。