Writing a client library in Go
There are multiple ways of writing client libraries in Go. In this post, I will explore what I look for in a library depending if I am developing or checking if it is suitable for a project.
Without any specific order of importance, these are my thoughts on the subject.
Usage and Examples
Like a book can be judged by its cover, a project can be judged by its README. Therefore providing examples helps getting an idea of how the library works: if it will fit a project or how good the developer usability is (this includes function naming, usage difficulty or how easy it is to misuse).
Specific to Go, reviewing godoc and runnable examples, if applicable.
Should the library have limitations or missing features, the README is the perfect place to leave that information.
Configuration
When evaluating a client, mainly API clients, another factor to take into consideration is how difficult it is to customize the client. Is it possible to set up different endpoints or for example http.Client. Can I use this library in tests easily if needed?
Is there a client builder or functional options in place? As long as there aren’t multiple overlapping ways of creating a client:
// builder style
client := unicorn.NewClient(http.DefaultClient)
// functional options style
client := unicorn.New(unicorn.WithHttpClient(http.DefaultClient))
Avoid being opinionated
Everyone has an opinion but when writing a library, avoid making decisions on behalf of your users. While you may have an idea of which http.Client works better, or which logger package gets it right, you don’t know if your user has different views or constraints. Not having opinions applies to any package or configuration: http.Client, logging, telemetry, metrics, etc.
Another benefit of not making decisions on behalf of the users is reducing the number of dependencies and potentially security or maintainability issues.
context.Context
In Go, context.Context
is used for cancellation and storing values during the lifetime of a request. Ensuring functions accept a context makes them easier to assist in propagation if required:
func Fetch(ctx context.Context, /* args */) {}
As stated in the context package:
Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it
func Fetch(p Params{
ctx: ctx,
/* other args */
}) {}
Provide useful errors
When errors happen, ensure all the relevant information is available, clear and useful. That can be a status code, an error message or any other exposed data.
Having a custom error with all the information readily accessible can unblock features such as circuit breakers, retries or just faster troubleshooting.
For example go-github defines the following error structure:
// https://github.com/google/go-github/blob/master/github/github.go#L653
type ErrorResponse struct {
Response *http.Response // HTTP response that caused this error
Message string `json:"message"` // error message
Errors []Error `json:"errors"` // more detail on individual errors
// Block is only populated on certain types of errors such as code 451.
// See https://developer.github.com/changes/2016-03-17-the-451-status-code-is-now-supported/
// for more information.
Block *struct {
Reason string `json:"reason,omitempty"`
CreatedAt *Timestamp `json:"created_at,omitempty"`
} `json:"block,omitempty"`
// Most errors will also include a documentation_url field pointing
// to some content that might help you resolve the error, see
// https://docs.github.com/en/rest/reference/#client-errors
DocumentationURL string `json:"documentation_url,omitempty"`
}
Not exposing important information or forcing the use of string matching on errors should be avoided:
// Please don't force users to do this
if strings.Contains(err.Error(), "something bad") {
...
}
License
Be clear about the license in place. Some companies have limitations on which licenses are acceptable.
Conclusion
Taking into consideration the points above, while sometimes there are constraints when writing a library,
attempting to follow the topics I mentioned is a good rule of thumb to writing an idiomatic library.
Examples that tick most boxes are: