Concrete first, abstract later
Sat, Oct 18, 2014If there is one standout language feature in Go, it is interfaces. Go’s internals combine useful ideas from various type systems, inevitably piquing curiosity. I recently surveyed globally available open source Go code bases for interface declarations, and the results indicate that Go developers often pollute their codebases with interfaces no one needs or will ever use.
Don’t export any interfaces until you have to.
Interfaces are great, but interface pollution is not. You are likely coming to Go from a language (often a static one) that generates a dispatch table during compilation, where the compiler requires you to explicitly declare the interfaces a type implements. That’s how the compiler generates a vtable with pointers to all available virtual functions. If your background is in C++ or Java, you might have a habit of starting your codebase with abstract types and writing the concrete implementation as a follow-up exercise.
This is not how you write idiomatic Go. Introduce concrete types first, and don’t export any interfaces unless you need to encourage external packages to implement them. The standard library’s io package is a good starting point for studying these best practices. It exports interfaces because it needs to provide generic functions like Copy:
func Copy(dst Writer, src Reader) (written int64, err error)
Should your package export generic functionality? If the answer is “maybe,” you are likely polluting your package with unnecessary interface declarations. Justify the need for multiple implementations and the likelihood that they will interact back with your package, and act accordingly.
Because Go doesn’t have a traditional dispatch table, it resolves method dispatches at runtime using interface values. This dynamic dispatch mechanism requires a bit of work during interface value assignment, where Go generates a small lookup table (often called an itab) mapping the interface to the underlying concrete type. While this assignment has a small runtime cost, it is a fair trade-off for a much more flexible type system. Ian Lance Taylor has a great blog post about these internals if you want to learn more.
If a consumer of your package requires inversion of control, they can simply define an interface on the fly in their own scope. This minimizes the assumptions you need to make about how your package will be consumed, freeing you from designing unnecessary initial abstractions.
This also applies to testability: you do not need to export interfaces just to help users write stubs. I once received a request to export an interface from a pubsub package to make it more mockable. Instead of doing that, the better approach is to have the user define a local interface containing only the methods they actually need to mock:
type acknowledger interface {
Ack(sub string, id ...string) error
}
type mockClient struct{}
func (c *mockClient) Ack(sub string, id ...string) error {
return nil
}
var acker acknowledger = pubsub.New(...)
acker = &mockClient{} // in the test package
It is noteworthy that the Go standard library defines small, focused interfaces that types implement implicitly and effortlessly. This encourages developers to write code that integrates seamlessly with both the standard library and third-party packages. Whenever possible, leverage standard interfaces (like io.Reader or fmt.Stringer) rather than defining your own.
Go’s approach to interfaces remains one of its most elegant design decisions. By keeping interfaces minimal and decentralized, Go allows software components that were never designed to work together to integrate seamlessly.