Skip to content
JavaGo
Part II · Types & Data

Interfaces

Interfaces are satisfied implicitly, defined by the consumer, and kept small. Accept interfaces, return structs.

Section 06 of 31 5 min read

An interface is a set of method signatures. Any type with those methods satisfies the interface automatically; there is no implements clause. The compiler still checks everything statically, at the point where a value is assigned to an interface type. This is structural typing, checked at compile time.

Implicit vs explicit implementation

Java — explicit implements
public interface MessageStore {
    Message getById(long id);
    void save(Message m);
}

// The class must name the interface
public class PostgresStore implements MessageStore {
    @Override
    public Message getById(long id) { ... }

    @Override
    public void save(Message m) { ... }
}
Go — implicit satisfaction
type MessageStore interface {
    GetByID(ctx context.Context, id int64) (Message, error)
    Save(ctx context.Context, m Message) error
}

// No 'implements': having the methods is enough
type PostgresStore struct{ db *sql.DB }

func (s *PostgresStore) GetByID(ctx context.Context, id int64) (Message, error) {
    // ... query ...
}

func (s *PostgresStore) Save(ctx context.Context, m Message) error {
    // ... insert ...
}

// Optional compile-time assertion (see Blank Identifier)
var _ MessageStore = (*PostgresStore)(nil)

Define interfaces where they are used

the consumer owns the interface
// package handler: the CONSUMER declares only what it calls
package handler

import (
    "context"

    "github.com/mycompany/app/internal/store"
)

type messageReader interface {
    GetByID(ctx context.Context, id int64) (store.Message, error)
}

type Handler struct {
    messages messageReader // depends on behaviour, not on PostgresStore
}

func New(messages messageReader) *Handler {
    return &Handler{messages: messages}
}

// *store.PostgresStore satisfies messageReader automatically.
// The store package never imports handler or knows it exists,
// and a test can pass a two-line fake.

Java: the implementer declares it

Go: the consumer declares it

Fig. 6.1Who owns the interface. A Java class must name each interface it implements, so the interface usually lives with the implementation. In Go the consumer declares the small interface it needs; any type with those methods satisfies it, and the store package never imports handler or knows it exists.

Accept interfaces, return structs

This Go proverb flips the Java habit of pairing every class with an interface. A constructor returns its concrete type (*PostgresStore), and consumers accept a small interface describing what they need. Do not create UserService + UserServiceImpl pairs up front; introduce an interface when a second implementation or a test fake actually needs one.

Pointer receivers and method sets

the compile error every newcomer meets
type Counter struct{ n int }

func (c *Counter) Inc() { c.n++ } // pointer receiver

type Incrementer interface{ Inc() }

var a Incrementer = &Counter{} // ok: *Counter has Inc
var b Incrementer = Counter{}  // compile error:
// Counter does not implement Incrementer
// (method Inc has pointer receiver)

// Why: storing a Counter value in an interface stores a copy.
// Calling Inc on that copy would silently lose the update,
// so Go refuses to let the value satisfy the interface.

Composing small interfaces

small interfaces, embedded into larger ones
type Reader interface {
    GetByID(ctx context.Context, id int64) (Message, error)
}

type Writer interface {
    Save(ctx context.Context, m Message) error
    Delete(ctx context.Context, id int64) error
}

// Interface embedding: the method set is the union
type ReadWriter interface {
    Reader
    Writer
}

// A function that only reads accepts Reader, not ReadWriter,
// so its test fake needs exactly one method.
func ShowMessage(r Reader) http.HandlerFunc { ... }

// The standard library is built from tiny interfaces:
//   io.Reader     Read(p []byte) (n int, err error)
//   io.Writer     Write(p []byte) (n int, err error)
//   io.Closer     Close() error
//   fmt.Stringer  String() string
//   error         Error() string

Asking a value what else it can do

type assertions on interfaces: optional capabilities
// Like "instanceof" + cast, but for behaviour, not classes.
func describe(w io.Writer) {
    if f, ok := w.(interface{ Flush() error }); ok {
        f.Flush() // this writer also supports flushing
    }
    if s, ok := w.(fmt.Stringer); ok {
        fmt.Println("writer:", s.String())
    }
}

// net/http uses this pattern: a ResponseWriter may also be
// an http.Flusher or http.Hijacker, checked at runtime.
↑ ↓ to navigateEnter to openEsc to close