This post describes 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 Link to heading

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 Link to heading

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
  • 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.

const DefaultBaseURL = ""

func (c *Client) urlBase() string {
    if c.BaseURL == "" {
        return DefaultBaseURL
    return c.BaseURL

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 Link to heading

As a general rule, every blocking or IO function call should be cancelable or have a timeout. 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()  

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  

Unit tests for client libraries Link to heading

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.

func ItemTest(t *testing.T) {
  // Common setup.  Abstract this out
  // This allows each test to create its own handler by changing handler variable
  handler := http.NotFound
  hs := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
       handler(rw, req)
  defer hs.Close()
  // Notice I set the base URL of the client to the httptest server
  c := Client{
       BaseURL: hs.URL,
  // Code specific to this test
  handler = func(rw http.ResponseWriter, req *http.Request) {
      if req.URL.Path != "/v1/smite/item/sword" {
        t.Error("Bad path!")
      io.WriteString(rw, `{"type":"sword"}`)
  item, err := c.GetItem(ctx, "sword")
  if err != nil {
    t.Error("Got error sending item")
  if item.Type != "sword" {
    t.Error("Did not get a sword!")

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 for this abstraction.

Another way to unit test client code is to abstract out the RoundTripper at With this, you can set an explicit response that looks like what you want your client library to work with.

type roundTripFunc func (r *http.Request) (*http.Response, error)

func (s roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
    return s(r)

func TestSword(t *testing.T) {
    var c Client
    c.Client.Transport = roundTripFunc(func(r *http.Request) (*http.Response, error) {
        assert.Equal(t, r.URL.Path, "/v1/item/sword")
        return &http.Response{
            StatusCode: http.StatusOK,
            Body: ioutil.NopCloser(strings.NewReader(`{"type":"sword"}`)),
        }, nil
    item, err := c.GetItem(ctx, "sword")
    assert.Nil(t, err)
    assert.Equal(t, item.Type, "sword")

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” Link to heading

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 Link to heading

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
// +build integration

package smiteclient

// Create a file named info.json and put it at the root of your project.  That file should have your
// devId and authKey.  The file should be inside .gitignore and not checked into git.  Then,
// run `go test -v --tags=integration .` to start integration tests using your auth key.
type devInfo struct {
    AuthKey string

func TestPing(t *testing.T) {
    Convey("With a client", t, func() {

        // Load the developer specific information for the integration test
        var di devInfo
        if f, err := os.Open(".client-testing.json"); err == nil {
            So(json.NewDecoder(f).Decode(&di), ShouldBeNil)
            So(f.Close(), ShouldBeNil)
        } else {
            t.Fatal("Could not find .client-testing.json")
        c := &Client {
            AuthKey: di.AuthKey
        ctx, canCtx := context.WithTimeout(context.Background(), time.Second*3)

        // Now we can run tests with a real client
        Convey("Ping should work", t, func() {
            So(client.AuthPing(ctx), ShouldBeNil)

        // Good practice to close your contexts when done
        Reset(func() {

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 Link to heading

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 Link to heading

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:

func (c *Client) doRequest(req *http.Request) {
    resp, err := c.client.Do(req)
    if err != nil {
        return err
    defer func() {
        maxCopySize := 2 << 10
        io.CopyN(ioutil.Discard, resp.Body, maxCopySize)
    // ....