Bootstrap

18 go语言(golang) - 反射(reflect)& 结构体标签

反射(reflect)

Go语言中的反射(reflection)是一种强大的工具,它允许程序在运行时检查变量的类型和值。反射是通过标准库中的reflect包提供的,主要用于处理动态类型和接口。

基本概念

在Go中,所有数据都有一个静态类型。在编译时,这个类型是已知的。通过反射,可以在运行时获取变量的动态类型和值。

Go语言的反射核心在于reflect包,它提供了TypeValue两个核心类型,分别代表了Go的类型信息和值信息。通过这两个类型,我们可以动态地获取和修改变量的类型和值。

reflect.Type

表示一个Go语言中的具体类型,通过reflect.TypeOf() 函数获取。

  • 提供了关于类型本身的信息,比如类型名称、种类(Kind,如结构体、切片、指针等)、字段信息(对于结构体)等。
  • 可以使用方法如 NumField()Field(i int) 来遍历结构体的字段。
import (
	"fmt"
	"reflect"
	"testing"
)

type MyStruct struct {
	name string
	id   int
}

func Test1(t *testing.T) {

	m1 := new(MyStruct)

	m2 := MyStruct{"小明", 1}

	r1 := reflect.TypeOf(m1)
	r2 := reflect.TypeOf(m2)

	fmt.Printf("r1 ,type: %s , kind : %s\n", r1, r1.Kind())
	fmt.Printf("r2 ,type: %s , kind : %s\n", r2, r2.Kind()) // Kind 表示底层类别,如struct, int, slice等

	// NumField() 只能用于结构体类型,而不能直接用于指向结构体的指针。
	//for i := range r1.NumField() {
	//	fmt.Printf("%v\n", r1.Field(i).Name)
	//}
	// 所以这段代码会报错:
	//	panic: reflect: NumField of non-struct type *main.MyStruct [recovered]
	//		panic: reflect: NumField of non-struct type *main.MyStruct

	/*
		解决方法:通过调用 Elem() 方法来获取所引用的元素类型。
	*/
	if r1.Kind() == reflect.Ptr {
		r1 = r1.Elem()
	}
	for i := range r1.NumField() {
		fmt.Printf("%v\n", r1.Field(i).Name)
	}

	fmt.Println("=========分割线=========")

	for i := range r2.NumField() {
		fmt.Printf("%v\n", r2.Field(i).Name)
	}

}

输出

r1 ,type: *main.MyStruct , kind : ptr
r2 ,type: main.MyStruct , kind : struct
name
id
=========分割线=========
name
id

reflect.Value

表示一个Go语言中的具体值,reflect.ValueOf() 函数获取。

  • 提供了对值的实际操作能力,可以获取或设置值,但前提是该值是可设置的。
  • 使用方法如 Interface() 可以将其转换回接口类型,或者使用具体的方法如 Int(), String(), 等来获取基础数据类型。
type MyStructNew struct {
	Name string
	Id   int
}

func Test2(t *testing.T) {

	m1 := MyStructNew{"abc", 2}
	m2 := new(MyStructNew)

	v1 := reflect.ValueOf(m1)
	v2 := reflect.ValueOf(m2)

	fmt.Printf("v1 value: %v , kind : %s\n", v1, v1.Kind())
	fmt.Printf("v2 value: %v , kind : %s\n", v2, v2.Kind())

	for i := 0; i < v1.NumField(); i++ {
		fmt.Println(v1.Field(i).Interface())
	}

	if v2.Kind() == reflect.Ptr {
		v2 = v2.Elem()
	}

	for i := 0; i < v2.NumField(); i++ {
		fmt.Println(v2.Field(i).Interface())
	}

}

输出:

v1 value: {abc 2} , kind : struct
v2 value: &{ 0} , kind : ptr
abc
2

0

注意!

字段名大小写:Go语言中的结构体字段如果要通过反射访问,必须是导出的(即首字母大写)

否则会报错:

panic: reflect.Value.Interface: cannot return value obtained from unexported field or method [recovered]
	panic: reflect.Value.Interface: cannot return value obtained from unexported field or method

修改值

如果要修改通过反射获得的数据,需要确保其是可设置(settable)的

  1. 传递指针:因为只有通过指针才能修改原始数据。
  2. 使用 Elem():当你有一个指向结构体的指针时,使用 Elem() 获取实际的结构体值以便进行操作。
  3. 检查可设置性:使用 CanSet() 方法来确认字段是否可以被设置。
