Generics (Type Parameters)

Reviewed & published by Brayan K

Added in Go 1.18, generics let you write one function or type that works across many types — without losing Go's type safety. By the end you'll write generic functions with type parameters, constrain them with interfaces, and build a fully generic data structure.

Part of the free Go course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

📊 Generics Vocabulary

TermMeaning
[T any]A type parameter named T, allowing any type
constraintAn interface limiting which types T may be
anyThe widest constraint — same as interface{}
comparableBuilt-in constraint for types usable with ==
~intint and any type whose underlying type is int

Type inference usually lets you call a generic function without spelling out the type: Max(3, 7) instead of Max[int](3, 7).

1️⃣ Generic Functions

A type parameter goes in square brackets after the function name: func Max[T ...](a, b T) T. Inside the function, T stands for whatever concrete type the caller uses. The list int | float64 | string is a quick inline constraint saying which types are allowed — here, the ones > works on.

package main

import "fmt"

// A type parameter [T any] lets ONE function work with ANY type.
// Before generics you'd write Min for int, Max for int, again for
// float64... Now you write it once.
func Max[T int | float64 | string](a, b T) T {
    if a > b {
        return a
    }
    return b
}

func main() {
    fmt.Println(Max(3, 7))           // int:    7
    fmt.Println(Max(2.5, 1.5))       // float64: 2.5
    fmt.Println(Max("apple", "pear")) // string: pear
}

A function can have several type parameters. Map below takes an input type T and a possibly-different output type U — the staple of functional-style slice transforms.

package main

import "fmt"

// Map applies fn to every element, returning a new slice. T is the input
// type, U is the output type — they can differ.
func Map[T, U any](s []T, fn func(T) U) []U {
    out := make([]U, len(s))
    for i, v := range s {
        out[i] = fn(v)
    }
    return out
}

func main() {
    nums := []int{1, 2, 3, 4}

    doubled := Map(nums, func(n int) int { return n * 2 })
    fmt.Println(doubled)             // [2 4 6 8]

    labels := Map(nums, func(n int) string { return fmt.Sprintf("#%d", n) })
    fmt.Println(labels)              // [#1 #2 #3 #4]
}

Your turn. Fill in the constraint that allows any element type so First works on both slices.

package main

import "fmt"

// 🎯 YOUR TURN — make First work for ANY type by adding a type parameter.
func First[T ___](s []T) T {       // 👉 any
    return s[0]
}

func main() {
    fmt.Println(First([]int{10, 20, 30}))       // 10
    fmt.Println(First([]string{"a", "b", "c"})) // a
}

2️⃣ Constraints

A constraint is just an interface that lists the permitted types (joined with |) and/or required methods. Naming it makes intent clear and reusable. The ~ token matters: ~int means "int or any named type whose underlying type is int", so your own type Celsius int still qualifies.

package main

import "fmt"

// A constraint is an interface listing the allowed types. The | means
// "or". This Number constraint permits the common numeric types.
type Number interface {
    ~int | ~int64 | ~float64
}

// Sum works for any type that satisfies Number.
func Sum[T Number](nums []T) T {
    var total T   // zero value of T
    for _, n := range nums {
        total += n
    }
    return total
}

func main() {
    fmt.Println(Sum([]int{1, 2, 3}))         // 6
    fmt.Println(Sum([]float64{1.5, 2.5}))    // 4
}

3️⃣ Generic Types

Types can take type parameters too. Stack[T any] is a stack of any element type, and its methods reuse the same T. To produce the zero value of an unknown type inside a method, declare var zero T — that's the idiomatic way to return "nothing" when the type isn't known until instantiation.

package main

import "fmt"

// Types can be generic too. Stack[T] is a stack of any element type.
type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(v T) {
    s.items = append(s.items, v)
}

func (s *Stack[T]) Pop() (T, bool) {
    var zero T
    if len(s.items) == 0 {
        return zero, false
    }
    last := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return last, true
}

func main() {
    var s Stack[string]
    s.Push("a")
    s.Push("b")

    v, ok := s.Pop()
    fmt.Println(v, ok)   // b true
    v, ok = s.Pop()
    fmt.Println(v, ok)   // a true
    v, ok = s.Pop()
    fmt.Println(v, ok)   // "" false (empty)
}

🧩 Reorder Challenge

These lines define a generic Contains function and call it. Put them in the correct order:

A.     return false
B. func Contains[T comparable](s []T, target T) bool {
C.         if v == target { return true }
D.     for _, v := range s {
E.     }
F. }
G. fmt.Println(Contains([]int{1, 2, 3}, 2))

