Go 更灵活地编解码 thrift 消息

2,100 阅读3分钟
原文链接: zhuanlan.zhihu.com

在 Go 语言里使用 thrift 进行编解码的过程中,我们发现了一些不够灵活的地方。所以我们写了一个新的 thrift 编解码实现,完全兼容 thrift 协议,更灵活而且性能更高。thrifter 项目地址:thrift-iterator/go

用起来就像 encoding/json 一样方便

thrifter 的 api 是完全照抄 encoding/json 的

import "github.com/thrift-iterator/go"
// marshal to thrift
thriftEncodedBytes, err := thrifter.Marshal([]int{1, 2, 3})
// unmarshal back
var val []int
err = thrifter.Unmarshal(thriftEncodedBytes, &val)

不仅仅支持简单的类型,struct 的数据绑定也是支持的

import "github.com/thrift-iterator/go"

type NewOrderRequest struct {
    Lines []NewOrderLine `thrift:",1"`
}

type NewOrderLine struct {
    ProductId string `thrift:",1"`
    Quantity int `thrift:",2"`
}

// marshal to thrift
thriftEncodedBytes, err := thrifter.Marshal(NewOrderRequest{
	Lines: []NewOrderLine{
		{"apple", 1},
		{"orange", 2},
	}
})
// unmarshal back
var val NewOrderRequest
err = thrifter.Unmarshal(thriftEncodedBytes, &val)

这个 api 并不依赖代码生成。而且速度也不会比 thrift 现在代码生成的编解码更慢。

不需要 IDL 也可以编码

不要 IDL。不需要静态代码生成。甚至前面那样的 struct 定义也可以不需要。

import "github.com/thrift-iterator/go"
import "github.com/thrift-iterator/go/general"

var msg general.Message
err := thrifter.Unmarshal(thriftEncodedBytes, &msg)
// the RPC call method name, type is string
fmt.Println(msg.MessageName)
// the RPC call arguments, type is general.Struct
fmt.Println(msg.MessageArgs)

这里的 general.Struct 是啥呢?

type FieldId int16
type Struct map[FieldId]interface{}

从 interface{} 里读取数据是很痛苦的。所以我们提供了一个简单的实用方法,可以一行 Get

productId := msg.MessageArgs.Get(
	protocol.FieldId(1), // lines of request
	0, // the first line
	protocol.FieldId(1), // product id
).(string)

任何 thrift 消息都可以用这个api进行解码。同时也可以编码回去。甚至用 thrifter.ToJSON 你可以把 thrift 消息 dump 成 JSON 字符串,在调试的时候非常有用。

部分解码

把 thrift 消息完整解码出来是需要消耗资源的。thrifter 提供了部分解码的选项,所有没有被修改的部分,可以用原始的 []byte 形式进行保存。

import "github.com/thrift-iterator/go"
import "github.com/thrift-iterator/go/protocol"
import "github.com/thrift-iterator/go/raw"

// partial decoding
decoder := thrifter.NewDecoder(reader)
var msgHeader protocol.MessageHeader
decoder.Decode(&msgHeader)
var msgArgs raw.Struct
decoder.Decode(&msgArgs)

// modify...

// encode back
encoder := thrifter.NewEncoder(writer)
encoder.Encode(msgHeader)
encoder.Encode(msgArgs)

这里 raw.Struct 的定义是

type StructField struct {
	Buffer []byte
	Type protocol.TType
}

type Struct map[protocol.FieldId]StructField

性能

thrifter 虽然方便,但是并不牺牲性能

gogoprotobuf

5000000	       366 ns/op	     144 B/op	      12 allocs/op 

thrift

1000000	      1549 ns/op	     528 B/op	       9 allocs/op 

thrifter by static codegen

5000000	       389 ns/op	     192 B/op	       6 allocs/op 

thrifter by reflection

2000000	       585 ns/op	     192 B/op	       6 allocs/op 

我们可以看到,基于反射的实现其实并不差,比 thrift 原版生成的代码要快很多。

当然,如果要追求极致的性能,也可以使用静态代码生成,来生成更高效率的 encoder/decoder。api 使用上是一样的。只需要在代码构建过程中增加代码生成的步骤。运行时会自动使用生成的代码。这里有一个例子:github.com/thrift-iter…

IDL 和 Go 结构体怎么同步呢?

IDL 生成的代码限制很多,比如 *int,比如没法添加不需要序列化的字段。手工维护 Go 结构体又很烦人,很容易出错。怎样才能又灵活,又能和 IDL 定义同步呢?

thrifter 项目并没有解决这个问题。因为我们内部有另外一套 IDL 的工具链。