Suppored Go releases

Following the official Go release policy, we support the latest two Go releases at the time of the Thrift release.

For example, at the time of Thrift v0.14.0 release, the latest two Go releases are go1.15 and go1.14, and those are the two Go releases supported by Thrift v0.14.* (including v0.14.1 and v0.14.2 patch releases).

Because of Go’s backward compatibility guarantee, older Thrift libraries usually works with newer Go releases (e.g. Thrift v0.14.* works with go1.16, although it’s not officially supported), but newer Thrift releases might use new APIs introduced in Go releases and no longer work with older Go releases. For example, Thrift v0.14.0 used APIs introduced in go1.13, and as a result no longer works on go1.12.

Using Thrift with Go

Thrift supports the currently officially supported Go releases (the latest 2).

After initializing the go modules file in your project, use the following command to add the most recent version of the package:

$ go get github.com/apache/thrift

A note about optional fields

The thrift-to-Go compiler tries to represent thrift IDL structs as Go structs. We must be able to distinguish between optional fields that are set to their default value and optional values which are actually unset, so the generated code represents optional fields via pointers.

This is generally intuitive and works well much of the time, but Go does not have a syntax for creating a pointer to a constant in a single expression. That is, given a struct like

struct SomeIDLType {
	OptionalField *int32
}

, the following will not compile:

x := &SomeIDLType{
	OptionalField: &(3),
}

(Nor is there any other syntax that’s built in to the language)

As such, we provide some helpers that do just this under lib/go/thrift/. E.g.,

x := &SomeIDLType{
	OptionalField: thrift.Int32Ptr(3),
}

And so on. The code generator also creates analogous helpers for user-defined typedefs and enums.

Adding custom tags to generated Thrift structs

You can add tags to the auto-generated thrift structs using the following format:

struct foo {
  1: required string Bar (go.tag = "some_tag:\"some_tag_value\"")
}

which will generate:

type Foo struct {
  Bar string `thrift:"bar,1,required" some_tag:"some_tag_value"`
}

A note about server handler implementations

The context object passed into the server handler function will be canceled when the client closes the connection (this is a best effort check, not a guarantee – there’s no guarantee that the context object is always canceled when client closes the connection, but when it’s canceled you can always assume the client closed the connection). The cause of the cancellation (via context.Cause(ctx)) would also be set to thrift.ErrAbandonRequest.

When implementing Go Thrift server, you can take advantage of that to abandon requests that’s no longer needed by returning thrift.ErrAbandonRequest:

func MyEndpoint(ctx context.Context, req *thriftRequestType) (*thriftResponseType, error) {
    ...
    if ctx.Err() == context.Canceled {
        return nil, thrift.ErrAbandonRequest
        // Or just return ctx.Err(), compiler generated processor code will
        // handle it for you automatically:
        // return nil, ctx.Err()
    }
    ...
}

This feature would add roughly 1 millisecond of latency overhead to the server handlers (along with roughly 2 goroutines per request). If that is unacceptable, it can be disabled by having this line early in your main function:

thrift.ServerConnectivityCheckInterval = 0

Please be advised that due to a Go runtime bug, currently if this interval is set to a value too low (for example, 1ms), it might cause excessive cpu overhead.

This feature is also only enabled on non-oneway endpoints.

A note about maps with container keys

Go maps cannot be keyed by a slice or a map, so a thrift map whose key type is a list, set, map, or a typedef of one of those is generated as a slice of key/value pairs instead:

struct S { 1: map<list<string>, i32> m }

type S struct {
    M []thrift.MapEntry[[]string, int32]
}

Entries are written to the wire in slice order. As with set fields, writing a slice that contains two equal keys fails with an INVALID_DATA protocol exception. A map is unordered, so Equals compares the entries as a collection: two values holding the same entries in a different order are equal. Maps keyed by binary remain Go maps keyed by string.

Maps keyed by a struct, an exception or a union are generated as map[*K]V by default. Because the key is a pointer, two decoded keys with equal contents are distinct map keys, and Equals compares them by identity. Pass the struct_key_entries option to generate those fields as []thrift.MapEntry[*K, V] as well:

thrift --gen go:struct_key_entries file.thrift

