Interfaces are one of the most powerful features in Go, but they can be confusing at first given their implicit nature.

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 and SpanishGreeter 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 a MyInterface 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 implement MyInterface.
  • 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)