学点Golang —— Reflect包
Reflect反射包,可用于更加灵活地处理不同类型的数据。在此简单介绍,并梳理一些常见的case。
# Reflect的基本概念
反射也称自省,是语言提供的一种运行时机制,可以在运行时动态检查类型、生成变量、调用函数等。 基于反射,我们能够动态地在运行中更加灵活地操作变量,但由于缺少静态检查,在实际的使用中更加需要注意安全性,防止在运行时出现panic,如果对panic的防御不当,更可能导致整个程序的退出,如果程序是服务性质,会对服务的可用性造成极大的影响。 Golang的反射机制由reflect包提供,主要包含以下两个核心概念:
- Type:表示类型,TypeOf函数可以获取变量的类型,返回reflect.Type类型。
- Value:表示值,ValueOf函数可以获取变量的值,返回reflect.Value类型。
# Reflect的基本使用
# TypeOf
TypeOf函数可以获取变量的类型,返回reflect.Type类型。示例如下:
package main
import (
"fmt"
"reflect"
)
func main() {
var a int = 10
var b string = "hello"
var c bool = true
var d []int = []int{1, 2, 3}
var e map[string]int = map[string]int{"a": 1, "b": 2}
var f func(int, string) bool = func(a int, b string) bool {
return a > 0 && b != ""
}
var g interface{} = 10
fmt.Println(reflect.TypeOf(a)) // int
fmt.Println(reflect.TypeOf(b)) // string
fmt.Println(reflect.TypeOf(c)) // bool
fmt.Println(reflect.TypeOf(d)) // []int
fmt.Println(reflect.TypeOf(e)) // map[string]int
fmt.Println(reflect.TypeOf(f)) // func(int, string) bool
fmt.Println(reflect.TypeOf(g)) // int
}
# ValueOf
ValueOf函数可以获取变量的值,返回reflect.Value类型。示例如下:
package main
import (
"fmt"
"reflect"
)
func main() {
var a int = 10
var b string = "hello"
var c bool = true
var d []int = []int{1, 2, 3}
var e map[string]int = map[string]int{"a": 1, "b": 2}
var f func(int, string) bool = func(a int, b string) bool {
return a > 0 && b != ""
}
var g interface{} = 10
fmt.Println(reflect.ValueOf(a)) // 10
fmt.Println(reflect.ValueOf(b)) // hello
fmt.Println(reflect.ValueOf(c)) // true
fmt.Println(reflect.ValueOf(d)) // [1 2 3]
fmt.Println(reflect.ValueOf(e)) // map[a:1 b:2]
fmt.Println(reflect.ValueOf(f)) // 0x10a9e20
fmt.Println(reflect.ValueOf(g)) // 10
}
# Type和Value的转换
Type和Value之间可以进行相互转换,示例如下:
package main
import (
"fmt"
"reflect"
)
func main() {
var a int = 10
var b string = "hello"
var c bool = true
var d []int = []int{1, 2, 3}
var e map[string]int = map[string]int{"a": 1, "b": 2}
var f func(int, string) bool = func(a int, b string) bool {
return a > 0 && b != ""
}
var g interface{} = 10
fmt.Println(reflect.TypeOf(a)) // int
fmt.Println(reflect.ValueOf(a)) // 10
fmt.Println(reflect.ValueOf(a).Type()) // int
fmt.Println(reflect.ValueOf(a).Int()) // 10
fmt.Println(reflect.TypeOf(b)) // string
fmt.Println(reflect.ValueOf(b)) // hello
fmt.Println(reflect.ValueOf(b).Type()) // string
fmt.Println(reflect.TypeOf(c)) // bool
fmt.Println(reflect.ValueOf(c)) // true
fmt.Println(reflect.ValueOf(c).Type()) // bool
fmt.Println(reflect.TypeOf(d)) // []int
fmt.Println(reflect.ValueOf(d)) // [1 2 3]
fmt.Println(reflect.ValueOf(d).Type()) // []int
fmt.Println(reflect.TypeOf(e)) // map[string]int
fmt.Println(reflect.ValueOf(e)) // map[a:1 b:2]
fmt.Println(reflect.ValueOf(e).Type()) // map[string]int
fmt.Println(reflect.TypeOf(f)) // func(int, string) bool
fmt.Println(reflect.ValueOf(f)) // 0x10a9e20
fmt.Println(reflect.ValueOf(f).Type()) // func(int, string) bool
fmt.Println(reflect.TypeOf(g)) // int
fmt.Println(reflect.ValueOf(g)) // 10
fmt.Println(reflect.ValueOf(g).Type()) // int
}
# Reflect的常见使用场景
# 动态调用函数
通过反射,我们可以动态地调用函数,示例如下:
package main
import (
"fmt"
"reflect"
)
func add(a, b int) int {
return a + b
}
func main() {
f := reflect.ValueOf(add)
args := []reflect.Value{reflect.ValueOf(1), reflect.ValueOf(2)}
result := f.Call(args)
fmt.Println(result[0].Int()) // 3
}
# 动态访问结构体字段
通过反射,我们可以动态地访问结构体字段,示例如下:
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "Tom", Age: 20}
v := reflect.ValueOf(p)
fmt.Println(v.FieldByName("Name").String()) // Tom
fmt.Println(v.FieldByName("Age").Int()) // 20
}
# 动态修改结构体字段
通过反射,我们可以动态地修改结构体字段,示例如下:
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "Tom", Age: 20}
v := reflect.ValueOf(&p).Elem()
v.FieldByName("Name").SetString("Jerry")
v.FieldByName("Age").SetInt(30)
fmt.Println(p) // {Jerry 30}
}
# 动态调用方法
通过反射,我们可以动态地调用结构体方法,示例如下:
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func (p Person) SayHello() {
fmt.Println("Hello, my name is", p.Name)
}
func main() {
p := Person{Name: "Tom", Age: 20}
v := reflect.ValueOf(p)
m := v.MethodByName("SayHello")
m.Call(nil)
}
# 一些个人的例子
# schema下的SQL构造
在早期的一些gorm版本中,构造SQL的时候,如果对一张较宽的表插入多条记录,gorm自身的日志性能会非常差,于是我们选择了自行构造SQL。在最开始其他人的实现中,字段名和字段值都是硬编码的,这样如果数据schema发生变更,这里修改起来不仅麻烦,而且容易出错,带来很高的线上风险。于是针对这种情况,我们使用了反射来动态获取字段名和字段值,这样在数据schema变更时,只需要通过生成工具修改数据结构体即可,不需要修改SQL构造逻辑。
具体的做法是:
- 在初始化时,通过一张map存储db_model结构体的字段名及对应的type;
- 在构造SQL时,通过反射获取结构体字段名和字段值,然后根据字段名从map中获取type,根据type对字段值进行escape和格式化;
- 将格式化的字段值按字段名的顺序用join拼接成单条记录,再将所有记录用join拼接成完整的SQL;
# 将结构体转换为map
# 反射的性能
相较于直接操作对象,反射的性能开销较大,但在可接受的范围内。对于同一个变量的操作,通过接口、反射、断言、直接操作对象的性能各不相同,为了进行对比,我们可以通过如下代码进行测试:
// reflect_test.go
package reflect_test
import (
"reflect"
"testing"
)
type Person struct {
Name string
Age int
}
type IName interface {
GetName() string
}
func (p Person) GetName() string {
return p.Name
}
type IName interface {
Name() string
}
func (p Person) Name() string {
return p.Name
}
func BenchmarkDirect(b *testing.B) {
p := Person{Name: "Tom", Age: 20}
for i := 0; i < b.N; i++ {
_ = p.Name
}
/*
goos: linux
goarch: amd64
cpu: AMD Ryzen 5 3600 6-Core Processor
BenchmarkDirect-12 1000000000 0.2619 ns/op 0 B/op 0 allocs/op
PASS
ok _/home/kel/go-project/go-test 0.296s
*/
}
func BenchmarkInterface(b *testing.B) {
p := Person{Name: "Tom", Age: 20}
inf := IName(p)
for i := 0; i < b.N; i++ {
if inf, ok := inf.(IName); ok {
_ = inf.GetName()
}
}
/*
goos: linux
goarch: amd64
cpu: AMD Ryzen 5 3600 6-Core Processor
BenchmarkInterface-12 757915819 1.564 ns/op 0 B/op 0 allocs/op
PASS
*/
}
func BenchmarkInterfaceAssert(b *testing.B) {
p := Person{Name: "Tom", Age: 20}
inf := interface{}(p)
for i := 0; i < b.N; i++ {
if inf, ok := inf.(IName); ok {
_ = inf.GetName()
}
}
/*
goos: linux
goarch: amd64
cpu: AMD Ryzen 5 3600 6-Core Processor
BenchmarkInterface-12 172292500 7.056 ns/op 0 B/op 0 allocs/op
PASS
ok _/home/kel/go-project/go-test 1.925s
*/
}
func BenchmarkReflect(b *testing.B) {
p := Person{Name: "Tom", Age: 20}
inf := interface{}(p)
for i := 0; i < b.N; i++ {
_ = reflect.ValueOf(inf).FieldByName("Name").String()
}
/*
goos: linux
goarch: amd64
cpu: AMD Ryzen 5 3600 6-Core Processor
BenchmarkReflect-12 13564076 83.67 ns/op 8 B/op 1 allocs/op
PASS
ok _/home/kel/go-project/go-test 1.231s
*/
}
从上述程序中可以看出性能从高到低排序如下:
- 直接操作对象
- 接口
- 断言 + 接口
- 反射 但从灵活程度上看,越往后灵活性越高,因此需要根据具体场景选择合适的方式。如果能够依赖明确的接口,能够保证在满足一定的灵活性的前提下,取得较好的性能。
# 总结
反射是Go语言中一个非常强大的特性,它可以让程序在运行时动态地访问和修改对象的属性和方法。但是,反射也会带来一些性能上的开销,因此在使用反射时需要谨慎。