Thriftgo 反射使用文档

简介

Thriftgo 在 v0.3.0 版本提供了 Thrift IDL 反射功能,可以在运行时获取 Thrift IDL 的相关信息与描述符,并支持通过描述符来找到对应的 Go Type 并进行反射操作。我们可以在运行时使用这些反射查询接口,获取到 IDL 信息,以便进行各种查询和操作,以及更轻松的使用反射。

快速开始

例如有这样一个 IDL

// sample.thrift
namespace go demo

struct A{
    // hello
    1:required string hello
}

struct B{
    // hello
    1:required string hello
}

enum Gender{
    MALE
    FEMALE
}

service MyService{
    B MyMethod(1:required A req)
}

以 Kitex Tool 为例,在代码生成时,额外添加 -thrift with_reflection 参数,执行如下命令

kitex -module xx -thrift with_reflection demo.thrift

生成代码的目录如下:

kitex_gen/
└── demo
    ├── demo-reflection.go
    ├── demo.go
    ├── k-consts.go
    ├── k-demo.go
    └── myservice
        ├── client.go
        ├── invoker.go
        ├── myservice.go
        └── server.go

相比原有场景,会多出 demo-reflection.go 文件。这个文件包含了 demo.thrift 的 IDL 信息记录以及反射相关接口。

// demo-reflection.go
// IDL Name: demo
// IDL Path: demo.thrift

// 记录 Descriptor 与 Go Type 的对应关系
var file_demo_thrift_go_types = []interface{}{
   (*A)(nil),      // Struct 0: demo.A
   (*B)(nil),      // Struct 1: demo.B
   (*Gender)(nil), // Enum 0: demo.Gender
}
var file_demo_thrift *thrift_reflection.FileDescriptor
var file_idl_demo_rawDesc = []byte{0x1f, 0x8b, 0x8, 0x0, 0x0, ......}
}

// 初始化反射信息,并全局注册这个 IDL 的元信息
func init() {
   if file_demo_thrift != nil {
      return
   }
   type x struct{}
   builder := &thrift_reflection.FileDescriptorBuilder{
      Bytes:         file_idl_demo_rawDesc,
      GoTypes:       file_demo_thrift_go_types,
      GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
   }
   file_demo_thrift = thrift_reflection.BuildFileDescriptor(builder)
}

// 生成该 IDL 对应的 File Descriptor 的获取方法
func GetFileDescriptorForDemo() *thrift_reflection.FileDescriptor {
   return file_demo_thrift
}
// 为结构体生成 Descriptor 的获取方法
func (p *A) GetDescriptor() *thrift_reflection.StructDescriptor {
   return file_demo_thrift.GetStructDescriptor("A")
}

//.....

当在 Golang 代码里引入这个包的时候,该 IDL 的元信息就会被初始化并加载记录,在后续的代码中就可以开始使用 Thrift 反射的 API 了。

获取 Descriptor 与 IDL 元信息

我们可以通过 Descriptor 来获取 IDL 里对应的信息。例如通过 StructDescriptor,来获取 IDL 在结构体定义时的信息,以及拿到字段的注释和注解等等:

func main() {
   a := demo.NewA()
   // get struct descriptor from golang struct generated by thriftgo
   desc := a.GetDescriptor()
   fmt.Println(desc.Name)
   fmt.Println(desc.Annotations)

   // get field descriptor from struct descriptor
   helloDesc := desc.GetFieldByName("hello")
   fmt.Println(helloDesc.GetID())
   fmt.Println(helloDesc.GetRequiredness())
   
   // get type descriptor from field descriptor    
   typeDesc := helloDesc.GetType()
   fmt.Println(typeDesc.GetName())
   fmt.Println(typeDesc.IsStruct())
   
   // get annotation and check annotation
   annotations := desc.getAnnotations()
   test.Assert(annotations["key"]=="value1")
}

具体来说,Descriptor 的种类以及他们之间的关系如下:

这些 Descriptor 也是用 Thrift IDL 定义的:https://github.com/cloudwego/thriftgo/blob/main/descriptor.thrift

除了在代码生成里提供 Golang 结构体找到 Descriptor 的方法以外,thrift_reflection 包内也提供了按照 Golang 类型或者名称查找 Descriptor 的方式:

func main() {
   a := demo.NewA()
   desc := a.GetDescriptor()

   // 通过传入一个 golang 对象来找到其类型对应的 Descriptor
   desc = thrift_reflection.GetStructDescriptorByGoType((*demo.A)(nil))
   desc = thrift_reflection.GetStructDescriptorByGoType(a)
 
   // 通过 struct name + filepath 来搜索结构体,如果 filepath 为空,则为模糊匹配第一个 struct name 相同的对象
   desc = thrift_reflection.LookupStruct("A", "demo.thrift")
 
   // 通过 type descriptor 来找到对应的 struct descriptor
   typeDesc := desc.GetFieldByName("xxx").GetType()
   if typeDesc.IsStruct(){
       typeDesc.GetStructDescriptor() 
   }
}

同时也附带了一些常用的工具函数来进行 Descriptor 的查找:

// 查询某结构体中直接和间接依赖的所有 Struct Descriptor
allStructDescs, err := LookupIncludedStructsFromStruct(structDesc)
// 查询某方法中直接和间接依赖的所有 Struct Descriptor
allStructDescs, err := LookupIncludedStructsFromMethod(methodDesc)
// 查询某 Type Descriptor 中直接和间接依赖的所有 Struct Descriptor
allStructDescs, err := LookupIncludedStructsFromType(typeDesc)

