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:
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.
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
Interfaces Are Not Meant for That
It’s time to ask ourselves how much abstraction in our Go code really makes sense.
Python is Easy. Go is Simple. Simple != Easy.
Python and Go have distinct qualities that can complement each other.
Focus on the Happy Path With Step Functions
A simple pattern that will help you reduce error handling, while keeping your Go code simple and idiomatic.