Single-Implementation Interfaces in Go
In Java, classes can be instantiated using the default constructor unless there is a non-default constructor defined. One feature that I’ve often missed in Go is the ability to have such control over struct instantiation. Any exported struct definition can be defined from any other package, and methods on that struct should be written to work with the struct zero values. For example, from my FlowCache project:
// Cache implements a cache
type Cache struct {
data map[string]*cacheItem
MaxSize int
mutex sync.Mutex
}
The first problem we run into here is that the data field has to be initialized before the cache can be used, but because it is unexported even a well-behaved caller cannot initialize it properly. The most obvious way to ensure proper initialization is to check that the map is initialized at the start of every method that uses it:
func (c *Cache) Get(...) (interface{}, error) {
if c.data == nil {
c.data = make(map[string]*cacheItem)
}
...
}
Already we’re getting into dangerous territory in terms of code cleanliness. If our struct initialization gets any more complicated we’ll find ourselves calling an init method at the beginning of even the most basic accessors! This becomes even more complicated in the case of FlowCache. Cache is by design a concurrent cache, so it has to be able to accept concurrent access at any time, including in an uninitialized state. Thankfully the sync.Mutex
type is usable even in its zero state so we can synchronize our initialization, but concurrency problems don’t end there.
Go structs can be passed by value, copying all of their contents. In the case of our cache this is especially concerning because the mutex will be duplicated but the data map will not. If the cache is passed by value then accesses on the copied cache will not be synchronized and multiple goroutines could access the data map simultaneously, causing undesired behavior and crashes!
Interfaces to the Rescue!
Go interfaces add an incredible amount of flexibility to the language, but they also allow for stricter control of type usage, as they cannot be instantiated directly. By exporting an interface without any public implementation it is possible to force the use of a constructor. Here’s how this would apply to our cache implementation:
// Cache implements a cache
type Cache interface {
Get(...) func() (interface{}, error)
...
}
func (c *Cache) Get(...) (interface{}, error) {...}
type cache struct {
data map[string]*cacheItem
maxSize int
mutex sync.Mutex
}
// NewCache creates a new cache of size maxSize
func NewCache(maxSize int) Cache {
return &cache{data: make(map[string]*cacheItem), maxSize: maxSize}
}
We’ve essentially wrapped our struct in an interface which only has one implementation. This prevents instantiation of the cache without going through our constructor method. It also prevents accidentally passing the cache by value and duplicating the mutex.
Note: In our package the Cache
interface only has a single implementation, but there is nothing preventing another package from implementing the same interface. If it is important that methods which accept the interface only receive our implementation we can make the interface unimplementable by adding an unexported method to it. Why this would possibly be of value in a language like Go with no real data privacy is an exercise for the reader.
Is is even useful?
While forcing the use of a constructor has its benefits, in reality many projects tend not to wrap structs. Take, for example, GroupCache, which has a necessary constructor but also exports the raw type. There isn’t a sane reason to instantiate the struct directly, but the benefits of preventing it are far outweighed by the drawbacks.
For me the biggest drawback here (and the reason I decided to implement FlowCache without interface wrapping) is that godoc
does not do a good job showing interface methods. It shows the interface almost verbatim how it appears in the code, whereas structs are shown with each method having its own section.
Another potential drawback is performance, as managing the indirection of an interface takes some extra time for each method call.
Single-implementation interfaces are most useful in places where implicit initialization is infeasible. For example, in Go’s net.Conn
interface has no possible way to be initialized implicitly - a connection must actually be made to a provided host before it’s usable. In cases like this a wrapped struct can save headaches from users passing around and initializing your code improperly. In the case of my caching code this may be necessitated in the future by more complicated caching options, though for now simply using an exported struct seems sufficient.