When should you use generics vs. an interface in Go?

5 minadvancedgogenericsinterfacesdesign

Quick Answer

Use an interface when you want to abstract over behavior — different types that do the same kind of thing but implement it differently, like io.Writer having many different concrete writers. Use generics when you want to abstract over data while keeping a single implementation, like a Stack[T] or a Map/Filter function that works identically regardless of the element type, just with different concrete types plugged in. A practical signal: if you find yourself writing a type switch inside a generic-feeling function to handle different types differently, that's often a sign you actually want an interface instead, since generics work best when the logic stays exactly the same across all type arguments, not when it needs to branch per type.

Detailed Answer

Both tools abstract over "more than one type," but they solve genuinely different problems, and picking the wrong one leads to awkward code.

Interfaces: same behavior, different implementations

type Storage interface {
    Save(key string, data []byte) error
    Load(key string) ([]byte, error)
}

type S3Storage struct{ /* ... */ }
type LocalDiskStorage struct{ /* ... */ }

S3Storage and LocalDiskStorage do fundamentally different things internally to satisfy the same contract. An interface is the right tool because you want to swap behavior while keeping the caller's code identical.

Generics: same logic, different data

func Filter[T any](items []T, keep func(T) bool) []T {
    var result []T
    for _, v := range items {
        if keep(v) {
            result = append(result, v)
        }
    }
    return result
}

evens := Filter([]int{1, 2, 3, 4}, func(n int) bool { return n%2 == 0 })
adults := Filter(people, func(p Person) bool { return p.Age >= 18 })

Filter's logic never changes based on the element type. Only the data flowing through it changes, which is exactly the shape generics fit best.

The warning sign you picked the wrong tool

func Process[T any](v T) {
    switch x := v.(type) {  // if you're doing this inside a "generic" function...
    case int:
        // ...
    case string:
        // ...
    }
    // ...you probably wanted an interface with per-type methods instead
}

Type-switching inside a generic function usually means the behavior genuinely differs per type, which is what interfaces (with each type implementing its own version of a method) are built for. Generics shine when one code path works, unmodified, across every type you plug in.

The short version

Interface: "many types, many behaviors, one contract." Generics: "many types, one behavior, reused code."

Related Resources