From f9b36e0c9d2dc26810da8c41d721db22a2487a91 Mon Sep 17 00:00:00 2001 From: alice <90381261+alice-yyds@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:05:38 +0800 Subject: [PATCH] =?UTF-8?q?doc:=20sync=20Thriftgo=20=E5=8F=8D=E5=B0=84?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=96=87=E6=A1=A3=20(#1144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Tutorials/code-gen/thrift-reflection.md | 296 ++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 content/zh/docs/kitex/Tutorials/code-gen/thrift-reflection.md diff --git a/content/zh/docs/kitex/Tutorials/code-gen/thrift-reflection.md b/content/zh/docs/kitex/Tutorials/code-gen/thrift-reflection.md new file mode 100644 index 0000000000..d023dcba36 --- /dev/null +++ b/content/zh/docs/kitex/Tutorials/code-gen/thrift-reflection.md @@ -0,0 +1,296 @@ +--- +title: "Thriftgo 反射使用文档" +date: 2024-09-20 +weight: 1 +keywords: ["Thriftgo 反射使用文档"] +description: "" +--- + +## 简介 + +Thriftgo 在 v0.3.0 版本提供了 Thrift IDL 反射功能,可以在运行时获取 Thrift IDL 的相关信息与描述符,并支持通过描述符来找到对应的 Go Type 并进行反射操作。我们可以在运行时使用这些反射查询接口,获取到 IDL 信息,以便进行各种查询和操作,以及更轻松的使用反射。 + +## 快速开始 + +例如有这样一个 IDL + +```go +// 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 参数,执行如下命令 + +```go +kitex -module xx -thrift with_reflection demo.thrift +``` + +生成代码的目录如下: + +```go +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 信息记录以及反射相关接口。 + +```go +// 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 在结构体定义时的信息,以及拿到字段的注释和注解等等: + +```go +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](https://github.com/cloudwego/thriftgo/blob/main/descriptor.thrift) + +除了在代码生成里提供 Golang 结构体找到 Descriptor 的方法以外,thrift_reflection 包内也提供了按照 Golang 类型或者名称查找 Descriptor 的方式: + +```go +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 的查找: + +```go +// 查询某结构体中直接和间接依赖的所有 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 类型: + +```go +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 查找反射字段 + +```go +goType := structDescriptor.GetGoType() +structEntity := reflect.New(goType).Elem() +for idx, fd := range des.GetFields() { + reflectField := structEntity.Field(idx) + // xxxx +} +``` + +另外 Thriftgo 也提供了一些简单的关于 Descriptor 与 Golang 反射结合使用的 API,例如下面这段代码,展示了通过 fieldDescriptor 来反射设置某个结构体的字段的值: + +```go +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:这个功能可以和公共结构体一起使用吗 + +[生成引用结构体](https://www.cloudwego.io/zh/docs/kitex/tutorials/code-gen/struct_reference_generator/) 公共结构体功能是将本地某个 IDL 对应的 Golang 代码全部指向远端公共仓库的 Golang 代码。Thriftgo 反射功能可以和公共结构体功能一起使用。但有一些需要注意的说明。 + +首先,公共结构体的远端仓库在生成代码时也需要生成 Thriftgo 反射内容。 + +理论上,公共结构体场景下,本地的这份 IDL 是要和远端的 IDL 完全一致的。但不排除存在微弱的不一致情况,例如远端 IDL 多了一些结构体,或者某些结构体下增加了新的字段。例如下面的案例: + +```go +// 本地 IDL +struct A{ + 1:required string hello; +} +``` + +```go +// 远端 IDL +struct A{ + 1:required string hello; + 2:required string hello2; +} + +struct B{ + 1:required string hello; +} +``` + +公共结构体场景生成的 Golang 代码为: + +```go +package xxx +import "repo/xxxx/remote" + +type A = remote.A +``` + +由于实际的 Golang 结构体是使用的远端的,所以在公共结构体场景下,这份 IDL 的元信息在反射注册时将以远端的 IDL 情况为准(也就是既有 A 结构体也有 B 结构体的那个 IDL)。 + +此外还有一种特殊情况目前无法支持(出现概率极小),当对远端的一个 IDL 进行了拆分,让本地的一份 IDL 对应到了远端的两个 IDL 时,这种场景无法成功注册反射信息。