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

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)