Interfaces in Go
Interfaces are one of the most powerful features in Go, but they can be confusing at first given their implicit nature.
Whether you’re a beginner or an experienced developer, understanding interfaces is essential for writing high-quality Go Code.
Understanding Interfaces in Go
An interface in Go is a type that specifies a set of method signatures. When a type provides definitions for all the methods in the interface, it implicitly implements that interface.
Let’s start with a simple example:
package main
import "fmt"
type Greeter interface {
Greet() string
}
type EnglishGreeter struct{}
func (e EnglishGreeter) Greet() string {
return "Hello!"
}
type SpanishGreeter struct{}
func (s SpanishGreeter) Greet() string {
return "¡Hola!"
}
func main() {
var greeter Greeter
greeter = EnglishGreeter{}
fmt.Println(greeter.Greet())
greeter = SpanishGreeter{}
fmt.Println(greeter.Greet())
}
This will output:
Hello!
¡Hola!
what did we do here?
- Greeter is an interface with a single method
Greet() string
. EnglishGreeter
andSpanishGreeter
are structs that implement the Greeter interface.- We can assign instances of these structs to a variable of type Greeter.
Implementing Interfaces
Go uses implicit implementation, meaning you don’t need to explicitly declare that a type implements an interface. If the type has all the methods declared in the interface, it implements it. This can be really confusing, especially if you come from a language like Java or PHP.
type Reader interface {
Read(p []byte) (n int, err error)
}
type File struct{}
func (f *File) Read(p []byte) (n int, err error) {
// Implementation here
return 0, nil
}
Here, *File
implements the Reader
interface because it has a Read
method with the same signature, even though we do not explicitly tell the compiler that is the case.
Does that mean a struct can implement multiple interfaces?
Yes!
MyRW
implements both Reader
and Writer
, and therefore also ReadWriter
.
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
type MyRW struct{}
func (rw *MyRW) Read(p []byte) (n int, err error) {
// Read implementation
return 0, nil
}
func (rw *MyRW) Write(p []byte) (n int, err error) {
// Write implementation
return 0, nil
}
Value Receivers vs. Pointer Receivers
In Go, methods can have either value receivers or pointer receivers:
- Value Receiver (
func (t T) Method()
): The method operates on a copy of the value. Changes inside the method do not affect the original value. - Pointer Receiver (
func (t *T) Method()
): The method operates on the original value through a pointer. Changes inside the method can modify the original value.
When implementing an interface, the method’s receiver type determines whether a type or its pointer implements the interface.
Case 1: Methods with Value Receivers
If the interface method is defined with a value receiver, both the type and its pointer can implement the interface.
type Greeter interface {
Greet() string
}
type EnglishGreeter struct{}
func (e EnglishGreeter) Greet() string {
return "Hello!"
}
func main() {
var greeter Greeter
greeter = EnglishGreeter{} // Works
fmt.Println(greeter.Greet())
greeter = &EnglishGreeter{} // Also works
fmt.Println(greeter.Greet())
}
Case 2: Methods with Pointer Receivers
If the method has a pointer receiver, only the pointer to the type implements the interface.
type Greeter interface {
Greet() string
}
type SpanishGreeter struct{}
func (s *SpanishGreeter) Greet() string {
return "¡Hola!"
}
func main() {
var greeter Greeter
greeter = SpanishGreeter{} // Error: Does not implement Greeter
fmt.Println(greeter.Greet())
greeter = &SpanishGreeter{} // Works
fmt.Println(greeter.Greet())
}
You might see an error such as this in the console:
cannot use SpanishGreeter{} (type SpanishGreeter) as type Greeter in assignment:
SpanishGreeter does not implement Greeter (Greet method has pointer receiver)
Best Practices with Interfaces
Keep them small!
Smaller interfaces are easier to implement and mock.
// Prefer this
type Reader interface {
Read(p []byte) (n int, err error)
}
// Over this
type ReadCloser interface {
Read(p []byte) (n int, err error)
Close() error
}
Use Interface Types as Function Parameters
Accept interfaces where possible to allow for flexible implementations.
func CopyData(r Reader, w Writer) error {
buf := make([]byte, 1024)
for {
n, err := r.Read(buf)
if err != nil {
return err
}
if n == 0 {
break
}
if _, err := w.Write(buf[:n]); err != nil {
return err
}
}
return nil
}
Return Concrete Types
Return concrete types when you want to expose all the functionalities of the type.
func NewBuffer() *bytes.Buffer {
return &bytes.Buffer{}
}
Use Interface Composition
Build complex interfaces using smaller ones.
type ReadCloser interface {
Reader
Closer
}
Avoid Overusing Interfaces
Don’t define interfaces until you need them. Premature abstraction can makes things more complicated and hard to read.
Testing with Interfaces
Interfaces make mocking and testing a breeze.
Suppose you have a function that depends on an external service:
type Service interface {
FetchData() (string, error)
}
func ProcessData(svc Service) error {
data, err := svc.FetchData()
if err != nil {
return err
}
// Process data
return nil
}
You can create a mock in your tests like so:
type MockService struct{}
func (m MockService) FetchData() (string, error) {
return "mock data", nil
}
func TestProcessData(t *testing.T) {
mockSvc := MockService{}
err := ProcessData(mockSvc)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
}
I really like gomock for testing, but it can be a little painful to get started with. It lets you generate mocks from interfaces.
Enforcing Interface Implementation at Compile Time
A common pattern in Go is to enforce that a type implements a particular interface at compile time. This is achieved using a simple assignment to the blank identifier _:
var _ MyInterface = (*MyStruct)(nil)
Here’s what this line does:
var MyInterface
declares a variable of type MyInterface but assigns it to the blank identifier , which means the variable is discarded.
(*MyStruct)(nil)
creates a nil pointer of type *MyStruct
.
- The assignment attempts to assign a
*MyStruct
to aMyInterface
variable.
By doing this, the Go compiler checks whether *MyStruct
implements MyInterface
. If it doesn’t, the compiler will throw an error during compilation. This technique is particularly useful for:
- Compile-Time Safety: Ensures that your types conform to the expected interfaces before the program runs.
- Documentation: Serves as an explicit declaration in your code that
*MyStruct
is intended to implementMyInterface
. - Avoiding Runtime Errors: Catches interface implementation issues early, preventing potential runtime panics due to missing method implementations.
Example
type MyInterface interface {
DoSomething()
}
type MyStruct struct{}
func (m *MyStruct) DoSomething() {
// Implementation
}
// Enforce at compile time that *MyStruct implements MyInterface
var _ MyInterface = (*MyStruct)(nil)
If you comment out or remove the DoSomething method from *MyStruct, the compiler will produce an error like:
cannot use (*MyStruct)(nil) (type *MyStruct) as type MyInterface in assignment:
*MyStruct does not implement MyInterface (missing DoSomething method)