Aspects of a good Go library

A short checklist of what I wish for in a good Go library, in no particular order. This is a companion to the effective go list, Go code review comments list, and Go proverbs list.

In general, when given two reasonable ways to do something, defer to the option that does not violate these rules. Bend these rules only with a strong explanation why.

Dependencies

Use git tags to version your library. Semantic versioning is a reasonable system. If you care enough to disagree with semantic versioning, then you’re not the target here :)

This is sometimes difficult to achieve, but managing a library’s dependencies makes upgrading to the latest version painless and allows library users to reason better about the logic in their application. You can actually make your programs simpler to maintain if you keep the dependency tree really, really small.

This is a corollary to the above non stdlib dependencies requirement. If your library absolutely requires a non stdlib dependency, try to break it into two packages: one for the core logic and another that has an external dependency that uses that logic.

For example, if you are writing a package that captures Go stacktraces and uploads them to Amazon’s S3, write two packages.

  1. A package that captures Go stack traces and hands them to an interface
  2. A S3 implementation of that interface.

The second package can simplify gluing the two pieces together for users. This abstraction level lets users take advantage of your core functionality, while choosing to sub out S3 for their own storage layer. And your glue in the second package can avoid putting extra burden on the majority of people that want to upload their data to S3.

The key part here is two separate packages. This allows users to inject your core logic without polluting their dependency tree.

If your library vendors its dependencies, strange side effects can happen around package managers that try to flatten vendors, or vendor polluting your public API space making integrations difficult or impossible. If you really need an exact implementation, copy it explicitly.

Your library should try to not have external dependencies. If you must, however, use dependency management of some kind to allow library users to run your unit tests in a consistent manner, by signaling which version of which dependency you initially ran your tests against.

API

Global mutable state makes code difficult to reason about, expand, stub, test, and back out of.

Make the zero value useful.

This behavior is mirrored by many of Go’s internal structs.

This is a side effect of making the zero value useful.

Libraries that do few things tend to do them well.

The bigger the interface, the weaker the abstraction.

More details here.

If your library maintains complex state where it’s not a simple process to just re instantiate it, allow users to modify reasonable configuration parameters while their application is running.

Your library is great, but one day I will want to phase it out. Go’s type system sometimes works against this. However, on the balance, implicit interfaces win against overly strong static typing. When possible, prefer stdlib types as function parameters and returns so users can create interfaces out of your structs to later replace your library.

type AvoidThis struct {}
type Key string
func (a *AvoidThis) Convert(k Key) {... }
type PreferThis struct {}
func (p *PreferThis) Convert(k string) { ... }

CPU is often unavoidable, but minimizing garbage collection during API calls is often possible with rethinking your API. For example, create APIs that don’t force garbage collection. It is easy to optimize implementations after the fact and almost impossible to optimize APIs after the fact.

type AvoidThis struct {}
func (a *AvoidThis) Bytes() []byte { ... }
type PreferThis struct {}
func (p *PreferThis) WriteTo(w Writer) (n int64, err error) { ... }

I personally disagree with the Go standard library pattern of creating global side effect behavior based upon an import. This behavior usually involves global mutable state, can cause funny issues when libraries are /vendor included multiple times, and removes many options for customization.

An expansion of the ideas on my previous post.

The init function is useful to create default values, but is logic that’s impossible for a user to customize or ignore. Do not take control away from the user of your library without good reason. Accordingly, avoid spawning background goroutines inside init, and instead prefer users to explicitly ask for your background behavior.

For example, do not force http.DefaultClient, when you can allow users to provide a http.Client.

Errors

If your library accepts an interface as input, and someone gives you an implementation that returns an error, there is an expectation that you will check and somehow handler the error or communicate it back to the caller some way. Checking an error doesn’t just mean return it back up the call stack, although that’s sometimes reasonable, you can log it, mutate the return value as a result, have fallback code, or simply increment an internal stat counter so users know something is up, rather than have something fail without knowing anything is wrong.

This is the library centric equivalent of Dave’s Assert errors for behaviour, not type. More information here.

Just don’t

Concurrency

This is a more explicit rule reasoned by the CodeReviewComments synchronous functions section. Synchronous functions give the library user more control. Goroutines are sometimes useful to parallelize logic, but as a library author you should start from the state of not having goroutines and reasoning your way into them, rather than starting from goroutines and being argued away from them.

This is a preferred restriction on Goroutine lifetimes feedback. There should be a way to end any goroutines your library creates, in a way that won’t signal spurious errors.

This is a code smell that you’re implying concurrency at the library level rather than letting the user of your library control concurrency.

Context is a standard way to give users of your library control over when actions should be interrupted.

Debugging

I need to monitor your library for efficiency, usage patterns, and timings. Expose these stats somehow, so I can import them into my favorite metrics system.

Expose internal configuration and state information via expvar, to allow users to quickly debug how their application is using your library, not just how they think they are using it.

Eventually your library will have a bug. Or the user will use your library incorrectly and need to figure out why. If your library has any reasonable amount of complexity, expose a way to debug or trace this information. This could be with debug logs or the context debug pattern.

Stringer makes it easier for people to debug code using your library.

There is no broadly accepted Go logging library. Expose a logging interface that does not force me to import your favorite.

Cleanliness

Go’s simple syntax and great standard library functions allows a wide array of static code checkers, which are aggregated in the gometalinter. Your default state, especially if you’re new to Go, should be to just pass them all. Bend them only if you can explain why, and given two reasonable implementations defer to the one that passes the linter.

100% test coverage is extreme and 0% test coverage is almost never a good thing. This is difficult to quantify into a rule, and I’ve settled upon no function should have 0% test coverage as a minimum bar. You can get per function test coverage using Go’s cover tool.

# go test -coverprofile=cover.out context
ok context 2.651s coverage: 97.0% of statements
# go tool cover -func=cover.out
context/context.go:162: Error 100.0%
context/context.go:163: Timeout 100.0%
context/context.go:164: Temporary 100.0%
context/context.go:170: Deadline 100.0%
context/context.go:174: Done 100.0%
context/context.go:178: Err 100.0%
...

Repository Layout

Go allows you to spread a struct’s functions across files. This is very useful when using build flags, but if you’re doing this as a way to organize your struct, it’s a sign your struct is too large and you should break it into multiple components.

The /internal package is woefully underused. I recommend both binaries and libraries take advantage of /internal to hide public functions that aren’t intended to be imported. Hiding your public import space also makes clearer which packages users should import and where to look for useful logic.

Software Engineer