【Go反射定律,运行时反射】

​ 在开发中会接触很多字符串和结构体之间的转换,尤其是在调用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结构体字段的值呢?参考变量的修改,可总结出以下步骤:

  1. 传递一个struct结构体的指针,获取对应的reflec.Value
  2. 通过Elem方法获取指针指向的值
  3. 通过Field方法获取要修改的字段
  4. 通过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,比如获取结构体内的字段,类型拥有的方法集等。

强调:反射虽然强大,可以简化编程,减少重复代码,但是多度使用会让代码变的复杂混乱,所以除非非常必要,所以尽可能少使用它们。


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值