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)。 程序通过实现接口断言完成。
接口对象.(断言类型)
- 接口内部的对总是 (值, 具体类型) 的形式,而不会是 (值, 接口类型) 的形式
- 向下类型转换(DownCasting)。 程序通过实现接口断言完成。
反射三法则
- 1、反射是从接口值到反射对象
- 变量/数据对象 – 反射 -> 值,具体类型
- reflect 包中的两种类型: Type 和 Value
- reflect.Type 是接口
- reflect.Value 是结构
- Type 和 Value 的函数
TypeOf(i interface{})
,ValueOf(i interface{})
- Value 相关的构造函数,
Zero,NewAt,MakeSlice...
- Type 相关的获取函数
- Value 相关的构造函数,
- Kind 方法
- 值不能离开类型而独立存在, Value.Type() 获取类型
- Kind 是值和类型的共有方法, Kind 是 go 基础类型的枚举
- 重要:反射是编译将值转为接口,TypeOf,ValueOf 只是简单取出接口值的内容
- 反射是编译期决定的扩展,go 运行期仅加载必要的元数据
- 根据接口的表示,反射对象类型不可能是接口
- go 反射的本质
- 数据对象 – 编译 -> 接口(值,具体类型)– 反射 -> 值,具体类型
- 2、从反射对象可反射出接口值
- 值可反射回接口值
- func (v Value) Interface() interface{}
- 特别注意
v
和v.Interface()
的区别
v.Interface()
是反射的原始对象v
是原始对象的 reflect.Value 值
- 值可反射回接口值
- 3、要修改反射对象,其值必须可设置
- reflect.Value 是 原始对象值的 copy 是不可变的
- 要修改原始对象需要传地址
v.Elem()
返回指针内容或接口值,它是可修改的
- 1、反射是从接口值到反射对象
结构体
- 必须传地址才能修改 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 获取包资源
程序中有许多资源,如配置文件、图片、网页等都是随着包提供。对于 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 语句,并将结果集合根据数据类型自动映射到对象,并加入结果表。
提示
- 可以直接使用 database.sql 或使用 sqlt 。使用 sqlt 可以简化程序开发,
- 任务 3 的结果映射,需要注意以下内容
- 在 rows.scan 前要生成制定类型的结构数据。可用
reflect.New(t)
函数 - 传给 scan 的参数必须是地址,或实现 Scanner 的接口
- 参数数组中 field 地址顺序,可以在scan 前确定,也可以在 scan 后确定
- 在 rows.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)
}
}
}