/*
报错,值需要是指针类型
panic: reflect: reflect.Value.SetString using unaddressable value [recovered]

	panic: reflect: reflect.Value.SetString using unaddressable value
*/
func Test3(t *testing.T) {
	m := MyStructNew{
		Name: "小明",
		Id:   18,
	}

	fmt.Println(m)

	r := reflect.ValueOf(m)

	name := r.Field(0)
	name.SetString("小明2")

	fmt.Println(m)
}

func Test4(t *testing.T) {
	m := new(MyStructNew)
	m.Name = "小明"
	m.Id = 18

	fmt.Println(m)

	r := reflect.ValueOf(m)
	// 修改字段值,需要传递指针并使用 Elem(),否则报错
	// panic: reflect: call of reflect.Value.Field on ptr Value [recovered]
	//	panic: reflect: call of reflect.Value.Field on ptr Value
	r = r.Elem()

	name := r.Field(0)
	if name.CanSet() {
		name.SetString("小明2")
	}

	fmt.Println(m)
}

输出:

=== RUN   Test4
&{小明 18}
&{小明2 18}

动态调用函数

反射还允许我们在运行时动态调用函数。这对于需要根据输入参数动态选择和调用不同函数时非常有用。

func MyMethod(a int, b int) int {
	return a * b
}

func Test5(t *testing.T) {
	// 反射出函数
	method := reflect.ValueOf(MyMethod)

	// 参数需要也使用reflect包的类型封装
	methodArgs := []reflect.Value{reflect.ValueOf(3), reflect.ValueOf(4)}
	// 调用函数,使用Call
	var result = method.Call(methodArgs)

	// 遍历结果集
	for i := range result {
		fmt.Println(result[i].Interface())
	}
}

简单应用

假设我们需要传入一个数据库表名,在clickhouse、elasticsearch、mysql中都有同名的表,尝试通过动态调用函数来实现:

  • 统一接口设计:通过定义统一接口(DataBase),确保不同数据库系统之间具有一致性
  • 反射与动态调度:利用 Go 语言中的反射特性,可以在运行时根据条件选择并执行不同对象上的方法。
package main

import (
	"fmt"
	"reflect"
	"slices"
)

type DataBase interface {
	Query(tableName string)
}

type MysqlDataBase struct{}

type ClickhouseDataBase struct{}

type ElasticsearchDataBase struct{}

func (m *MysqlDataBase) Query(tableName string) {
	fmt.Println("查询mysql表:" + tableName)
}

func (c *ClickhouseDataBase) Query(tableName string) {
	fmt.Println("查询CK表:" + tableName)
}

func (e *ElasticsearchDataBase) Query(tableName string) {
	fmt.Println("查询ES表:" + tableName)
}

func DynamicQuery(dbType string, tableName string, databases map[string]DataBase) {
	var d DataBase = databases[dbType]

	var l = []string{"mysql", "clickhouse", "elasticsearch"}
	if !slices.Contains(l, dbType) {
		fmt.Println("不支持的数据连接类型:" + dbType)
		return
	}

	d.Query(tableName)
}

func DynamicQueryNew(dbType string, tableName string, databases map[string]DataBase) {

	if base, exist := databases[dbType]; exist {
		query, methodExist := reflect.TypeOf(base).MethodByName("Query")
		if methodExist {
			query.Func.Call([]reflect.Value{reflect.ValueOf(base), reflect.ValueOf(tableName)})
		}
	} else {
		fmt.Println("不支持的数据连接类型:" + dbType)
	}
}

func main() {
	m := map[string]DataBase{
		"mysql":         &MysqlDataBase{},
		"clickhouse":    &ClickhouseDataBase{},
		"elasticsearch": &ElasticsearchDataBase{},
	}

	DynamicQuery("mysql", "user_info", m)
	DynamicQuery("clickhouse", "user_info", m)
	DynamicQuery("elasticsearch", "user_info", m)
	DynamicQuery("redis", "user_info", m)

	DynamicQueryNew("mysql", "user_info", m)
	DynamicQueryNew("clickhouse", "user_info", m)
	DynamicQueryNew("elasticsearch", "user_info", m)
	DynamicQueryNew("redis", "user_info", m)

}

输出:

