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
Tagged library versions
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 :)
No non stdlib dependencies
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.
Abstract non stdlib dependencies into their own packages
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.
- A package that captures Go stack traces and hands them to an interface
- 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.
Do not use /vendor
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.
Use dependency management so people can run your tests
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
No global mutable state
Global mutable state makes code difficult to reason about, expand, stub, test, and back out of.
Empty object has reasonable behavior
Make the zero value useful.
Read operations on the nil instance behave the same as read operations on the empty instance
This behavior is mirrored by many of Go’s internal structs.
Avoids constructor functions unless required
This is a side effect of making the zero value useful.
Minimal public functions
Libraries that do few things tend to do them well.
Small interfaces with few functions
The bigger the interface, the weaker the abstraction.
Accept interfaces, return structs
More details here.
Configuration mutable at runtime without violating -race
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.
An API I can interface without importing your library
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) { ... }
Minimal object (GC) creation on API calls
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) { ... }
No side effect imports
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.
Avoided use of context.Value when alternatives exist
An expansion of the ideas on my previous post.
Avoid complex logic inside init
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.
Allow injecting global dependencies
For example, do not force http.DefaultClient
, when you can allow users to provide a http.Client
.
Errors
Check all 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.
Expose errors by behavior, not type
This is the library centric equivalent of Dave’s Assert errors for behaviour, not type. More information here.
Do not panic
Concurrency
Avoid creating goroutines
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.
Allow clean shutdown of background goroutines
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.
Avoid channels in your public API
This is a code smell that you’re implying concurrency at the library level rather than letting the user of your library control concurrency.
All long, blocking operations take context.Context
Context is a standard way to give users of your library control over when actions should be interrupted.
Debugging
Export internal stats
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 expvar.Var information
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.
Supports debugability
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.
Reasonable Stringer implementation
Stringer makes it easier for people to debug code using your library.
Easily customizable loggers
There is no broadly accepted Go logging library. Expose a logging interface that does not force me to import your favorite.
Cleanliness
Passes a reasonable subset of gometalinter checks
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.
No functions with 0% unit test coverage
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
Avoid splitting a struct’s functions across files
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.
Use of /internal
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.