前言:
Golang 反射比 C++ RTTI 要强大的多,但是比 .NET C#/VB/C++ 来说,它大约属于低阶反射支持的范畴。
但是 Golang 语言提供了相对强大的反射。
它总要比没有提供易用反射支持的要好的多,C++ 之中我们基本只能依赖模板、宏通过元编程来实现相对强大的反射机制。
Golang 反射弱的原因:
1、没有强大的动态编程,Emit 反射发出机制,
这会导致,很难实现真正意义上的 "Dynamic Proxy 动态代理框架",这常常用于AOP切面编程,但的确可以通过如生成代码,或其它的解决方案来代替。
2、没有强大的动态表达式树编程,即:System.Linq.Expressions
相对于 Emit 嵌入平台IL(IL Assembly)来说要相对更舒服一些,但其接口的缺点是,拽写动态表达式 CodeDom 树比嵌入(IL Assembly)还要晦涩难懂一些,但学好规约树,深入理解计算机编程本质的童鞋,不会存在上手太困难的问题。
但是鱼合掌不可兼得,Golang 是一门AOT静态编译的语言,的确很难令反射支持 "动态反射并构建 Emit"。
这或许没有办法,但反射支持 Emit 动态编程才算是 "高阶反射",否则都属于中低阶反射机制,这确实大家认同且不争的事实。
让我们展望一下下述例程代码:
反射:获取字段值、设置字段值
Go
type Person struct {
Name string
Age int
}
func main() {
p := Person{"小明", 20}
t := reflect.TypeOf(p)
fmt.Println("类型名:", t.Name())
// 获取值信息
v := reflect.ValueOf(p)
fmt.Println("值信息:", v)
// 修改值信息
v = reflect.ValueOf(&p).Elem()
v.FieldByName("Name").SetString("Bob")
v.FieldByName("Age").SetInt(30)
fmt.Println("修改值:", p)
}
1、它定义一个 Person 类型,其类型描述了人的名字跟年龄,我们首先构造一个 Person 对象的实例,并且通过 reflect.TypeOf(any) 函数来获取它的反射类型信息,并输出它的类型名到控制台窗口上面。
2、通过 reflect.ValueOf(p) 函数获取实例 p 的值信息,并将其打印到控制台上面。
3、通过 reflect.ValueOf(&p).Elem() 函数获取对象的所有可见成员。
注意:
Golang 反射不可以 "侵入式访问私有的成员" (即反射:侵入式编程)。
在 Golang 之中成员声明首字母不为大写字符,即:"^[A-Z]" 那么则被 Golang 编译器视为私有的内部成员。
并且通过 FieldByName 函数查找其类型可见成员字段并且设置它的值。
本人需要提一嘴,反射访问效能无论在哪个语言之中都会是一个大的损耗问题,在 C# 语言之中,不优化的情况下,反射效率性能至少相对正常访问代码差10倍以上,但深入优化之后可以减少到1倍甚至更低。
JAVA 语言反射相对 C# 语言反射效率更差,但好歹可以从字节码上面优化,C# 做到1倍甚至更低是需要动态构建函数的。(高阶反射:动态编程)
所以:一个合格的开发人员必须考虑提高反射成员的效率,那么最差的情况,你至少可以减少对于查找成员的开销。
例如:
在 Go 语言运行时库之中实现的 reflect::type.go 文件,对于 FieldByName 基础框架库函数的内部实现为:
Go
// FieldByName returns the struct field with the given name
// and a boolean to indicate if the field was found.
func (t *structType) FieldByName(name string) (f StructField, present bool) {
// Quick check for top-level name, or struct without embedded fields.
hasEmbeds := false
if name != "" {
for i := range t.Fields {
tf := &t.Fields[i]
if tf.Name.Name() == name {
return t.Field(i), true
}
if tf.Embedded() {
hasEmbeds = true
}
}
}
if !hasEmbeds {
return
}
return t.FieldByNameFunc(func(s string) bool { return s == name })
}
可见,如若不优化反射效率,在一秒钟内成千上万次的重复执行之中,无意义的效能损耗,是多么恐怖、没有意义与价值的。
在曾经 .NET 未开源的时候,人们只有依靠 ILdasm、ILSpy、dnspy++、.Net Reflector、DEIL 这些反编译的工具来分析基础框架库的代码,来判断那些可能会有潜在的问题,但这个问题不应该出现在开源的 Go 语言之中,一个专整某门高级语言的开发人员,其对语言的基础库实现一无所知是很可怕的事情。
就像早前的 C/C++ 开发人员,有多少人是呕心沥血,熬了多少个通宵,不眠夜的玩 IDA、OD、Ghidra,等这些工具,硬着头皮逆向别人程序进行反汇编来学习别人的高级玩法及程式实现的,又有多少的前辈是在技术研究的汪洋大海之中烟消玉殒的,比如国内最早研究 ffmpeg 及流媒体的大神,大家知道是那位先行者,唯有惋惜。
反射调用函数有哪些形式?
1、调用外部函数
Go
func Add(a, b int) int {
return a + b
}
func main() {
// 获取函数信息
funcValue := reflect.ValueOf(Add)
// 创建参数列表
args := []reflect.Value{
reflect.ValueOf(10),
reflect.ValueOf(20),
}
// 调用函数
result := funcValue.Call(args)
// 获取返回值
sum := result[0].Int()
fmt.Println("返回值:", sum)
}
2、调用成员函数
Go
type Person struct {
Name string
Age int
}
func (p *Person) SayHello() int {
fmt.Printf("你好, 我的名字是 %s,当前年龄已经 %d 岁了.\n", p.Name, p.Age)
return 1
}
func main() {
p := &Person{
Name: "小明",
Age: 25,
}
// 获取方法信息
methodValue := reflect.ValueOf(p).MethodByName("SayHello")
// 调用方法
result := methodValue.Call(nil)
sum := result[0].Int()
fmt.Println("返回值:", sum)
}
如何通过反射函数的返回值类型?
Go
func Add(a, b int) (int64, error) {
return int64(a) + int64(b), nil
}
func main() {
// 获取函数信息
funcValue := reflect.ValueOf(Add)
funcType := funcValue.Type()
// 获取返回值数量
numOut := funcType.NumOut()
// 遍历返回值类型
for i := 0; i < numOut; i++ {
returnType := funcType.Out(i)
fmt.Println("返回值类型:", returnType)
}
}
如果通过反射实例化一个类的对象实例?
Go
type Person struct {
Name string
Age int
}
func main() {
// 获取类型信息
personType := reflect.TypeOf(Person{})
// 创建对象
personPtrValue := reflect.New(personType)
personValue := personPtrValue.Elem()
// 设置属性值
nameField := personValue.FieldByName("Name")
nameField.SetString("小名")
ageField := personValue.FieldByName("Age")
ageField.SetInt(25)
// 转换为接口类型
person := personValue.Interface().(Person)
fmt.Println(person)
}
大家似乎发现了一些问题,为什么不能像:
1、C/C++ 通过XXX来获取类型信息?
1.1、typeid(T)
1.2、Type ^t = Int32::typeid;
2、C# 通过 typeof(T) 来获取类型信息。
而是必须先构建一个类型实例,在通过实例去获取类型的信息,就像人们在 JAVA 之中调用对象实例的 .getClass() 函数,或者C#语言之中调用对象的 .GetType() 函数来获取其类型信息。
这无疑是浪费了一定的内存,只要人们去构建对象就需要分配这个对象持有的内存资源,明明应当提供直接获取类型信息的方法才对,对此我的确表示;不太理解。
即便是JAVA那类让我感到些许讨厌的语言也提供 T.class 的语法来获取T的类型信息,但在Go语言之中是暂不可行的。
如果通过反射类字段上面的 Tag(标记),其实标记这个概念,真的令我理解的有些稍许蛋疼,其实更习惯理解为在 C++、C# 语言之中,名为 "Attribute 特性" 的概念。
例如:
1.1、GUN C/C++
设置变量内存对齐
cpp
int tmp __attribute__ ((aligned (16))) = 0;
1.2、VC++
cpp
typedef __declspec(align(8)) struct {
int age;
};
2、C#
cs
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool MoveNext() {
return false;
}
在 Golang 语言之中,我们可以通过以下方法来反射并且打印类型字段上面被 Tag 标记的值。
Go
type Person struct {
Name string `json:"name" db:"名字"`
Age int `json:"age" db:"年龄"`
}
func main() {
p := Person{
Name: "小明",
Age: 25,
}
// 获取类型信息
personType := reflect.TypeOf(p)
// 遍历字段
for i := 0; i < personType.NumField(); i++ {
field := personType.Field(i)
tag := field.Tag
// 获取标签信息
jsonTag := tag.Get("json")
dbTag := tag.Get("db")
fmt.Printf("字段名: %s, JSON标记: %s, DB标记: %s\n", field.Name, jsonTag, dbTag)
}
}
这大约就是 Golang 提供的反射系统机制,可以为开发人员提供的支援,仔细思虑,以 Golang 语言本身的反射机制,大约可以用于设计并实现那些 "FCL/基础框架" 场景呢?
1、ORM(数据对象映射框架)
2、DI(依赖注入框架)
3、AOP(面向切面编程框架,分离日志、安全审计等)
但需要面向接口编程,人们可以手动实现 Redirector 重定向代理
4、基于反射运行时的序列化(如:XML、JSON、二进制)
5、IoC(控制反转框架)
6、BDD 行为驱动测试及 TDD 测试驱动开发
7、Web 应用服务器路由框架(如:MVC)
更多的情况恕我孤陋寡闻, 人们可以查漏补缺,但反射在实际解决方案之中的大多数用途,通用被应用于这几个方面。