Module 1 · Lesson 4 · ~25 min read

Interfaces — Structural and Implicit

Go's interfaces are the feature you'll use most and the one that's weirdest compared to Python/JS/Hack. They're structural (satisfied by shape, not by declaration) and implicit (no implements keyword). The resulting idioms reshape how you design code.

The one paragraph

An interface is a named set of method signatures. Any type that has all those methods automatically satisfies the interface — no declaration required. You rarely define interfaces where the types live; you define them at the consumer. "Accept interfaces, return concrete types" is the slogan. Learn it.

The shape

type Writer interface {
    Write(p []byte) (int, error)
}

// A concrete type. Nowhere does it say "implements Writer".
type StdoutWriter struct{}

func (StdoutWriter) Write(p []byte) (int, error) {
    return os.Stdout.Write(p)
}

// It just works:
var w Writer = StdoutWriter{}
w.Write([]byte("hi\n"))

Structural = "shape matches, it satisfies." Implicit = "no ceremony to declare satisfaction." This seems unremarkable until you realize it means you can define an interface after the types that satisfy it have been written, in a different package, and no one has to change anything.

Accept interfaces, return concrete types

In Python/JS/Hack, you often type a function parameter as "class Foo or any subclass." In Go, you type a parameter as "any type that satisfies this interface." And you define the interface at the function that consumes it, not where the types live.

// Bad: exposes too much. We only need Write(), not every method on os.File.
func WriteHello(f *os.File) { f.WriteString("hi") }

// Good: minimum interface needed. Now the caller can pass anything with Write.
func WriteHello(w io.Writer) { w.Write([]byte("hi")) }

Consequence: Go interfaces are usually small. One or two methods. The standard library's io.Reader, io.Writer, io.Closer are one method each. sort.Interface is three. When you see a Go codebase with 10-method interfaces, that's a smell.

Tip

If you have a concrete type with 15 methods, don't make a 15-method interface. Make several small interfaces for the behaviors callers actually need. A consumer needing just reading gets io.Reader; one needing both read and close gets io.ReadCloser (a composed interface).

Interface composition

type Reader interface { Read(p []byte) (int, error) }
type Writer interface { Write(p []byte) (int, error) }

type ReadWriter interface {
    Reader
    Writer
}

Embedding interfaces composes their method sets. A ReadWriter is anything that can both read and write.

The empty interface

var anything interface{}  // pre-Go 1.18
var anything any            // Go 1.18+ alias

The empty interface has no methods, so every type satisfies it. It's Go's Any or Python's bare object. Use sparingly — it defeats the type system. Common legitimate uses: heterogeneous JSON decoding, logging context values, reflection-driven code.

Type assertions and type switches

Since an interface value erases the concrete type, you need a way to recover it when needed:

var v any = 42

n := v.(int)      // type assertion. Panics if v isn't an int.
n, ok := v.(int)  // comma-ok: n=42, ok=true. If not int: n=0, ok=false. No panic.

switch x := v.(type) {
case int:
    fmt.Println("int", x)
case string:
    fmt.Println("string", x)
default:
    fmt.Println("other:", x)
}

Type switches are legible and the idiomatic way to handle multiple concrete types behind an interface. You'll see them often in error handling (check errors.As, next module) and in reflection-adjacent code (JSON, logging).

Interfaces and nil — revisiting the trap

An interface value has two words internally: a type descriptor and a data pointer.

var p *MyErr   // p is nil, but has type *MyErr
var e error    // e is nil — both words are zero

e = p           // now e is NOT nil. type=*MyErr, data=nil.
if e == nil {
    // this branch does NOT run
}

The rule — "an interface equals nil only when both its type and data are nil" — is not optional. Every Go engineer learns this; several learn it from a production incident.

Type embedding vs interface embedding — different mechanisms, same syntax

In Lesson 2 you saw struct embedding: put a named type inside another struct as an unnamed field. The outer gets the inner's methods promoted.

Interface embedding (above) is a separate thing: embed an interface inside another interface to compose method sets.

Where they interact: a struct can embed an interface as a field. The struct gets the interface's methods "for free" — dispatched to whatever concrete value fills the interface.

type loggingWriter struct {
    io.Writer   // embedded interface field
}

// loggingWriter.Write delegates to whatever concrete io.Writer is inside.
// We can override just the methods we want:
func (l *loggingWriter) Write(p []byte) (int, error) {
    log.Printf("writing %d bytes", len(p))
    return l.Writer.Write(p)  // explicit delegation
}

This is the standard "decorator" pattern in Go. Middleware for an HTTP handler, a traced gRPC client, a retrying writer — all use this pattern.

Design thinking — where to put the interface

Python/Java instinct: define the interface near the implementation, export it, reuse it.

Go instinct: define the interface near the consumer, minimal to the consumer's needs. Implementers implicitly satisfy it without even importing the consumer.

// Package: pkg/commandsubmitter
type Submitter interface {
    Submit(ctx context.Context, cmd Command) error
}

func DoBusinessLogic(s Submitter, cmd Command) error {
    return s.Submit(ctx, cmd)
}

// Package: pkg/ledgerclient — the concrete gRPC client
type Client struct { /* lots of fields */ }
func (c *Client) Submit(ctx context.Context, cmd Command) error { ... }

// The Client package doesn't import commandsubmitter. It just works.

Tests benefit enormously: to test DoBusinessLogic, you write a fake Submitter in the test file. No mock libraries needed.

Generics — a brief note

Go 1.18+ added type parameters. They're useful but not a replacement for interfaces:

func Max[T cmp.Ordered](a, b T) T {
    if a > b { return a }
    return b
}

Use generics when you want to abstract over types (containers, algorithms). Use interfaces when you want to abstract over behavior. Most of the code you'll write for Canton integrations is behavior-abstracting; interfaces will be your tool.

Takeaways

Module 1's concepts are in place. Now go make them stick by working through the exercises.