The option changes more than the Go type. Equals starts comparing key contents, which is the point. Writing a slice that holds two keys with equal contents fails with INVALID_DATA, where the map form silently wrote both.

How that uniqueness check is done depends on the key. When every field of the key struct is a non-pointer scalar, the struct value is itself a valid Go map key and == on it agrees with Equals, so the check collects the keys in a set and runs in linear time. In a local benchmark, writing a field of 5000 such keys came within about 20 percent of the same data in map[*K]V, at the cost of one temporary set sized to the entry count.

Any other key falls back to comparing every pair, which is quadratic: 5000 entries cost about 12.5 million calls to Equals on every write. A key struct takes that path if it has an optional field, a field holding another struct, or a binary, list, set or map field, because none of those compare by content under ==. Unions always take it, since all of their fields are optional. Container keys take it too. Keep those maps small, or keep the key struct to plain scalars.

A typedef of a struct used as a key is the struct itself, since the generated type KeyAlias = Key is an alias, so the entry type is []thrift.MapEntry[*Key, V] and the key compares like Key does.

A note about typedefs of structs

A typedef of a struct, union or exception is generated as a Go type alias:

typedef Inner Alias

type Alias = Inner

Alias is the struct itself, with its Read, Write and Equals methods, and a field or argument declared with it is a *Inner like any other struct field. Earlier releases generated type Alias *Inner, a defined type over a pointer with no methods, so a package that used such a typedef in a field or a service signature did not compile. A typedef of a base type is unchanged and stays a defined type, type TimestampMilliseconds int64, with its <Name>Ptr helper.

What changes for hand-written code that used an alias the IDL declared but never used, the only shape that compiled before:

A note about undefined enum values

An enum holding an integer the IDL does not define used to print <UNSET> from String(), which reads as “no value was sent” even though the integer arrived intact and is written back to the wire unchanged. String() now formats such a value as Color(999), the shape stringer produces. MarshalText, and with it encoding/json, follow String(), so the text and JSON encoding of an undefined value changes the same way. Neither form round-trips through UnmarshalText, which still rejects any string that is not a defined name.

Every enum also gains an IsDefined() bool method that reports whether the value is one of the IDL-defined constants. Code that compared String() against the literal "<UNSET>" should call IsDefined() instead.

A note about server stop implementations

TSimpleServer.Stop will wait for all client connections to be closed after the last received request to be handled, as the time spent by Stop may sometimes be too long:

To prevent Stop from hanging for too long, you can set thrift.ServerStopTimeout in your main or init function:

thrift.ServerStopTimeout = <max_duration_to_stop>

If it’s set to <=0, the feature will be disabled (by default), and server will wait for all the client connections to be closed gracefully with zero err time. Otherwise, the stop will wait for all the client connections to be closed gracefully util thrift.ServerStopTimeout is reached, and client connections that are not closed after thrift.ServerStopTimeout will be closed abruptly which may cause some client errors.

A note about the minimum TLS version

The TLS socket constructors no longer pin a minimum protocol version of their own when the caller leaves tls.Config.MinVersion unset. Such a config now follows the crypto/tls default minimum – TLS 1.2 in current Go releases – where the constructors previously set TLS 1.0.

A MinVersion the caller sets is left untouched, so a caller that needs an older floor can ask for it explicitly:

conf := &thrift.TConfiguration{
    TLSConfig: &tls.Config{MinVersion: tls.VersionTLS10},
}

A note about the frames THeaderTransport writes

THeaderTransport.Flush now holds the frames it writes to the transport’s configured MaxFrameSize, the limit ReadFrame already applies to the frames it reads. A larger frame is not written; Flush returns a TProtocolException of type SIZE_LIMIT instead. The same applies to a header block larger than the frame format can describe (65535 words of four bytes).

A client that sends frames larger than the default of 16384000 bytes needs a larger MaxFrameSize in its own configuration, not only in the server’s:

conf := &thrift.TConfiguration{
    MaxFrameSize: 64 * 1024 * 1024,
}

A note about the frames TFramedTransport writes

TFramedTransport.Flush holds the frames it writes to the same limit, through the same check. A larger frame is not written: Flush returns a TProtocolException of type SIZE_LIMIT and drops the frame, where it previously returned no error and wrote a frame readFrame refuses. The note above on raising MaxFrameSize applies to it as well.