golang 反射(reflect)

golang 反射(reflect)

反射是现代程序必备的元素,用于在 运行时 获取程序元素,如对象等的 元数据,实现动态识别类型及其结构,以及相关的语义信息。

反射在程序中应用非常多,例如:

  • 动态生成数据:json 序列化/反序列化; orm 映射, proxy 透明代理对象
  • 动态调用方法:plugin 实现
  • 框架自动处理程序:annotation tag 注解标签
  • 其他需要数据元数据的应用

在必要的场合,灵活应用反射,是中高级程序员能力的评价标准之一。灵活应用的根本是加深对 go 语言编译与实现的理解,并阅读典型应用案例。

滥用反射,也是低中级程序员最常见的问题,造成程序效率底下、不确定性错误增多。

一、Go 中的反射

go 是静态语言,表示内存中任何一个数据对象(data object)的值及其类型必须是编译期可确定的。因此,go 应用运行时不会像 java 等动态语言一样,在运行期维护所有对象的元数据,以支持多态等需要。也不像 c 语言,不提供任何元数据支持。 但注定 go 语言的反射是简单和有限的。

大神文章,必读!必读!必读!在短短的文章中,说明了 go 语言反射的要点!

  • The Laws of Reflection 其中文翻译 “反射三法则[https://blog.go-zh.org/laws-of-reflection]”

请使用 $go tool tour 验证该文中所有代码!!!

这里,仅提示其中要点:

  • 类型与接口

    • Go是静态类型的语言。每个变量都有一种静态类型
    • 有一种重要的类别就是接口类型,它是一组方法的集合
      • 向上类型转换(UpCasting)。编译期完成,如果一个数据对象或接口的方法集合包含要转换的类型的方法集合
      • 空接口 interface{} , 它是任何数据都包含的接口
    • 接口值(内部表示) - 元组(数据对象的值,数据对象的静态类型)
      • 向下类型转换(DownCasting)。 程序通过实现接口断言完成。 接口对象.(断言类型)
      • 接口内部的对总是 (值, 具体类型) 的形式,而不会是 (值, 接口类型) 的形式
  • 反射三法则

    • 1、反射是从接口值到反射对象
      • 变量/数据对象 – 反射 -> 值,具体类型
      • reflect 包中的两种类型: Type 和 Value
        • reflect.Type 是接口
        • reflect.Value 是结构
      • Type 和 Value 的函数 TypeOf(i interface{}), ValueOf(i interface{})
        • Value 相关的构造函数,Zero,NewAt,MakeSlice...
        • Type 相关的获取函数
      • Kind 方法
        • 值不能离开类型而独立存在, Value.Type() 获取类型
        • Kind 是值和类型的共有方法, Kind 是 go 基础类型的枚举
      • 重要:反射是编译将值转为接口,TypeOf,ValueOf 只是简单取出接口值的内容
        • 反射是编译期决定的扩展,go 运行期仅加载必要的元数据
        • 根据接口的表示,反射对象类型不可能是接口
      • go 反射的本质
        • 数据对象 – 编译 -> 接口(值,具体类型)– 反射 -> 值,具体类型
    • 2、从反射对象可反射出接口值
      • 值可反射回接口值
        • func (v Value) Interface() interface{}
      • 特别注意 vv.Interface() 的区别
        • v.Interface() 是反射的原始对象
        • v 是原始对象的 reflect.Value 值
    • 3、要修改反射对象,其值必须可设置
      • reflect.Value 是 原始对象值的 copy 是不可变的
      • 要修改原始对象需要传地址
        • v.Elem() 返回指针内容或接口值,它是可修改的
  • 结构体

    • 必须传地址才能修改 struct 中的字段
    • 获取结构体 Fields (仅可导出的)
      • v.NumField() 获得结构体 Field 数量
      • v.Field(i int) 获取 Field 的值
      • v.FieldByXXX(...)
    • 获取结构体字段的 tag
      • type StructField
      • StructTag 的文字表达规范:key:”value”
      • demo
  • 获取方法

package main

import "fmt"
import "reflect"

type T struct {
    A int
    B string
}

func (t *T) SetA(i int) {
    t.A = i
}

func main() {

    t := T{23, "skidoo"}
    s := reflect.ValueOf(&t).Elem()
    typeOfT := s.Type()
    for i := 0; i < s.NumField(); i++ {
        f := s.Field(i)
        fmt.Printf("%d: %s %s = %v\n", i,
        typeOfT.Field(i).Name, f.Type(), f.Interface())
    }
    typePT := reflect.TypeOf(&t)
    fmt.Printf("%d\n",typePT.NumMethod())
    for i := 0; i < typePT.NumMethod(); i++ {
        m := typePT.Method(i)
        fmt.Printf("%d: %s %v\n", m.Index,m.Name,m.Type)
    }
    s.Field(0).SetInt(77)
    s.Field(1).SetString("Sunset Strip")
    fmt.Println("t is now", t)

    //调用方法/函数
    m := typePT.Method(0)
    params := make([]reflect.Value,2) 
    params[0] = reflect.ValueOf(&t) 
    params[1] = reflect.ValueOf(5)
    m.Func.Call(params)
    fmt.Println("t is now", t)
}

参考:golang反射中函数和方法的调用

二、golang 获取包资源

程序中有许多资源,如配置文件、图片、网页等都是随着包提供。对于 windows 程序或 java 程序都有 ResourceLoad 函数读取运行程序(exe,dll,jar)中的资源。go 语言一般都源代码提供,因此资源都是直接放置在包目录下,而不打包。

go 包 为你提供了按需管理程序资源的能力。其中,go/build 子包 是管理包以及应用环境最重要的包。

var Default Context = defaultContext()

Context 包含了程序构建工作区、版本等重要信息。

go tour 的源代码,local.go 的 findRoot 函数提供查询教学资源目录的案例!

三、反射练习

设计一个简单 ORMEngin 对象,使它完成以下任务:

数据库表

CREATE TABLE `userinfo` (
    `uid` INT(10) NOT NULL AUTO_INCREMENT,
    `username` VARCHAR(64) NULL DEFAULT NULL,
    `departname` VARCHAR(64) NULL DEFAULT NULL,
    `created` DATE NULL DEFAULT NULL,
    PRIMARY KEY (`uid`)
);

1、orm 规则

我们在field对应的Tag中对Column的一些属性进行定义,例如:

// UserInfo .
type UserInfo struct {
    UID        int   `orm:"id,auto-inc,type=INT(10)"` //语义标签
    UserName   string
    DepartName string
    CreateAt   *time.Time `orm:"name=created" json:",omitempty"`
} 

在 orm 标签中,用“,”号作为属性的分割,每个属性为“key=value”。如果只有key,表示它是 Bool 属性,默认是 true。例如:id 表示这个字段是关键字。 更多字段属性可参考 Column属性定义 ,也可以用自己定义的规则和 key。

2、实现自动插入数据

用户的样例代码:

user := UserInfo{...}
affected, err := engine.Insert(user)
// INSERT INTO user (name) values (?)

要求利用反射技术,根据输入数据的类型自动生成插入 sql 语句,实现函数 Insert(o interface{})

3、实现查询结果自动映射

用户的样例代码:

pEveryOne := make([]*Userinfo, 0)
err := engine.Find(&pEveryOne)
// SELECT `col-name`,`col-name` ... FROM UserInfo

要求利用反射技术,根据输入数据的类型自动生成查询 sql 语句,并将结果集合根据数据类型自动映射到对象,并加入结果表。

提示

  1. 可以直接使用 database.sql 或使用 sqlt 。使用 sqlt 可以简化程序开发,
  2. 任务 3 的结果映射,需要注意以下内容
    • 在 rows.scan 前要生成制定类型的结构数据。可用 reflect.New(t) 函数
    • 传给 scan 的参数必须是地址,或实现 Scanner 的接口
    • 参数数组中 field 地址顺序,可以在scan 前确定,也可以在 scan 后确定

附: scan 后确定映射的参考代码,它来自如何在 golang 使用反射调用扫描可变参数函数?

package main

import (
    "fmt"
    _ "github.com/lib/pq"
    "database/sql"
)

func main() {

    db, _ := sql.Open(
        "postgres",
        "user=postgres dbname=go_testing password=pass sslmode=disable")

    rows, _ := db.Query("SELECT * FROM _user;")

    columns, _ := rows.Columns()
    count := len(columns)
    values := make([]interface{}, count)
    valuePtrs := make([]interface{}, count)

    for rows.Next() {

        for i, _ := range columns {
            valuePtrs[i] = &values[i]
        }

        rows.Scan(valuePtrs...)

        for i, col := range columns {
            var v interface{}
            val := values[i]
            b, ok := val.([]byte)

            if (ok) {
                v = string(b)
            } else {
                v = val
            }

            fmt.Println(col, v)
        }
    }
}

你可能感兴趣的:(golang)