查询mysql表:user_info
查询CK表:user_info
查询ES表:user_info
不支持的数据连接类型:redis
查询mysql表:user_info
查询CK表:user_info
查询ES表:user_info
不支持的数据连接类型:redis

其中:

  1. reflect.ValueOf(base):这是方法的接收者,即Query方法所属的对象实例。在Go中,方法的接收者不是通过普通的参数传递的,而是在方法调用时隐式地作为第一个参数传递。在使用反射调用方法时,你需要显式地提供它。
  2. reflect.ValueOf(tableName):这是Query方法的参数,即要查询的表名。
  3. 直接调用而非反射:这个例子只是展示用法,不是最佳实现:如果所有数据结构都实现了相同的方法,可以直接通过接口进行调用,而不必使用反射,这样可以提高性能和代码可读性。例如代码中的DynamicQuery

高级应用场景

  1. ORM框架:许多ORM框架利用反射来自动映射数据库表与Go结构体之间的数据关系,从而减少手动编码工作量。

  2. 依赖注入:一些依赖注入框架利用反射机制,在运行时解析和提供所需依赖对象,实现松耦合设计模式。

  3. 测试工具:测试工具可能会使用反射来自动生成测试用例或验证某些条件,而不需要显式编写大量重复代码。

  4. 插件系统:通过动态加载和执行模块化代码片段,使得程序能够根据需求灵活扩展功能而无需重新编译整个应用程序包。

注意

尽管 Go 的设计初衷是避免复杂性,并鼓励开发者采用简单直接的方法解决问题,但在某些情况下,尤其是在构建通用库或框架时,合理运用好这种强大特性将极大提升项目灵活性及扩展能力。然而,由于其潜在性能开销及复杂度增加,应始终谨慎评估并确保正确实现,以免引发难以调试及维护的问题。

  1. 性能开销:由于涉及到大量元信息处理,过度使用反射可能导致性能下降,因此应谨慎评估其必要性。

  2. 安全性与错误处理:由于是在运行时进行操作,如果不小心可能会引发恐慌(panic),所以在实际应用中应做好错误检测及恢复机制设计。

  3. 复杂度增加:虽然提供了很大的灵活性,但同时也增加了代码复杂度,应尽量避免滥用而导致难以维护的问题出现。

  4. 常见用途

    • 序列化/反序列化(如JSON/XML)
    • 深拷贝实现
    • 动态代理模式

结构体标签

在Go语言中,结构体标签(Struct Tag)是结构体字段后面紧跟的一个或多个以反引号包围的字符串。标签提供了一种为结构体字段关联元数据的方式,这些元数据可以被反射(reflection)或其他工具用来影响字段的行为或提供额外的信息。

它是一个非常灵活的特性,它允许开发者在不修改代码逻辑的情况下,通过元数据来控制程序的行为。

定义

结构体标签的一般形式如下,一个字段可以有多个标签,只需用逗号分隔即可

// 这里的`key`是标签的名称,而`value`是与该标签关联的值。
FieldName Type `key:"value"`
FieldName Type `key1:"value1" key2:"value2"`

当然也可以不按要求写,但goland会提示 :Expected ':' after the key

Name string `这是个名字标签`

读取标签

要读取结构体字段的标签,可以使用reflect包中的Field方法获取StructField,然后使用Tag方法获取标签字符串。

import (
	"fmt"
	"reflect"
	"testing"
)

type MyStruct struct {
	Name string `这是个名字标签`                         // 不推荐
	id   int    `desc:"这是id标签" sort:"这个字段可以用来排序"` // 推荐使用key value的方式定义标签
}

func Test1(t *testing.T) {
	// 通过反射获取标签
	m := MyStruct{"小明", 1}

	r := reflect.TypeOf(m)

	for i := range r.NumField() {
		field := r.Field(i)
		tag := field.Tag
		fmt.Println(field.Name, "的标签->", tag)
		// 单个标签中查找某个key

		if value, ok := tag.Lookup("desc"); ok {
			fmt.Println("标签有一个desc的key,value:" + value)
		} else {
			fmt.Println("标签有没有desc的key")
		}

		fmt.Println("============分割线============")
	}

}

输出

Name 的标签-> 这是个名字标签
标签有没有desc的key
============分割线============
id 的标签-> desc:"这是id标签" sort:"这个字段可以用来排序"
标签有一个desc的key,value:这是id标签
============分割线============
;