golang反射

反射


如何动态的获取某个对象的类型或值信息?通过是反射.

反射是指程序在运行期间 (Runtime) 可以访问、检测和修改它本身状态或行为的一种能力。简单理解就是动态获取对象有什么(字段),能做什么(方法)、甚至可以创建或设置对象,例如添加、修改字段、添加、调用、修改方法等.

反射的使用场景

  • 项目中需要动态获取对象的信息,类似于自动生成、自动调用等类似的场景
  • 序列化和反序列化,获取字段的tag
  • 配置文件解析的库,例如ymal,ini
  • 一些框架如ORM,会使用反射。动态获取对象的类型或值信息并操作

应该使用反射吗

  • Clear is better than clever. Reflection is never clear. 明确胜于聪明,反射不够明确
  • 反射是比较高级的概念,应谨慎使用. 使用反射很难编写清晰且可维护的代码, 尽可能避免使用,仅在必要情况时使用

总结起来就是如果有一个场景需要动态获取对象的信息,比如有什么字段或方法,做一些自动的操作,必要时可以使用反射,而一般的业务场景不太会使用到反射,也不推荐大量使用,影响性能并不利于维护

reflect包


在go中提供了reflect包用于反射操作,主要会用到reflect.TypeOfreflect.ValueOf两个函数

  • reflect.TypeOf 接收一个任意类型的对象(空接口), 反射得到该对象的类型信息,返回一个reflect.Type接口
func TypeOf(i interface{}) Type     
  • reflect.ValueOf 接收到一个任何类型的对象(空接口),反射得到该对象的值信息,返回一个reflect.Value结构体
func ValueOf(i interface{}) Value

文件上传中...

反射的常用操作

  • 获取对象的Type(类型),Kind(类别)、值信息
  • 获取、修改对象的值
  • 如果反射的对象是结构体,可以获取、添加、修改字段信息,调用方法、通过字符串方式调用方法
  • 甚至可以创建对象
  • 可以将反射类型转成interface,通过断言转换成原始列席

Type (类型) 与Kind (类别) 的概念

Type(类型)是指用type关键字定义的类型名,而Kind(类别)是指具体类型 (如int、strcut、map)等, Type与Kind有可能一样也可能不一样。Kind代表类型范围更大一点

  • 当 x = 5, 那么x的TypeintKind也为int
  • type integer int32, 那么该类型的变量的Typeinterger, Kindint32
  • 当 type Stu struct, 那么该结构体的变量的Type为Stu,Kindstruct
package main

import (
    "fmt"
    "reflect"
)

func main() {
    // 1. 变量x的Type和Kind都为int
    x := 5
    xType := reflect.TypeOf(x).Name()                      
    xKind := reflect.TypeOf(x).Kind()     
    fmt.Printf("变量x的Type为: %s, Kind为: %s\n", xType, xKind) // 变量x的Type为: int, Kind为: int

    // 2. 变量y的Type为Integer,Kind为int
    type Integer int
    var y Integer = 300
    yType := reflect.TypeOf(y).Name()
    yKind := reflect.TypeOf(y).Kind()
    fmt.Printf("变量y的Type为: %s, Kind为: %s\n", yType, yKind) // 变量y的Type为: Integer, Kind为: int

    // 3. 结构体变量stu的Type为Integer,Kind为int
    type Stu struct{}
    stu := Stu{}
    stuType := reflect.TypeOf(stu).Name()
    stuKind := reflect.TypeOf(stu).Kind()
    fmt.Printf("结构体变量stu的Type为: %s, Kind为: %s\n", stuType, stuKind) // 结构体变量stu的Type为: Stu, Kind为: struct
}

反射基本类型

package main

import (
    "fmt"
    "reflect"
)

var x int = 5

反射变量x的类型与值信息

func main() {
    // 对变量x的类型反射,得到reflect.Type类型
    xType := reflect.TypeOf(x)
    // 对变量x的值反射,得到reflect.Value类型
    xValue := reflect.ValueOf(&x)
    // 类型与值的类别(kind)
    fmt.Println(xType.Kind())  // int
    fmt.Println(xValue.Kind()) // ptr
}

转成具体类型

将反射后的变量值转换成in64,必须要要和原数据类型匹配

// 通过Int方法转换成Int64
xValue.Elem().Int()        // 10
// 查看转换后的类型为int64
fmt.Printf("%T\n", xValue.Elem().Int()) // int64

修改变量的值

通过反射将变量x的值进行修改,必须要传入指针类型的变量,因为是拷贝不能直接修改

// 修改变量x的值,必须和原先的类型一致
xValue.Elem().SetInt(100)  // 通过反射将变量x的值修改为100

