Skip to main content
Preslav Rachev
  1. My Writings /Programming /

Partially-Implemented Interfaces in Go

·3 mins

Cover photo credits: Stable Diffusion

Go interfaces differ from what we know from other languages, such as Java or C#. They are often used as a form of static duck typing when dealing with dependencies. By declaring a local interface, we can specify what we want the dependency to do, instead of how the dependency looks like:

// Notice that the interface does not even need to be exported 
// for an outside dependency to satisfy it.
type doer interface {
	Do()
}

func doStuff(d doer) {
	d.Do()
}

Anything with a method called Do() qualifies as a potential dependency implementation. By convention, most interfaces in the Go standard library are small - one or One of Go’s popular proverbs says:

The bigger the interface, the weaker the abstraction.

However, interfaces often deviate from the proverb in the name of practicality when it comes to real-world applications. Therefore, it is not surprising to see an interface declaring 10-15 methods - this may be the case when developers decide to abstract away something as detailed as database persistence, for example - usually for testing purposes.

Suppose you have one of those and want to provide a mock implementation to a function that maybe uses one or two of its declared methods at most. Do you need to implement the rest to satisfy the interface?

One idea is to provide mock implementations for every method anyway - your tests will need each one sooner or later. However, this may lead to a single global mock implementation that is inflexible to the requirements of each specific test.

In my practice, I have found a simple way to satisfy an entire interface by only implementing the methods a given method would need. This is possible thanks to the way interface embedding works.

NOTE: The code below may result in runtime errors, so please use it with caution - primarily in tests or for small scratch code demos. As the proverb above suggests, try to avoid having large interfaces in the first place.

Check out the example below:

type Store interface {
	FindLatestOrder() (Order, error)
	FindCustomerByID(id int) (Customer, error)
	FindProductByID(id int) (Product, error)
	// ... and many more
}

func doSomething(s Store) {
	latestOrder, _ := s.FindLatestOrder() // error checks ignored for brevity
	u, _ := s.FindCustomerByID(latestOrder.CustomerID)
	fmt.Println(u.ID)
}

Our Store interface requires a lot of methods, but the function that uses it will make use of only two of them. How can we meet the requirements of the function without having to implement the entire interface?

type dummyStore struct{}

func main() {
	doSomething(&dummyStore{})
}

The code above will result in a pretty verbose (and rightly so) compile-time error.

However, the code below will not only compile but will run perfectly fine:

type dummyStore struct{
	Store
}

func (ds *dummyStore) FindLatestOrder() (Order, error) {
	return Order{CustomerID: 42, ProductID: 24}, nil
}

func (ds *dummyStore) FindCustomerByID(id int) (Customer, error) {
	return Customer{ID: 42}, nil
}

// NOTE that we are not implementing FindProductByID

func main() {
	doSomething(&dummyStore{})
}

How is this possible? Well, because of how embedding works, by embedding the Store interface in dummyStore, we automatically promote all of its methods to the embedding struct. Thus, dummyStore becomes compliant with Store, at least on paper.

Because we didn’t provide a concrete implementation of Store when instantiating dummyStore, its value will be nil. Thus, calling any of Store’s methods, not implemented by dummyStore will result in a nil pointer dereference.

In short, this is how one can satisfy a large-ish interface in Go. It was also a good exercise in seeing the boundaries of struct and interface embedding. Please, use it with caution.

Have something to say? Join the discussion below 👇

Want to explore instead? Fly with the time capsule 🛸

You may also find these interesting