What an interface is

An interface is a set of method signatures. Any type that has all of those methods satisfies the interface — automatically, without declaring intent. This is called structural (or implicit) typing.

type Stringer interface {
    String() string
}

type Money struct{ Cents int }
func (m Money) String() string {
    return fmt.Sprintf("$%d.%02d", m.Cents/100, m.Cents%100)
}

// Money now satisfies fmt.Stringer with no `implements` keyword.

fmt.Println checks for fmt.Stringer at runtime via a type assertion and will call String() if present.

Small interfaces win

The Go standard library is full of one- or two-method interfaces, e.g.

type Reader interface {
    Read(p []byte) (n int, err error)
}

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

type Closer interface {
    Close() error
}

and combinations:

type ReadWriter interface {
    Reader
    Writer
}

The rule of thumb is: define interfaces in the consuming package, not next to the type that implements them. The consumer knows which methods it needs.

Satisfying an interface

type Speaker interface {
    Speak() string
}

type Dog struct{ Name string }
func (d Dog) Speak() string { return d.Name + ": woof" }

type Cat struct{ Name string }
func (c Cat) Speak() string { return c.Name + ": meow" }

animals := []Speaker{Dog{"Rex"}, Cat{"Mia"}}
for _, a := range animals {
    fmt.Println(a.Speak())
}

Pointer vs value receivers and interface satisfaction

If a method is defined on *T, then *T satisfies the interface but T does not:

type Greeter interface { Greet() }

type P struct{ Name string }
func (p *P) Greet() { fmt.Println("hi", p.Name) }

var g Greeter
g = P{"x"}    // COMPILE ERROR: P does not satisfy Greeter
g = &P{"x"}   // OK

This is one of the most common stumbling blocks. The compiler error message spells it out.

Empty interface and any

interface{} (alias any since Go 1.18) is satisfied by every type:

var x any = 42
x = "hello"
x = []int{1, 2}

You'll see any in JSON decoding (map[string]any), generic-ish containers, and fmt.Println(args ...any). Prefer concrete types or generics where you can; any defers type checking to runtime.

Type assertions

var x any = "hello"

s := x.(string)        // panics if x is not a string
s, ok := x.(string)    // safe form: ok=false on mismatch

Type switches

func describe(x any) string {
    switch v := x.(type) {
    case nil:
        return "nil"
    case int:
        return fmt.Sprintf("int %d", v)
    case string:
        return fmt.Sprintf("string %q", v)
    case fmt.Stringer:
        return "stringer: " + v.String()
    default:
        return fmt.Sprintf("other %T", v)
    }
}

Errors

error is a built-in interface:

type error interface {
    Error() string
}

Any type with an Error() string method is an error.

Sentinel errors

var ErrNotFound = errors.New("not found")

Compared with errors.Is(err, ErrNotFound).

Custom error types

type ValidationError struct {
    Field, Reason string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation: %s: %s", e.Field, e.Reason)
}

Extracted with errors.As:

var ve *ValidationError
if errors.As(err, &ve) {
    fmt.Println("bad field:", ve.Field)
}

Wrapping with %w

return fmt.Errorf("load config: %w", err)

Composing behaviour with embedded interfaces

type readWriter struct {
    io.Reader
    io.Writer
}

The struct now satisfies io.Reader, io.Writer, and any combination, delegating to the embedded values.

Interface design checklist

  • Keep interfaces small (1–3 methods).
  • Define them in the consumer package, not the producer.
  • Don't return interfaces from constructors; return concrete types so callers see the full API.
  • Accept interfaces, return structs (a common slogan).

Run the example

go run ./part1-language/examples/07_interfaces