// 查看是否修改成功(必须要通过指针)
fmt.Println(Value.Elem())    // 100

类型断言

将反射类型转换为具体类型,思路是先将反射类型转换为空接口,然后在通过断言转换成具体类型

// 将反射类型先转成Interface,然后在断言
data, ok := xValue.Interface().(*int)
if ok {
        ret := *data
        fmt.Printf("ret的值为: %d, 类型为: %T\n", ret, ret) // ret的值为: 100, 类型为: int
}

反射结构体

对基本类型反射意义不大,更多是反射结构体对象

package main

import (
    "fmt"
    "reflect"
)

type Stu struct {
    Name string `json:"name"`
    Age  int
    City string
}

func (s *Stu) Hello() {
    fmt.Println("hello i am", s.Name)
}

func (s *Stu) Say(str string) string {
    return fmt.Sprintf("hello %s", str)
}

func main() {
  // stu实例,是指针类型的结构体实例
    stu := &Stu{Name: "alice", Age: 28, City: "shanghai"}
}

上面创建了一个Stu结构体,以及对*Stu结构体添加了Hello、Say两个方法,并创建了stu(指针结构体)对象

反射stu对象得到类型和值信息

// 对stu反射,得到类型信息
stuType := reflect.TypeOf(stu)
// 对stu反射,得到值信息
stuVal := reflect.ValueOf(stu)
fmt.Println(stuType) // *main.Stu
fmt.Println(stuVal)  // &{alice 28 shanghai}

// 获取stu的Kind信息(类别)
stuType.Kind())                // ptr
// 获取stu的Type信息
stuType.String()             // *main.Stu

// stu的kind是指针类型吗?
fmt.Println(stuType.Kind() == reflect.Ptr) // true

获取对象字段名与tag

// 获取stu对象的方法个数
stuType.Elem().NumField() // 3

// 第0个字段的字段名
stuType.Elem().Field(0).Name // Name

// 第0个字段的类型
stuType.Elem().Field(0).Type.Name) // string

// 第0个字段的的tag
stuType.Elem().Field(0).Tag.Get("json")) // name

上述表示反射对象共有3个字段名,可以通过数字索引的方式访问,得到字段的类型、名字等。如果觉得比较麻烦可以使用遍历

// 遍历字段的的信息,例如字段名,类型, tag
for i := 0; i < stuType.Elem().NumField(); i++ {
    fmt.Printf("字段名: %s 字段类型: %T\n", stuType.Elem().Field(i).Name, stuType.Elem().Field(i).Type.Name)
}

// 输出
字段名: Name 字段类型: string
字段名: Age 字段类型: int
字段名: City 字段类型: string

获取方法信息

// 反射对象的方法个数
stuType.NumMethod() // 2

// 第1个方法的名字(函数名)
stuType.Method(1).Name) // Say

// 第一个方法的类型,也就是函数的类型
fmt.Println(stuType.Method(1).Type)  // func(*main.Stu, string) string

调用方法

调用方法只能通过reflect.Value对象,也就是通过reflect.ValueOf函数返回的对象 (这里为stuVal)

// 获取方法个数,与stuType一样
stuVal.NumMethod()  // 3
stuVal.Type()       // 可以将Value类型转成Type接口类型

调用没有的参数的方法,也就是Hello方法

// 这里调用的是第0个方法,也就是Hello方法,Call函数传的值为nil,表示没有参数
stuVal.Method(0).Call(nil)     // hello i am alice

调用有参数的方法,这里是Say方法,此时Call函数的参数必须是[]reflect.Value类型

