学点Golang —— Reflect包

9/16/2024 GolangReflect

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构造逻辑。

具体的做法是:

  1. 在初始化时,通过一张map存储db_model结构体的字段名及对应的type;
  2. 在构造SQL时,通过反射获取结构体字段名和字段值,然后根据字段名从map中获取type,根据type对字段值进行escape和格式化;
  3. 将格式化的字段值按字段名的顺序用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
  */
}


从上述程序中可以看出性能从高到低排序如下:

  1. 直接操作对象
  2. 接口
  3. 断言 + 接口
  4. 反射 但从灵活程度上看,越往后灵活性越高,因此需要根据具体场景选择合适的方式。如果能够依赖明确的接口,能够保证在满足一定的灵活性的前提下,取得较好的性能。

# 总结

反射是Go语言中一个非常强大的特性,它可以让程序在运行时动态地访问和修改对象的属性和方法。但是,反射也会带来一些性能上的开销,因此在使用反射时需要谨慎。