Create Go SDKs from OpenAPI / Swagger

SDK Overview

Speakeasy's Go SDK is designed to build idiomatic Golang modules, using language standard features. We aim at being backwards compatible as well, currently requiring only Go 1.14. Being Go, the code continues to work with all newer compilers as well.

The SDK is strongly typed, makes minimal use of third party modules, and is straight-forward to to debug. It should feel familiar to Golang developers when they use SDKs we generate. We make opinionated choices in some places, but we do so in a thoughtful and deliberate way.

For example, many Golang developers would rather rely upon zero values rather than pointers for unset optional values. However, so many REST APIs have nuanced distinctions between zero values and null values that this is not generally practical for interoperability. So we accept the compromise of using pointers, but also provide nil-safe getters to help offset the increased risk of panic from mishandled pointers.

The core features of the Go SDK include:

  • Using struct field tags and reflection-based marshaling and unmarshalling of data
  • Pointers are used for optional fields
  • Automatic getters that provide nil-safety and hooks for building interfaces
  • Context-aware method calls for programmatic timeouts and cancellation
  • A utils package that segments off common operations, making generated code easier to follow and understand
  • Use of variadic options functions are provided to ease construction whether you have many options or none
  • Authentication support for OAuth flows as well as support for standard security mechanisms (HTTP Basic, application tokens, etc.)
  • Optional pagination support for supported APIs
  • Optional support for retries in every operation
  • Complex number types
    • "github.com/ericlager/decimal".Big
    • "math/big".Int
  • Date and date/time types using RFC3339 date formats
  • Custom type enums using strings and ints
  • Union types and combined types

The SDK includes minimal dependencies, with the only third party dependencies being:

  • github.com/ericlagergren/decimal - provides big decimal support features
  • github.com/cenkalti/backoff/v4 - implements automatic retry support
  • github.com/spyzhov/ajson - is used to help implement pagination

Go Package Structure

lib-structure.yaml

|-- go.mod # part of the standard go mod setup
|-- go.sum # part of the standard go mod setup
|-- sdk.go # defines the root SDK package and the generate SDK class and New() constructor
|-- ... # Other SDK classes
|-- pkg # Root for all additional provided packages
| └-- models # Namespace for the SDK's models
| | └-- operations # Namespace for the SDK's operations models, including request/response models for each API
| | | └-- ...
| | └-- shared # Namespace for the SDK's shared models, including those from OpenAPI components
| | └-- ...
| |-- types # Namespace for additional helper types used during marshaling and unmarshalling
| | └-- ...
| └-- utils # Namespace for utility classes and functions
|-- docs # Markdown files for the SDK's documentation
| └- ...
└-- ...

HTTP Client

The Go SDK makes API calls that wrap an internal HTTP client. The requirements for the HTTP client are very simple. It must match this interface:


type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}

The built-in net/http client satisfies this interface and a default client based on the built-in is provided by default. To replace this default with a client of your own, you can implement this interface yourself or provide your own client configured as desired. Here's a simple example, which adds a client with a 30 second timeout.


import (
"net/http"
"time"
"github.com/myorg/your-go-sdk"
)
var (
httpClient = &http.Client{Timeout: 30 * time.Second}
sdkClient = sdk.New(sdk.WithClient(httpClient))
)

This can be a convenient way to configure timeouts, cookies, proxies, custom headers, and other low-level configuration.

Go Client Data Types & Enums

The Speakeasy Go SDK has a strong preference for familiar built-in types. The Go language has a rich built-in type system, so we are able to rely almost completely on it. Here is a list of types we use:

  • string
  • time.Time
  • int
  • int64
  • big.Int
  • float32
  • float64
  • bool
  • etc.

We do provide a few custom types in the types package, which are used to aid with marshaling and unmarshalling data exchanged with the server-side API. For example, types.Date is a thin wrapper around time.Time, which is able to decode and encode dates in "2006-01-02" format.

We also have made use of the decimal.Big class provided by github.com/ericlagergren/decimal. We feel this is a better alternative to big.Float as it provides high precision floating point math that lacks the rounding errors that can sometimes occur with big.Float.

Enumeration types are built by employing the typical Golang practice. Speakeasy defines a type alias to string or int or int64, as appropriate. Then constants of this type are defined for the predefined values.

Go SDK Generated Classes

The Go SDK generates a struct for each request and response object as well as for each component object. All fields in the struct objects are public. Optional fields will be given pointer types and may be set to nil. A getter method is also defined for each public field. The Get prefix distinguishes the getters from the public field names which remain directly accessible. The getters work correctly even when called on a nil value, in which case they return the zero value of the field.

For example, the following code shows a nested component object where the inner object is optional. The following code is safe from nil pointer related panics.


var outer *shared.Outer
var safe string = outer.GetInner().GetName()
if safe == "" {
fmt.Println("Don't Panic")
}
// output: Don't Panic

The getters also provide useful hooks for defining interfaces.

Parameters

As described above, the Speakeasy SDK will generate a class with public fields for each request and response object. Each field will be tagged to control marshaling and unmarshalling into other data formats while interacting with the underlying API. However, if the maxMethodParams value is set in gen.yaml, we will remove up to that number of parameters from the generated struct. They will be placed as positional parameters in the operation method after the context object and before the request object.


// maxMethodParams: 1
res, err := sdk.GetDrink(ctx, "Sangria")
if err != nil {
return err
}
// work with res...

Compare this with the example in the next section where maxMethodParams is 0.

Errors

Following Golang best practices, all operation methods in the Speakeasy SDK will return a response object and an error. Callers should always check for the presence of the error. The object used for errors is configurable per request. Any error response may return a custom error object. A generic error will be provided when any sort of communication failure is detected during an operation.

Here's an example of custom error handling in a theoretical SDK:


longTea := operations.GetDrinkRequest{Name: "Long Island Iced Tea"}
res, err := sdk.GetDrink(ctx, &longTea)
var apiErr sdkerrors.APIError
if errors.As(err, &apiErr) {
return fmt.Errorf("failed to get drink (%d): %s", apiErr.GetCode(), apiErr.GetMessage())
} else if err != nil {
return fmt.Errorf("unknown error getting drink: %w", err)
}
// work with res...

User Agent Strings

The Go SDK will include a user agent (opens in a new tab) string in all requests. This can be leveraged for tracking SDK usage amongst broader API usage. The format is as follows:


speakeasy-sdk/go {{SDKVersion}} {{GenVersion}} {{DocVersion}} {{PackageName}}

Where

  • SDKVersion is the version of the SDK, defined in gen.yaml and released
  • GenVersion is the version of the Speakeasy generator
  • DocVersion is the version of the OpenAPI document
  • PackageName is the name of the package defined in gen.yaml