When to Use Interfaces
Interfaces should be used in the following situations:
- Common behavior. For example,
sort.Interface
,io.Reader
that are used by multiple functions - Decoupling. Allows to use mocks in unit tests; use different algorithm without code changes
- Restricting behavior. Interface on the client side restricts the set of methods that can be called
As a general rule:
- We should create an interface when we need it, not when we foresee that we could need it
- Interfaces should be as small as possible, because the bigger the interface, the weaker the abstraction
Accept Interfaces and Return Structs
Accepting interfaces rather than concrete types makes functions more flexible and decoupled, allowing them to work with any type that satisfies the interface rather than a single specific type. It allows us to provide mocks for testing and swap implementations
In most cases we should return concrete implementations, not interfaces. Returning concrete types means that we can add new methods without breaking backward compatibility. Also, clients can use all the methods of the type, not just the one defined in the interface
When to Return an Interface
There are exceptions in the standard library, for example error
interface and io.LimitReader
function that returns io.Reader
instead of io.LimitedReader
. Although, it seems, that returning io.Reader
instead of a concrete io.LimitReader
was a mistake by Go designers
If a function needs to be able to return multiple implementations, it must return an interface
If a type exists only to implement an interface and will never have exported methods beyond that interface, there is no need to export the type itself. Exporting just the interface makes it clear the value has no interesting behavior beyond what is described in the interface. It also avoids the need to repeat the documentation on every instance of a common method. For example, if we have a Router
struct implementing http.Handler
, it’s better to return http.Handler
rather than Router
Interface on the Producer Side
Interfaces generally should be created on the client side, not the producer side
Defining the interface on the producer side forces clients to depend on all the methods of the interface. Instead, the client should define the interface with only the methods it needs. This relates to the concept of the interface segregation principle, which states that no client should be forced to depend on methods it doesn’t use
Note that interfaces sometimes can be used on the producer side. For example, io
package exports interfaces because it also needs to export generic-use functions like io.Copy
: func Copy(dst Writer, src Reader) (int64, error)
Interface Embedding
Interface Wrapping Method Erasure
- Fixing Interface Erasure in Go | doxsey.net
- Interface wrapping method erasure | by Jack Lindamood | Medium
- The trouble with optional interfaces | Mero’s Blog
Degrading Capability with a More Restrictive Interface
We can embed an interface in a struct that will act as a wrapper for the original value
This technique can be used to restrict the interfaces the original value implements
For example, the io.ReaderFrom
is defined as:
The os.File
type implements this interface, and inside its ReadFrom
method, it invokes:
It uses io.Copy
to copy from r
to f
, but instead of passing f
directly, it wraps it in an onlyWriter
struct:
To understand why, we should look at what io.Copy
does. If its destination implements io.ReaderFrom
, it will invoke ReadFrom
. But this brings us back in a circle, since we ended up in io.Copy
when File.ReadFrom
was called. This causes an infinite recursion
By wrapping f
in the call to io.Copy
, what io.Copy
gets is not a type that implements io.ReaderFrom
, but only a type that implements io.Writer
. It will then call the Write
method of our File
and avoid the infinite recursion trap of ReadFrom
Note that, we can use an anonymous wrapper struct:
Method Interception
We can define a struct with an embedded interface to intercept its method(s)
When such a struct is initialized with a proper value implementing the interface for the embedded field, it ‘inherits’ all the methods of that value
The key insight is that we can intercept any method we wish, leaving all the others intact
For example, we can implement middleware to provide logging. To achieve this, we need to capture the HTTP status code from the handler:
We can use the middleware as follows:
References
- 100 Go Mistakes and How to Avoid Them. Teiva Harsanyi
- SOLID Go Design | Dave Cheney
- Go Proverbs
- GopherCon 2023: Dylan Bourque - Clean Up Your GOOOP: How to Break OOP Muscle Memory - YouTube
- GopherCon 2019: Jonathan Amsterdam - Detecting Incompatible API Changes - YouTube
- Interface pollution in Go · rakyll.org
- Embedding in Go: Part 3 - interfaces in structs - Eli Bendersky’s website
- Golang Tip: Wrapping http.ResponseWriter for Middleware
- Designing Go Libraries | Abhinav Gupta
- Avtok. Interface Upgrades in Go