Skip to content

Commit

Permalink
doc: sync Thriftgo 反射使用文档 (#1144)
Browse files Browse the repository at this point in the history
  • Loading branch information
alice-yyds authored Sep 20, 2024
1 parent 5641fba commit f9b36e0
Showing 1 changed file with 296 additions and 0 deletions.
296 changes: 296 additions & 0 deletions content/zh/docs/kitex/Tutorials/code-gen/thrift-reflection.md
Original file line number Diff line number Diff line change
@@ -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 时,这种场景无法成功注册反射信息。

0 comments on commit f9b36e0

Please sign in to comment.