Generics

intermediate go stdlib

Generics let us write one function or type that works for many types, instead of copy-pasting the same logic for int, string, float64, and so on.

They landed in Go 1.18 — a huge addition the community had wanted for over a decade.

In simple language, generics let us say “this function works for any type T, and the compiler fills in the real type at compile time.”

The problem they solve

Before generics, a Max function needed one copy per type. Painful.

func MaxInt(a, b int) int       { if a > b { return a }; return b }
func MaxFloat(a, b float64) float64 { if a > b { return a }; return b }
// ...and so on, forever

With generics it’s one function:

import "golang.org/x/exp/constraints"

// T is a type parameter, constrained to ordered types (things we can compare with >)
func Max[T constraints.Ordered](a, b T) T {
	if a > b {
		return a
	}
	return b
}

func main() {
	fmt.Println(Max(3, 5))       // works with int
	fmt.Println(Max("a", "b"))   // works with string
	fmt.Println(Max(1.5, 0.5))   // works with float64
}

The [T constraints.Ordered] part is the type parameter list. T is a placeholder, and constraints.Ordered says “T must be a type that supports <, >, etc.” The compiler usually infers T from the arguments, so we just call Max(3, 5) normally.

Constraints — the rules on T

A constraint says what a type parameter is allowed to be. We can’t just do a > b on any type — only on comparable/ordered ones. Constraints enforce that. There are a few sources:

  • comparable — a built-in constraint for types that support == and != (needed for map keys, equality checks).
  • The constraints package (golang.org/x/exp/constraints) — ready-made ones like Ordered, Integer, Float, Signed.
  • any — the loosest constraint, an alias for interface{}. Means “literally any type.”
  • Custom interface constraints — we write our own.
// a custom constraint: T must be one of these number types
type Number interface {
	~int | ~int64 | ~float64 // | means "union", ~ means "any type based on this"
}

func Sum[T Number](nums []T) T {
	var total T
	for _, n := range nums {
		total += n
	}
	return total
}

The ~int (with a tilde) means “int or any type whose underlying type is int” — so a custom type Celsius int still qualifies. Without the ~ it would only match int exactly.

Generic types, not just functions

Structs can be generic too. A type-safe stack that holds any type:

type Stack[T any] struct {
	items []T
}

func (s *Stack[T]) Push(item T) { s.items = append(s.items, item) }
func (s *Stack[T]) Pop() T {
	last := s.items[len(s.items)-1]
	s.items = s.items[:len(s.items)-1]
	return last
}

// usage — we specify the type once: Stack[string]
var s Stack[string]
s.Push("hi")

Now Stack[int] and Stack[string] are separate, fully type-checked types. No interface{} casting, no runtime surprises.

Generics vs interfaces vs any — when to use what

This is the real interview question. Here’s the mental model:

Generics
Same logic across many types, keep type safety. e.g. Max, Map, Stack[T]
Interfaces
Different types share behavior. e.g. io.Writer, fmt.Stringer
any
Truly don't care about type, willing to type-assert. Last resort.
  • Generics — when the same algorithm runs over different types and we want to keep static type safety. The function body doesn’t care what T is.
  • Interfaces — when different types provide different behavior behind a shared method set. The whole point is polymorphism (different code per type).
  • any — when we genuinely don’t care about the type and accept losing type safety (and paying for runtime type assertions).

The community caution: don’t overuse them

The Go team’s own advice: reach for generics last, not first. They make code harder to read and were deliberately kept minimal.

In simple language — if a plain interface or a concrete type does the job, use that. Generics earn their place only when we’d otherwise copy-paste the exact same logic for several types (containers, and utilities like Map/Filter/Max). Sprinkling [T any] everywhere is a code smell.

Interview soundbite

Generics arrived in Go 1.18 and let us write one function or type parameterized over types, like func Max[T constraints.Ordered](a, b T) T. Constraints (comparable, the constraints package, or custom interface unions with ~ and |) restrict what T can be. Use generics when the same logic spans many types, interfaces when types share different behavior, and any only as a last resort. The Go team’s guidance is explicit: don’t overuse them.