When should you use generics vs. an interface in Go?
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."