// 先组织[]reflect.Value类型的参数,用于传给Call函数
args := []reflect.Value{reflect.ValueOf("eric")  // 后面的eric表示要给函数传的参数
// 传参并调用,返回的是[]Value类型
methodValue := stuVal.Method(1).Call(args)   // 返回的是[]Value类型                      
// 因为只有一个结果,所以取第0个Value,然后将它转成字符串(必须和该方法的返回类型一致)
result := methodValue[0].String()
fmt.Println(result)  // hello eric                        
  • Call函数的参数是切片[]reflect.Value类型,切片每个元素就是传入参数值
  • Call函数返回值类型也是切片[]reflect.Value类型,切片对应的是返回的每个值,真正的返回需要转成具体的类型

通过字符串方式调用方法

给定一个字符串方法,如果有该方法则这些这个方法,下面的例子如果Say方法则执行,没有报错

// 准备参数,mike是要传入方法的参数值
args := []reflect.Value{reflect.ValueOf("mike")}
// 通过字符串的方式获取到指定的方法
method := stuVal.MethodByName("Say")
// 得到返回值[]Value并转成String类型
ret := method.Call(args)[0].String()
fmt.Println(ret) // hello mike

上面的问题,如果方法不存在调用则会异常,所以可以按照下面的方式改进下,先判断下有没有这个方法,然后在执行

method := "Say"

// 遍历方法,匹配方法名
for i := 0; i < stuType.NumMethod(); i++ {
        if method == stuType.Method(i).Name {
            // 组装参数,mike是传入方法的参数
            args := []reflect.Value{reflect.ValueOf("mike")}
            // 将method字符串方法传给MethodByName函数
            methodValue := stuVal.MethodByName(method)
            // 将args传给Call方法然后调用,最后转成字符串
            ret := methodValue.Call(args)[0].String()
            fmt.Println(ret) // hello mike
        }
}

获取与修改字段值信息

// 字段个数
stuVal.Elem().NumField() // 3
// 第0个字段的值
stuVal.Elem().Field(0) // alice
// 第1个字段的值
stuVal.Elem().Field(1) // 28
// 第2个字段的值
stuVal.Elem().Field(2) // shanghai
// 修改第1个字段的值,必须用对应的类型方法才能设置
stuVal.Elem().Field(1).SetInt(120)
// 验证
stuVal.Elem().Field(1) // 120

觉得通过索引的方式获取字段值比较麻烦,同样可以遍历字段的值

// stuVal := reflect.ValueOf(stu)
for i := 0; i < stuVal.Elem().NumField(); i++ {
    fmt.Println(stuVal.Elem().Field(i))
}

// 输出go run main.go
alice
28
shanghai

反射使用细节


私有(小写开头)字段不能修改、私有方法名是不能反射

如果结构体对象的字段是私有的则不能修改值,如果方法名私有也不能反射得到,也不能调用

package main

import (
    "fmt"
    "reflect"
)

type Stu struct {
    Name string `json:"name"`
    age  int
    City string
}

func (s Stu) hello() {
    fmt.Println("hello i am", s.Name)
}

func (s Stu) Say() {
    fmt.Printf("hello %s\n", "word")
}

func main() {
    stu := Stu{Name: "alice", age: 30, City: "beijing"}
    stuType := reflect.TypeOf(stu)
    stuVal := reflect.ValueOf(stu)

// 1. 获取字段数
stuType.NumField() // 3

// 2. 获取方法数
stuType.NumMethod() // 1

// 3. 将age字段(私有)的值修改为30,报错
stuVal.Field(1)      // 30
stuVal.Field(1).SetInt(100)  // panic: reflect: reflect.Value.SetInt using value obtained using unexported field

// 4. 尝试调用第0个方法,也就是hello方法,保错
stuVal.Method(0).Call(nil)  // panic: reflect: reflect.Value.SetInt using value obtained using unexported field
}
  • 第2步,为什么只有1个方法?因为hello方法是小写开头,不能被反射
  • 第3步,不能修改age字段的值,因为age字段是小写开头,不能被反射
  • 第4步,不能调用hello方法,因为hello是小写开头,不能被反射

指针类型的对象需要通过Elem()函数获取

如果变量的类型是指针结构体,访问字段时必须通过Elem()函数得到指针地址

package main

import (
    "fmt"
    "reflect"
)

type Stu struct {
    Name string `json:"name"`
    age  int
    City string
}

func (s Stu) hello() {
    fmt.Println("hello i am", s.Name)
}

func (s Stu) Say() {
    fmt.Printf("hello %s\n", "word")
}

func main() {
    // 1. stu是指针结构体
    stu := &Stu{Name: "alice", age: 30, City: "beijing"}
    stuVal := reflect.ValueOf(stu)
    // 2. 访问第0个字段,必须要通过Elem()函数获取指针
    fmt.Println(stuVal.Elem().Field(0)) // alice
    // 3. 访问第0个字段,必须要通过Elem()函数获取指针
    fmt.Println(stuVal.Elem().Field(1)) // 30
    // 4. 没有通过Elem()函数获则抛出异常
    fmt.Println(stuVal.Field(1)) // panic: reflect: call of reflect.Value.Field on ptr Value
}
  • 第1步只是得到结构体指针类型
  • 第2-3步,必须通过Elem函数获取到指针才能引用字段值
  • 第4步,没有通过Elem函数获取指针则抛出异常

接口值、反射对象以及原始类型互相转换

  • 原始类型分配给接口值,然后反射为反射对象
  • 反射对象转换成接口类型的值,然后对接口值断言,转成原始数据类型

golang发射

package main

import (
    "fmt"
    "reflect"
)

type Stu struct {
    Name string `json:"name"`
    age  int
    City string
}

func (s Stu) hello() {
    fmt.Println("hello i am", s.Name)
}

func (s Stu) Say() {
    fmt.Printf("hello %s\n", "word")
}

func Ref(p interface{}) {

    // 1. 转换成反射对象
    pVal := reflect.ValueOf(p)

    // 根据kind,先将反射类型转换为interface,然后断言得到原始类型
    switch pVal.Kind() {
    case reflect.Struct:
        if pp, ok := pVal.Interface().(Stu); ok {
            fmt.Printf("pp的值为: %v, 类型为: %T\n", pp, pp)
        }
    case reflect.Int:
        if pp, ok := pVal.Interface().(int); ok {
            fmt.Printf("pp的值为: %d, 类型为: %T\n", pp, pp)
        }
    case reflect.Float64:
        if pp, ok := pVal.Interface().(float64); ok {
            fmt.Printf("pp的值为: %f, 类型为: %T\n", pp, pp)
        }
    }
}

func main() {
    // 1. stu是指针结构体
    stu := Stu{Name: "alice", age: 30, City: "beijing"}
    Ref(stu)
    // 2. float64类型
    x := 5.14
    Ref(x)
    // 3. int类型
    y := 100
    Ref(y)
}

// 输出:
pp的值为: {alice 30 beijing}, 类型为: main.Stu
pp的值为: 5.140000, 类型为: float64
pp的值为: 100, 类型为: int

反射实战


根据用户的输入进行反射

接收用户的输入参数,判断是否在结构体有该方法,有则执行

package main

import (
    "fmt"
    "os"
    "path"
    "reflect"
    "strings"
)

type Service struct{}

func (s *Service) Start() {
    fmt.Println("service start ...")
}

func (s *Service) Stop() {
    fmt.Println("service stop ...")
}

func (s *Service) Restart() {
    fmt.Println("service restart ...")
}

func (s *Service) Status() {
    fmt.Println("service status ...")
}

func main() {
    // 异常处理
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("%s [start|stop|restart|status]\n", path.Base(os.Args[0]))
        }
    }()
  // 第一个参数
    argv := os.Args[1]
    sr := &Service{}

  // 反射
    srType := reflect.TypeOf(sr)
    srVal := reflect.ValueOf(sr)

    // 遍历方法,如果有存在argv方法那么就去执行
    for i := 0; i < srType.NumMethod(); i++ {
        // 转成小写比较
        if strings.ToLower(srType.Method(i).Name) == strings.ToLower(argv) {
            srVal.MethodByName(srType.Method(i).Name).Call(nil)
            return
        }
    }
    fmt.Println("错误选项:", argv)
}