获取 GoTypes 与反射操作

Descriptor 也能和 Golang 反射类型配合使用,Descriptor 提供了 GetGoType 方法,用来获取对应的 Golang 类型:

func main() {
   person := &simple.Person{
      Name: "CloudWeGo Guy",
      Id: &simple.IDCard{
         Number: "123",
         Age:    23,
      },
   }
   // get struct descriptor
   pd := person.Descriptor()
   // get go type from struct descriptor
   fmt.Println(pd.GetGoType())
   // get field descriptor
   fd := pd.GetFieldByName("id")
   // reflection
   idCardGoType, err := fd.GetGoType()
   if err != nil {
      idCardInstance := reflect.New(idCardGoType).Elem()
      fmt.Println(idCardInstance)
   }
}

类似的,除了 StructDescriptor,其他 Descriptor 也都有对应的 GoType 获取方法

要注意的是,Descriptor 查询到的信息是来自 IDL 的,名称和 Go 代码的结构体不一定完全一致,因为下划线转驼峰,或者重名避免策略,实际的 IDL 命名和 Golang 代码名称会有出入。例如某结构体的 Field 名叫 my_name,通过 Descriptor 拿到的 Name 为 my_name,但实际上 Golang 结构体里,这个 Field 是 MyName (下划线转驼峰命名)

Descriptor 也可以配合 Golang 反射 API 使用。虽然无法通过 GetFieldByName 来直接查找,但 Descriptor 里的 Field 的顺序是和 Golang 反射里的一致的,可以像下面这段代码一样通过 Descriptor 查找反射字段

goType := structDescriptor.GetGoType()
structEntity := reflect.New(goType).Elem()
for idx, fd := range des.GetFields() {
   reflectField := structEntity.Field(idx)
   // xxxx
}

另外 Thriftgo 也提供了一些简单的关于 Descriptor 与 Golang 反射结合使用的 API,例如下面这段代码,展示了通过 fieldDescriptor 来反射设置某个结构体的字段的值:

func TestReflection(t *testing.T) {

   p := thrift_reflection_test.NewPerson()
   p.Name = "CloudWeGo"

   nameFieldDesc := p.GetDescriptor().GetFieldByName("name")
   assert(t, nameDesc != nil)

   // test get instance value by field descriptor reflection api
   val, err := nameFieldDesc.GetInstanceValue(p)
   assert(t, err == nil)
   stringVal, ok := val.(string)
   assert(t, ok && stringVal == "CloudWeGo")

   // test set instance value by field descriptor reflection api
   err = nameFieldDesc.SetInstanceValue(p, "KiteX")
   assert(t, err == nil)
   assert(t, p.Name == "KiteX")

}

在 Thriftgo v0.3.0 版本中,由于 Thrift 反射功能刚刚发布,所以配合 Golang 反射使用的 API 封装较少,后续会根据用户建议与常用场景,完善这方面的 API。

FAQ

Q:为什么初始化阶段出现错误会直接采用 panic 来停止整个程序

因为若初始化与注册 IDL 时出现错误,导致某个 IDL 无法被注册上,那么在后续的使用里,会影响到 IDL 信息之间的查询结果(例如找不到某个本应存在的 Descriptor),可能会因此导致用户的业务逻辑出错。所以 IDL 注册出错时直接 panic,以明显的方式提示用户。

Q:初始化时报错 “thrift reflection: file ‘xxx’ is already registered”

因为 Thrift IDL 元信息在注册时,是以 IDL 文件的 filepath 为唯一的 Key 的,所以当遇到有重复名称的 IDL 注册时,会报这个提示。这个设计与 Google Protobuf 反射的注册重复报错是类似的。解决方式是根据报错提示的路径找到对应的文件,重新用不同的 IDL 名称再生成代码即可。

Q:考虑把反射信息的生成设置为默认行为吗

目前要生成反射信息,需要 thriftgo 额外开启参数。由于反射功能刚刚推出,可能在后续投入实际使用后可能会有修改,所以打算等该功能较为成熟后,再作为默认行为开启,从而尽可能在现阶段减少对不使用该功能的用户的打扰。

Q:这个功能可以和公共结构体一起使用吗

生成引用结构体 公共结构体功能是将本地某个 IDL 对应的 Golang 代码全部指向远端公共仓库的 Golang 代码。Thriftgo 反射功能可以和公共结构体功能一起使用。但有一些需要注意的说明。

首先,公共结构体的远端仓库在生成代码时也需要生成 Thriftgo 反射内容。

理论上,公共结构体场景下,本地的这份 IDL 是要和远端的 IDL 完全一致的。但不排除存在微弱的不一致情况,例如远端 IDL 多了一些结构体,或者某些结构体下增加了新的字段。例如下面的案例:

// 本地 IDL 
struct A{
    1:required string hello;
}
// 远端 IDL 
struct A{
    1:required string hello;
    2:required string hello2;
}

struct B{
    1:required string hello;
}

公共结构体场景生成的 Golang 代码为:

package xxx
import "repo/xxxx/remote"

type A = remote.A

由于实际的 Golang 结构体是使用的远端的,所以在公共结构体场景下,这份 IDL 的元信息在反射注册时将以远端的 IDL 情况为准(也就是既有 A 结构体也有 B 结构体的那个 IDL)。

此外还有一种特殊情况目前无法支持(出现概率极小),当对远端的一个 IDL 进行了拆分,让本地的一份 IDL 对应到了远端的两个 IDL 时,这种场景无法成功注册反射信息。