Go Interfaces

Implicit Satisfaction

Interfaces are satisfied implicitly — no "implements" keyword
type Writer interface {
    Write(p []byte) (n int, err error)
}

// Any type with this method satisfies Writer
type LogSink struct {
    entries []string
}

func (l *LogSink) Write(p []byte) (int, error) {
    l.entries = append(l.entries, string(p))
    return len(p), nil
}

// LogSink is now a Writer — no registration needed
var w Writer = &LogSink{}

This is structural typing: if a type has all the methods an interface requires, it satisfies the interface. The implementing type does not need to import the package that defines the interface.

Key Standard Library Interfaces

The small interfaces that power Go
// io.Reader — reads bytes from a source
type Reader interface {
    Read(p []byte) (n int, err error)
}

// io.Writer — writes bytes to a destination
type Writer interface {
    Write(p []byte) (n int, err error)
}

// fmt.Stringer — custom string representation
type Stringer interface {
    String() string
}

// error — yes, it is an interface
type error interface {
    Error() string
}

Go’s power comes from composing small interfaces. io.Reader connects files, network sockets, compression, encryption, and HTTP bodies — any type with Read works with all of them.

Interface Composition

Combining interfaces
type ReadWriter interface {
    Reader
    Writer
}

type ReadWriteCloser interface {
    Reader
    Writer
    io.Closer
}

Compose larger interfaces from smaller ones. Prefer accepting the smallest interface your function actually needs.

Type Assertions & Switches

Extract the concrete type from an interface
var r io.Reader = os.Stdin

// Safe assertion — check ok
f, ok := r.(*os.File)
if !ok {
    log.Fatal("not a file")
}

// Type switch — branch on concrete type
switch v := r.(type) {
case *os.File:
    fmt.Println("file:", v.Name())
case *bytes.Buffer:
    fmt.Printf("buffer: %d bytes\n", v.Len())
default:
    fmt.Println("unknown reader")
}

The two-value form v, ok := r.(Type) never panics — use this in production code. Type switches are the clean way to handle multiple possible concrete types.

The Empty Interface

any (alias for interface{}) — accept anything
// Go 1.18+ — any is an alias for interface{}
func printAnything(v any) {
    fmt.Printf("%T: %v\n", v, v)
}

any accepts every type but you lose compile-time type safety. Prefer generics (Go 1.18+) or concrete interfaces when possible.

Design Principle

Accept interfaces, return structs
// Good — accepts the narrowest interface needed
func CountLines(r io.Reader) (int, error) {
    scanner := bufio.NewScanner(r)
    count := 0
    for scanner.Scan() {
        count++
    }
    return count, scanner.Err()
}

// Callers pass anything that implements io.Reader
n, _ := CountLines(os.Stdin)
n, _ := CountLines(resp.Body)
n, _ := CountLines(bytes.NewReader(data))

Accept the smallest interface your function needs, return a concrete type. The caller decides how to supply the dependency.