Skip to content

Design of Go-Github Library

Usually, we write a client package to hide the logic about rpc to get the value, hide complex about network.

This article will based on the library of https://github.com/google/go-github to introduce useful experience when we want to design a client for results from the remote.

As usual, our client package needs to satisfy some requirements, which why we create a saperate package for it.

  • we need to call many api from server, and most logics of them are duplicated.
  • hide some duplicated work and could be reused by the caller.

Brief

I would like to introduce the following points in this article:

  • how to wrap API as a client?
  • what abstractions are made by go-github?
  • how go-github library maintain its APIs?

Library as a client

For the Github go client, it wraps the github API and users only need to pass the nessary arguments(for example, owner and reporsitory name for a reporsitory details) and then get result from the call.

Can see the function signature, it only receives an option for this request, and returns the analyzed result and origin response.

func (s *IssuesService) List(ctx context.Context, all bool, opts *IssueListOptions) (
    []*Issue, *Response, error)

It hide some logics which should learn here:

  • how to convert option to query string.
  • how to create request and make http request.
  • how to analyze result for different type of requests.

Because of the amount of requests, we obviously cannot make a bulk of raw logic for each function which will cause high duplication.

Abstraction in go-github

The go-github library does a good abstraction on constructing URL, creating request and making response with well format result.

convert option to query string

This way convert the options to the query string. Note that ************************************************the function receives an interface************************************************. It uses the library https://github.com/google/go-querystring to convert struct to query string. So it receives an interface to parse it to query string.

func addOptions(s string, opts interface{}) (string, error) {
    v := reflect.ValueOf(opts)
    if v.Kind() == reflect.Ptr && v.IsNil() {
        return s, nil
    }
    u, _ := url.Parse(s)
    qs, _ := query.Values(opts)
    u.RawQuery = qs.Encode()
    return u.String(), nil
}

In my code, I create a function called constructURL, its backward is I define that the argument is not flexiable for the caller, how the caller know what query string it should pass?

It’s better to define the struct that is easy for caller to use, instead of passing the url directly.

func (g *GitLabClient) constructURL(path string, queryValue *url.Values) string

Create http request object

Before explaining the details, let us think why we need to create a http request object? Of course, if I’m familiar with the net/http, I will clear that all kinds of requests should create a http request object first, and then send it by the http client.

The client NewRequest only wraps the general headers in the request.

Send the request and analyze the wanted data

There is a common case that we need to unmarshal the data to a certain data type, it should be done during making request.

  • Do method: get response and analysis the response body for v parameter. So next time you can pass an interface and let it is analyzed inside the function.
func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
    resp, err := c.BareDo(ctx, req)
    if err != nil {
        return resp, err
    }
    defer resp.Body.Close()

    switch v := v.(type) {
    case nil:
    case io.Writer:
        _, err = io.Copy(v, resp.Body)
    default:
        decErr := json.NewDecoder(resp.Body).Decode(v)
        if decErr == io.EOF {
            decErr = nil // ignore EOF errors caused by empty response body
        }
        if decErr != nil {
            err = decErr
        }
    }
    return resp, err
}
  • BareDo method: makes resquest, check error and manipulate the response. The library do this as it needs to check the rate limitation.

How to manage different parts of API

Github provides many types of API, issues, pulls and so on. Each of them needs the client we encapsulate, but we cannot define all methods with the client receivers.

Go-github uses a good way to do that, by this way it saparates apis in different topic.

type Client struct {
    common service // Reuse a single struct instead of allocating one for each service on the heap.

    // Services used for talking to different parts of the GitHub API.
    Actions        *ActionsService
    Activity       *ActivityService
    Admin          *AdminService
        // ignore many lines...
}
type service struct {
    client *Client
}