B, D, C, E, A, F, G. Open the function with a comparable constraint (needed for ==), loop over the slice checking each element, close the loop, return false if nothing matched, close the function, then call it. comparable is required because the body uses ==.

🧠 Quick Recall

Predict the output before revealing the answer.

func Max[T int | float64](a, b T) T {
    if a > b { return a }
    return b
}
fmt.Println(Max(4, 9))

9 — Go infers T = int from the arguments, so no explicit [int] is needed, and 9 > 4.

2. Does this compile?

func Eq[T any](a, b T) bool {
    return a == b
}

No. any doesn't guarantee == works (slices, maps, and funcs aren't comparable). Change the constraint to comparable and it compiles.

3. What does this print?

func Zero[T any]() T {
    var z T
    return z
}
fmt.Println(Zero[int](), Zero[string]() == "")

0 true — var z T is the zero value of whatever T is: 0 for int, and the empty string for string (so the comparison to "" is true).

Pro Tips

Common Errors (and the fix)

Frequently Asked Questions

Q: Generics or interfaces — which should I use?

Use an interface when you want different types to share behaviour behind a contract. Use generics when you want the same logic to operate on many types while keeping the exact type (so the compiler still checks it). Containers and slice utilities are the classic generics case.

Q: What is comparable?

A built-in constraint for types that can be compared with == and != — most types, but not slices, maps, or functions. Use it for map keys and equality checks in generic code.

Q: Why does ~int have a tilde?

Without ~, the constraint matches the exact type int only. With ~int, it also matches any type whose underlying type is int, such as type Age int. That makes constraints work with user-defined named types.

Q: Do generics slow my program down?

Generics are resolved at compile time, so there's no runtime type-checking cost like reflection. Go may share generated code across types in some cases, but for everyday use you can treat generics as zero-overhead abstractions.

Mini-Challenge: Generic Filter

No blanks — just a brief. Write a single Filter that works on any slice type, then use it on both ints and strings. Together with Map from earlier, you now have the building blocks of a functional toolkit.

package main

import "fmt"

func main() {
    // 🎯 MINI-CHALLENGE: a generic Filter
    // 1. Write Filter[T any](s []T, keep func(T) bool) []T that returns a
    //    new slice containing only the elements where keep(v) is true.
    // 2. Use it on []int{1,2,3,4,5,6} to keep the even numbers.
    // 3. Use it on []string{"go","rust","c","python"} to keep names
    //    longer than 2 characters.
    //
    // ✅ Expected output:
    //    [2 4 6]
    //    [go rust python]

    // your code here
}

🎉 Lesson Complete!

Practice quiz

In which Go version were generics added?

  • 1.11
  • 1.16
  • 1.18
  • 1.21

Answer: 1.18. Generics arrived in Go 1.18.

What does [T any] declare?

  • a type parameter T allowing any type
  • a variable named T
  • an interface method
  • a constant

Answer: a type parameter T allowing any type. [T any] is a type parameter named T that accepts any type.

In func Map[T, U any](s []T, fn func(T) U) []U, T and U are...

  • always the same type
  • both int
  • return values
  • input and output types that can differ

Answer: input and output types that can differ. T is the input element type and U the output type; they may be different.

What does fmt.Println(Max(3, 7)) print for a generic Max?

  • 3
  • 7
  • 10
  • a compile error

Answer: 7. Go infers T = int and Max returns the larger, 7.

What does a constraint like ~int | ~int64 | ~float64 express?

  • the set of allowed types (the | means or)
  • a list of methods
  • a tuple
  • a struct

Answer: the set of allowed types (the | means or). It's a type set; | means 'or', listing the permitted underlying types.

What is the widest constraint, equivalent to interface{}?

  • comparable
  • Ordered
  • any
  • Number

Answer: any. any is the widest constraint and is an alias for interface{}.

Which built-in constraint is needed to use == in generic code?

  • any
  • comparable
  • Number
  • Stringer

Answer: comparable. comparable permits == and !=; not all types (slices, maps, funcs) qualify.

Inside a generic method, what does var zero T produce?

  • nil always
  • a panic
  • the first element
  • the zero value of T

Answer: the zero value of T. var zero T is the zero value of whatever type T is instantiated as.

Why does ~int use a tilde?

  • to negate int
  • to also match named types whose underlying type is int
  • to make it a pointer
  • for performance

Answer: to also match named types whose underlying type is int. Without ~ only the exact int matches; ~int also admits type Age int.

Do generics add runtime type-checking overhead like reflection?

  • yes, always
  • only for slices
  • no, they are resolved at compile time
  • only with comparable

Answer: no, they are resolved at compile time. Generics are compile-time, so there's no reflection-style runtime cost.

Continue this course

Related lessons