The Design of Everyday Go APIs

By Ross Light

Frequently when people discuss what is a “good” Go library, they usually use terms like “idiomatic” or “the Go way”. These terms are imprecise and make it difficult to discuss the merits of different API designs. I recently re-read Don Norman’s The Design of Everyday Things and realized that the same principles of design discussed in the book can be used to evaluate the design of APIs.

The two main ideas in The Design of Everyday Things I want to discuss are affordances and signifiers.

Affordances describe the relationship between the user and the system. If a user can do X with the system, then the system affords X to that user. For example, handles afford pulling to users and physical buttons afford pressing to users. It’s important to note that an affordance is a relationship between a particular user and the system. Designing for accessibility means making an affordance work for the most people possible. (Julia Ferraioli gave a great talk about Writing Accessible Go at Gophercon 2018.) If a user cannot perform an action with a system, this is an anti-affordance. We’ll come back to why that is useful in a moment.

Signifiers convey information to the user beyond what the system affords. Push/pull signs on doors and road signs are examples of signifiers. While signifiers are a useful tool in a designer’s belt, it is best to use natural affordances that the user can perceive to communicate a system’s purpose. One example that Don Norman gives is the push/pull sign on a door. These signifiers are seldom necessary if the door has a handle (to pull) or bar (to push), since these perceived affordances already communicate how the user needs to operate the system. Road signs are necessary signifiers. There is no anti-affordance that a road designer can give, so a stop sign communicates to drivers that this intersection requires them to come to a complete stop.

So how does this translate to Go? Go’s type system is the way of providing affordances and anti-affordances. Using a string type affords sending textual data around in your program. Using an interface affords using any type that implements a method set. Declaring the type of a variable doesn’t just afford that data type, it provides anti-affordances for other data types. You can’t assign an integer to a string-typed variable. Another example is having an unexported name presents an anti-affordance for the function, variable, or type in other packages.

Documentation and variable/function names act as signifiers in Go: they do not change the affordances (other than visibility). But even types can be signifiers. For instance, Go strings can contain any sequence of bytes but the convention is that a []byte is used when a function operates on non-text data and string is used if a function expects text.

Applying these principles, we can see what makes for a better API. An API that uses the type system to afford correct usage and ideally uses anti-affordances to prevent errors is superior than one that does not. This matches most gophers' intuitions, like “interface{} says nothing” (a Go Proverb). However, Go’s type system does not prevent certain classes of bugs, and thus careful documentation in interfaces is important — like in io.Reader. This is the essence of human-focused design. The user is not “in error” for using your library wrong. The burden is on you as a library author to convey the right information at the right time in the right way. Studying user experience design is just as important when designing an API as it is when designing a GUI.