For most of its life, Go didn’t have generics — the ability to write a function or type that works with many types without giving up type safety. To handle “any type”, developers used interface{} (now any), which works but loses type information at compile time.
Go 1.18 (released in 2022) added generics. They’re still a relatively new tool, used carefully and sparingly in the Go community. This lesson teaches you what they are, how to use them, and when not to.
The problem generics solve
Imagine you want a function that returns the largest element in a slice:
func MaxInt(s []int) int {
m := s[0]
for _, v := range s {
if v > m {
m = v
}
}
return m
}
That works for []int. What about []float64? You’d write a near-identical MaxFloat64. And []string? Another copy. The logic is the same; only the type changes.
Before generics, the workaround was to take an []any, run runtime type assertions, and hope nothing crashed. Slow, error-prone, ugly.
Generics let you write the function once, for any type.
Your first generic function
package main
import "fmt"
func Max[T int | float64 | string](s []T) T {
m := s[0]
for _, v := range s {
if v > m {
m = v
}
}
return m
}
func main() {
fmt.Println(Max([]int{3, 1, 4, 1, 5, 9, 2, 6}))
fmt.Println(Max([]float64{1.5, 0.7, 9.1}))
fmt.Println(Max([]string{"banana", "apple", "cherry"}))
}
9
9.1
cherry
Reading the new syntax:
[T int | float64 | string]— the type parameter list.Tis a name for “the type the caller will use.” Theint | float64 | stringis the constraint — what types are allowed.(s []T) T— a normal parameter list, but it usesTinstead of a concrete type- The function body works exactly like a regular function
When you call Max([]int{...}), Go figures out T = int and instantiates the function for that type — checked at compile time, fast at runtime, completely type-safe.
Constraints from golang.org/x/exp/constraints and cmp
Listing types manually (int | float64 | ...) is fine for one-off functions. For common cases, the standard library provides ready-made constraints. The main one you’ll use is cmp.Ordered, which covers any type that supports <, >, ==:
package main
import (
"cmp"
"fmt"
)
func Max[T cmp.Ordered](s []T) T {
m := s[0]
for _, v := range s {
if v > m {
m = v
}
}
return m
}
func main() {
fmt.Println(Max([]int{3, 1, 4}))
fmt.Println(Max([]string{"a", "z", "m"}))
}
4
z
cmp.Ordered means “any type whose values can be compared with <, >, <=, >=.” That covers all integer types, all float types, and strings — exactly what you want for Max. (Complex numbers aren’t ordered, so they’re excluded.)
The comparable constraint
There’s a built-in constraint called comparable for types that support == and !=. Useful for things like deduplication:
package main
import "fmt"
func Unique[T comparable](s []T) []T {
seen := make(map[T]struct{})
result := []T{}
for _, v := range s {
if _, ok := seen[v]; !ok {
seen[v] = struct{}{}
result = append(result, v)
}
}
return result
}
func main() {
fmt.Println(Unique([]int{1, 2, 1, 3, 2, 4}))
fmt.Println(Unique([]string{"a", "b", "a", "c"}))
}
[1 2 3 4]
[a b c]
comparable is required because we’re using T as a map key — and map keys must be comparable.
struct{}{}(an empty anonymous struct) is the Go idiom for “I want a set, not a real value-mapping.” It takes zero memory.
Generic types
You can also define generic types — most often containers like stacks, queues, sets, or trees.
package main
import "fmt"
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 := len(s.items) - 1
v := s.items[last]
s.items = s.items[:last]
return v, true
}
func main() {
s := &Stack[int]{}
s.Push(1)
s.Push(2)
s.Push(3)
for {
v, ok := s.Pop()
if !ok {
break
}
fmt.Println(v)
}
}
3
2
1
Stack[int] is a stack of ints. Stack[string] would be a stack of strings. The type is checked at compile time — you can’t push a string into a Stack[int].
The
var zero Ttrick gives you the zero value of whatever typeTis —0for ints,""for strings,nilfor pointers, etc. It’s the generic way to say “no value yet.”
When to use generics — and when not to
Generics are tempting. They’re also a footgun. Here are some rules of thumb the Go community has settled on:
Good fits for generics:
- Container types — stack, queue, set, tree
- Functions on collections —
Map,Filter,Reduce - Numeric helpers —
Max,Min,Sum,Clamp - Generic comparison helpers
Bad fits for generics:
- “Just in case” — don’t make a function generic before there’s a concrete second type that needs it
- When an interface fits —
io.Readerworks for any reader without generics - When the bodies for different types would actually be different — generics force you to write one body for all of them
A famous line from the Go team: “If you’re not sure whether to use generics, don’t.”
Generics vs interfaces — when each wins
Both let you write code that handles many types. The difference:
- Interfaces describe behavior (“anything with a
Writemethod”) - Generics describe structure (“any type that’s a number”)
If you’d be calling methods on the value, use an interface. If you’d be doing operations like +, <, or storing in a slice, use generics.
Some functions need both. That’s allowed — interfaces can themselves be used as generic constraints.
Course wrap-up
You’ve completed Golang Programming - Fundamentals. To recap what you can now do:
- Write Go programs from scratch with proper structure
- Use the language fundamentals — variables, types, control flow, loops
- Model data with slices, maps, structs, and pointers
- Build reusable code with functions, methods, and interfaces
- Handle errors the Go way and organize code into packages and modules
- Write concurrent programs with goroutines, channels, and synchronization primitives
- Test, benchmark, and document your code
- Reach for reflection and generics when (and only when) you need them
Where to go from here:
- Build something. A CLI tool, a web server, a Discord bot. Real practice cements these concepts.
- Read real Go code. Browse the standard library on pkg.go.dev/std, or open-source Go projects on GitHub.
- The official tour. tour.golang.org is a fantastic interactive walk through the language.
- Effective Go. go.dev/doc/effective_go — a short, opinionated guide to writing idiomatic Go.
Thanks for going through the course. Welcome to Go.