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
- Add a type parameter [T any] to a function
- Let Go infer the type from the arguments
- Transform slices with generic Map and Filter
- Write constraints that list the allowed types
- Use the ~ token for underlying-type matching
- Define generic types like Stack[T]
- Get the zero value of a type parameter with var zero T
📊 Generics Vocabulary
| Term | Meaning |
|---|---|
| [T any] | A type parameter named T, allowing any type |
| constraint | An interface limiting which types T may be |
| any | The widest constraint — same as interface{} |
| comparable | Built-in constraint for types usable with == |
| ~int | int 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
- 💡 Don't reach for generics first. If a plain function or an interface already does the job clearly, use that. Generics shine for containers and slice/map utilities.
- 💡 Use comparable when you need ==, and a numeric constraint when you need + or <. The constraint must permit every operation the body uses.
- 💡 Lean on type inference. Spell out [int] only when Go can't figure the type out (for example, a generic function with no arguments of type T).
- 💡 Check the standard library — slices and maps packages already provide generic helpers like slices.Contains and slices.Sort.
Common Errors (and the fix)
- "invalid operation: a == b (incomparable types in type set)" — your constraint is any but you used ==. Use comparable instead.
- "T does not support < (operator not defined)" — the constraint doesn't allow ordering. Add a numeric/ordered constraint such as int | float64 | string.
- "cannot infer T" — Go couldn't deduce the type from the arguments. Provide it explicitly: MyFunc[int](...).
- Forgetting ~ — int in a constraint matches only int exactly; your type ID int won't qualify. Use ~int to include defined types.
- "missing type constraint" — you wrote [T] with no constraint. Every type parameter needs one; use [T any] if anything goes.
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!
- ✅ Type parameters [T any] let one function serve many types
- ✅ Go usually infers the type, so calls stay clean
- ✅ Constraints are interfaces listing allowed types (with |) or methods
- ✅ ~int matches defined types; comparable enables ==
- ✅ Types can be generic too — like Stack[T] — and var zero T gives the zero value
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
- Previous: Error Wrapping
- Next: Working with JSON — Marshal and Unmarshal structs with struct tags