English | 中文
A very fast dynamic Thrift serializer & deserializer without generating code.
Traditional Thrift serializer and deserializer are based on generated code which is no longer needed since we can make use of struct field tags.
Based on the test cases in frugal/tests, Frugal's performance is 3 to 4 times better than Apache Thrift (TBinaryProtocol).
There may be variations between different test cases. Feel free to share your test cases with us.
go version go1.23.6 linux/amd64
goos: linux
goarch: amd64
pkg: github.com/cloudwego/frugal/tests
cpu: Intel(R) Xeon(R) Gold 5118 CPU @ 2.30GHz
Marshal_ApacheThrift/small-4 3468714 346.1 ns/op 1684.32 MB/s 0 B/op 0 allocs/op
Marshal_ApacheThrift/medium-4 128386 9343 ns/op 1875.07 MB/s 0 B/op 0 allocs/op
Marshal_ApacheThrift/large-4 7208 164521 ns/op 1845.68 MB/s 109 B/op 0 allocs/op
Marshal_Frugal/small-4 13032746 92.45 ns/op 6306.09 MB/s 0 B/op 0 allocs/op
Marshal_Frugal/medium-4 327564 3669 ns/op 4774.38 MB/s 0 B/op 0 allocs/op
Marshal_Frugal/large-4 18751 64212 ns/op 4728.90 MB/s 0 B/op 0 allocs/op
Unmarshal_ApacheThrift/small-4 1548732 774.1 ns/op 753.15 MB/s 1120 B/op 4 allocs/op
Unmarshal_ApacheThrift/medium-4 42676 30665 ns/op 571.27 MB/s 44704 B/op 175 allocs/op
Unmarshal_ApacheThrift/large-4 2106 515642 ns/op 588.88 MB/s 775936 B/op 3030 allocs/op
Unmarshal_Frugal/small-4 4963635 266.2 ns/op 2189.92 MB/s 544 B/op 1 allocs/op
Unmarshal_Frugal/medium-4 99786 11321 ns/op 1547.45 MB/s 19908 B/op 57 allocs/op
Unmarshal_Frugal/large-4 5838 197987 ns/op 1533.69 MB/s 349252 B/op 997 allocs/op
Use Frugal as Kitex serializer and deserializer
No more massive serialization and deserialization code, leads to a more tidy project. No more meaningless diff of generated code in code review.
Serialized and Deserialize struct generated by Thriftgo
If you have a Thrift file, and all you need is using Frugal to do serialization and deserialization. You can use thriftgo to generate Go struct, then you can use Frugal.
If you don't want any Thrift files, and you want serialize or deserialize a customized Go struct. You can add some struct field tag to the Go struct, then you can use Frugal.
go get github.com/cloudwego/kitex@latest
go get github.com/cloudwego/frugal@latestExample:
kitex -thrift frugal_tag -service a.b.c my.thriftIf you don't need codec code, you can use -thrift template=slim option to reduce generated code significantly.
kitex -thrift frugal_tag,template=slim -service a.b.c my.thriftNote: Latest thriftgo (>= v0.3.0) generates thrift struct tags that are compatible with Frugal by default, so frugal_tag is optional for structs without list, set or enum fields.
Use thrift.NewThriftCodecWithConfig to enable Frugal. Codec type priority: Frugal > FastCodec > Apache Thrift.
Client example:
package main
import (
"context"
"example.com/kitex_test/client/kitex_gen/a/b/c/echo"
"github.com/cloudwego/kitex/client"
"github.com/cloudwego/kitex/pkg/remote/codec/thrift"
)
func main() {
codec := thrift.NewThriftCodecWithConfig(thrift.FrugalRead | thrift.FrugalWrite)
cli := echo.MustNewClient("a.b.c", client.WithPayloadCodec(codec))
...
}Server example:
package main
import (
"log"
"github.com/cloudwego/kitex/server"
c "example.com/kitex_test/kitex_gen/a/b/c/echo"
"github.com/cloudwego/kitex/pkg/remote/codec/thrift"
)
func main() {
codec := thrift.NewThriftCodecWithConfig(thrift.FrugalRead | thrift.FrugalWrite)
svr := c.NewServer(new(EchoImpl), server.WithPayloadCodec(codec))
err := svr.Run()
if err != nil {
log.Println(err.Error())
}
}Note: Kitex automatically falls back to FastCodec or Apache Thrift for types that don't have frugal struct tags, so it's safe to enable Frugal globally.
| Option | Description |
|---|---|
thrift.FrugalRead |
Use Frugal for deserialization |
thrift.FrugalWrite |
Use Frugal for serialization |
thrift.FrugalReadWrite |
Shorthand for FrugalRead | FrugalWrite |
thrift.FastRead |
Use FastCodec for deserialization |
thrift.FastWrite |
Use FastCodec for serialization |
thrift.EnableSkipDecoder |
Required when using Buffered transport (no Framed/TTHeader) |
For Framed or TTHeader transport, FrugalRead | FrugalWrite is sufficient. For Buffered (PurePayload) transport, add EnableSkipDecoder:
codec := thrift.NewThriftCodecWithConfig(thrift.FrugalRead | thrift.FrugalWrite | thrift.EnableSkipDecoder)We can define a struct in Thrift file like below:
my.thrift:
struct MyStruct {
1: string msg
2: i64 code
}Now we have thrift file, we can use Thriftgo to generate Go code.
Latest thriftgo (>= v0.3.0) generates thrift struct tags that are compatible with Frugal by default. For structs containing list, set or enum fields, add frugal_tag to generate explicit Frugal tags:
thriftgo -r -o thrift -g go:frugal_tag,package_prefix=example.com/kitex_test/thrift my.thriftIf you don't need codec code, you can use template=slim option to reduce generated code:
thriftgo -r -o thrift -g go:frugal_tag,template=slim,package_prefix=example.com/kitex_test/thrift my.thriftNow we can use Frugal to serialize or deserialize the struct defined in thrift file.
Example:
package main
import (
"github.com/cloudwego/frugal"
"example.com/kitex_test/thrift"
)
func main() {
ms := &thrift.MyStruct{
Msg: "my message",
Code: 1024,
}
...
buf := make([]byte, frugal.EncodedSize(ms))
frugal.EncodeObject(buf, nil, ms)
...
got := &thrift.MyStruct{}
frugal.DecodeObject(buf, got)
...
}We can define a struct like this:
type MyStruct struct {
Msg string
Code int64
Numbers []int64
}Frugal tag is like frugal:"1,default,string", 1 is field ID, default is field requiredness, string is field type. Field ID and requiredness is always required, but field type is only required for list, set and enum.
You can add Frugal tag to MyStruct like below:
type MyStruct struct {
Msg string `frugal:"1,default"`
Code int64 `frugal:"2,default"`
Numbers []int64 `frugal:"3,default,list<i64>"`
}All types example:
type MyEnum int64
type Example struct {
MyOptBool *bool `frugal:"1,optional"`
MyReqBool bool `frugal:"2,required"`
MyOptByte *int8 `frugal:"3,optional"`
MyReqByte int8 `frugal:"4,required"`
MyOptI16 *int16 `frugal:"5,optional"`
MyReqI16 int16 `frugal:"6,required"`
MyOptI32 *int32 `frugal:"7,optional"`
MyReqI32 int32 `frugal:"8,required"`
MyOptI64 *int64 `frugal:"9,optional"`
MyReqI64 int64 `frugal:"10,required"`
MyOptString *string `frugal:"11,optional"`
MyReqString string `frugal:"12,required"`
MyOptBinary []byte `frugal:"13,optional"`
MyReqBinary []byte `frugal:"14,required"`
MyOptI64Set []int64 `frugal:"15,optional,set<i64>"`
MyReqI64Set []int64 `frugal:"16,required,set<i64>"`
MyOptI64List []int64 `frugal:"17,optional,list<i64>"`
MyReqI64List []int64 `frugal:"18,required,list<i64>"`
MyOptI64StringMap map[int64]string `frugal:"19,optional"`
MyReqI64StringMap map[int64]string `frugal:"20,required"`
MyOptEnum *MyEnum `frugal:"21,optional,i64"`
MyReqEnum *MyEnum `frugal:"22,optional,i64"`
}Example:
package main
import (
"github.com/cloudwego/frugal"
)
func main() {
ms := &thrift.MyStruct{
Msg: "my message",
Code: 1024,
Numbers: []int64{0, 1, 2, 3, 4},
}
...
buf := make([]byte, frugal.EncodedSize(ms))
frugal.EncodeObject(buf, nil, ms)
...
got := &thrift.MyStruct{}
frugal.DecodeObject(buf, got)
...
}