根据结构体字段生成插入sql

创建一个函数,接收任意参数(空接口类型),如果传的结构体对象,通过反射获取字段与字段值,然后生成insert语句

package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    name string
    age  int
    city string
}

type Employee struct {
    name   string
    age    int
    salary float64
}

func GenInsert(obj interface{}) {
    objVal := reflect.ValueOf(obj)
    objKind := objVal.Kind()

    if objKind == reflect.Struct {
        objtab := reflect.TypeOf(obj).Name()
        insert := fmt.Sprintf("insert into %s values(", objtab)
        for i := 0; i < objVal.NumField(); i++ {
            switch objVal.Field(i).Kind() {
            case reflect.Int:
                if i == 0 {
                    insert = fmt.Sprintf("%s%d", insert, objVal.Field(i).Int())
                } else {
                    insert = fmt.Sprintf("%s, %d", insert, objVal.Field(i).Int())
                }
            case reflect.String:
                if i == 0 {
                    insert = fmt.Sprintf("%s%s", insert, objVal.Field(i).String())
                } else {
                    insert = fmt.Sprintf("%s, %s", insert, objVal.Field(i).String())
                }
            case reflect.Float64:
                if i == 0 {
                    insert = fmt.Sprintf("%s%f", insert, objVal.Field(i).Float())
                } else {
                    insert = fmt.Sprintf("%s,%f", insert, objVal.Field(i).Float())
                }
            default:
                fmt.Println("Unsupported type")
                return
            }
        }
        insert = fmt.Sprintf("%s)", insert)
        fmt.Println(insert)
    }
    //fmt.Println("error")
}

func main() {
    e := Employee{"alice", 20, 864.56}
    o := Student{"bob", 33, "beijing"}
  // 传结构体实例e和o给函数,通过反射拿到字段的值,拼装成SQL
    GenInsert(e)   // insert into Employee values(alice, 20,864.560000)
    GenInsert(o)  // insert into Student values(bob, 33, beijing)
}
本作品采用《CC 协议》,转载必须注明作者和本文链接
关注运维,开发,devops,sre,容器云等技术
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!