Go client library best practices

Jack Lindamood
4 min readJul 5, 2016

--

This post will describe what I feel are best practices when creating a client library for a service. The initial setup is that you’re writing a library with an API to talk to a RESTful service over HTTPS, and all the library needs to do is return JSON unmarshalled objects. I’ll use some examples from a client I wrote to talk to Smite’s API.

Directly use the struct

While constructor functions are sometimes needed for Go libraries, it’s strongly preferred to support users directly using your struct{} object. For example, the Go standard library’s http client is used by directly instantiating it. The advantages of direct struct initialization vs constructor functions is worthy of its own post, but the primary reasons are local clarity and simplicity of code.

// preferred.go
client := smitego.Client{
DevID: 123,
AuthKey: "AuthKey123",
}
// discouraged.go
client := smitego.NewClient(123, "AuthKey123")

Reasonable empty struct

The empty struct of your client should have reasonable behavior. This usually means things like

  • If no URL is set, the default URL should be https://api.yoursite.com
  • If no userID is set, the default userID should be anonymous

One easy way to get good default behavior is to internally access struct variables in a wrapper that checks for empty and returns a default.

Sometimes a default URL isn’t totally possible if the client is for an internal service. In this case it’s reasonable to either default localhost for developers or return an explicit error when the client is used.

Cancelable requests

As a general rule, every blocking or IO function call should be cancelable or timeoutable. How you achieve this is up to you. I prefer to use context.Context. It’s strongly preferred that this is not a global timeout on all requests through your client library, but rather a per request settable timeout or cancel. For example, setting a HTTP timeout of 3 seconds on your HTTP client would be a half measure. One way to achieve this is with a context on each function. For example:

func (c *Client) Ping(ctx context.Context) error {
// ...
}

With this code, if someone wanted a specific timeout they would do:

ctx := context.Background()
ctx, cancel = context.WithTimeout(ctx, 3*time.Second)
defer cancel()
client.Ping(ctx)

On the other hand, if someone wanted a dynamic timeout, the code would be like the following

ctx := context.Background()
ctx, cancel = context.WithCancel(ctx)
defer func() {
<- eventHappens
cancel()
}()
client.Ping(ctx)

Unit tests for client libraries

Client libraries tend to be (and probably should be) pretty shallow abstractions around a REST interface. This makes unit tests for client libraries very shallow. At most, you can unit test that the URL the client talks to appears ok or that it will marshall or unmarshall data correctly. One way to test requests is to use the httptest library built into Go.

Example client library test

One thing to note about this test is that lines 2–11 are common setup code that you would abstract out once, and lines 14–26 are specific to your test. I personally use https://github.com/smartystreets/goconvey for this abstraction.

Another way to unit test client code is to abstract out the RoundTripper at https://golang.org/pkg/net/http/#RoundTripper. With this, you can set an explicit response that looks like what you want your client library to work with.

In this version of a unit test, I don’t need to create a testing HTTP server. Instead, by changing the Transport variable of the Client I can assert my own request and return a request I expect the server to return.

Don’t name your client package “client”

Package names are prepended to your functions or struts. Client is very ambiguous and broad. A better name would be one that includes your protocol’s name. For example, the HTTP client is inside the package “http”.

Integration tests are important

Integration testing is arguably more important for clients than unit testing. While unit tests can easily fall behind the service implementation, integration tests for client libraries are easy sanity tests that your application works in practice and the server implementation doesn’t change from under you. A good practice I’ve found is to:

  • Build tag the test as an integration test with // +build integration
  • Store hostname or testing credentials in a file .<client>-testing.json
  • Add .<client>-testing.json to your .gitignore

If this was a company internal service, devInfo struct may also include things like a development hostname to connect to. Integration tests can be run with

go test -v --tags integration .

Supporting Go 1.4 and context in HTTP

You may want to support multiple versions of Go. In Go 1.5, for example, http.Request has a Cancel object that you can manipulate directly, while Go 1.4 may need more code to correctly use the context object. You can do this with build tags. Create a function where you would do Go 1.5 only code. In one file, add at the top // +build !go1.5 while in another you add // +build go1.5.

You can see an example of this in the following two files

HTTP client usage tips

Two things that may be specific to using the HTTP client in Go is to remember to close the HTTP response body and drain the response body before closing it. The primary purpose is to allow the connection to be reused by the client library. Example code would look like the following

--

--