In the previous tutorial, you learned API best practices. Now let’s explore one of Go’s most powerful features — generics.
Before Go 1.18, you had two choices for writing reusable code: use interface{} (losing type safety) or write the same function for every type. Generics solve this problem. You write a function once, and it works with any type that meets your constraints.
The Problem Without Generics
Imagine you need a function to find the minimum value in a slice. Without generics, you write one function per type:
func MinInt(values []int) int {
min := values[0]
for _, v := range values[1:] {
if v < min {
min = v
}
}
return min
}
func MinFloat64(values []float64) float64 {
min := values[0]
for _, v := range values[1:] {
if v < min {
min = v
}
}
return min
}
func MinString(values []string) string {
min := values[0]
for _, v := range values[1:] {
if v < min {
min = v
}
}
return min
}
Three functions that do the exact same thing. With generics, you write it once.
Your First Generic Function
package main
import (
"cmp"
"fmt"
)
func Min[T cmp.Ordered](values []T) T {
min := values[0]
for _, v := range values[1:] {
if v < min {
min = v
}
}
return min
}
func main() {
fmt.Println(Min([]int{5, 3, 8, 1, 9})) // 1
fmt.Println(Min([]float64{3.14, 2.71, 1.41})) // 1.41
fmt.Println(Min([]string{"banana", "apple", "cherry"})) // apple
}
Let’s break down func Min[T cmp.Ordered](values []T) T:
[T cmp.Ordered]— type parameterTwith constraintcmp.Orderedcmp.OrderedmeansTmust support<,>,<=,>=operatorsvalues []T— the parameter is a slice of whatever typeTis- The return type is also
T
Go infers the type from the arguments. You can also specify it explicitly:
result := Min[int]([]int{5, 3, 8})
Type Constraints
A constraint tells Go what operations a type parameter supports. You define constraints as interfaces:
package main
import "fmt"
// Custom constraint: any type that can be added
type Addable interface {
~int | ~int32 | ~int64 | ~float32 | ~float64 | ~string
}
func Sum[T Addable](values []T) T {
var total T
for _, v := range values {
total += v
}
return total
}
func main() {
fmt.Println(Sum([]int{1, 2, 3, 4, 5})) // 15
fmt.Println(Sum([]float64{1.1, 2.2, 3.3})) // 6.6
fmt.Println(Sum([]string{"Hello", " ", "World"})) // Hello World
}
The ~int syntax means “any type whose underlying type is int.” This includes custom types like type UserID int.
Built-in Constraints
Go provides useful built-in constraints. Since Go 1.21, the cmp package in the standard library has everything you need:
| Constraint | Meaning |
|---|---|
any | Any type (same as interface{}) |
comparable | Types that support == and != |
cmp.Ordered | Types that support <, >, <=, >= |
package main
import "fmt"
// comparable constraint: supports == and !=
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
func main() {
fmt.Println(Contains([]int{1, 2, 3}, 2)) // true
fmt.Println(Contains([]string{"a", "b", "c"}, "d")) // false
}
Generic Types
You can create generic structs, not just functions:
package main
import "fmt"
// A generic stack
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, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
func (s *Stack[T]) Peek() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
return s.items[len(s.items)-1], true
}
func (s *Stack[T]) Size() int {
return len(s.items)
}
func main() {
// Stack of ints
intStack := Stack[int]{}
intStack.Push(10)
intStack.Push(20)
intStack.Push(30)
val, _ := intStack.Pop()
fmt.Println(val) // 30
// Stack of strings
strStack := Stack[string]{}
strStack.Push("hello")
strStack.Push("world")
top, _ := strStack.Peek()
fmt.Println(top) // world
fmt.Println(strStack.Size()) // 2
}
Notice: when you declare a variable of a generic type, you must specify the type parameter: Stack[int]{}. Go cannot infer type parameters for types — only for functions.
Useful Generic Patterns
Map, Filter, Reduce
These functional patterns work great with generics:
package main
import "fmt"
func Map[T any, U any](slice []T, f func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = f(v)
}
return result
}
func Filter[T any](slice []T, f func(T) bool) []T {
var result []T
for _, v := range slice {
if f(v) {
result = append(result, v)
}
}
return result
}
func Reduce[T any, U any](slice []T, initial U, f func(U, T) U) U {
result := initial
for _, v := range slice {
result = f(result, v)
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5}
// Double each number
doubled := Map(numbers, func(n int) int { return n * 2 })
fmt.Println(doubled) // [2 4 6 8 10]
// Keep only even numbers
evens := Filter(numbers, func(n int) bool { return n%2 == 0 })
fmt.Println(evens) // [2 4]
// Sum all numbers
sum := Reduce(numbers, 0, func(acc, n int) int { return acc + n })
fmt.Println(sum) // 15
// Convert ints to strings
words := Map(numbers, func(n int) string {
return fmt.Sprintf("item_%d", n)
})
fmt.Println(words) // [item_1 item_2 item_3 item_4 item_5]
}
Result Type
A generic Result type is useful for functions that can fail:
package main
import (
"errors"
"fmt"
"strconv"
)
type Result[T any] struct {
Value T
Err error
}
func Ok[T any](value T) Result[T] {
return Result[T]{Value: value}
}
func Fail[T any](err error) Result[T] {
return Result[T]{Err: err}
}
func (r Result[T]) IsOk() bool {
return r.Err == nil
}
func (r Result[T]) Unwrap() T {
if r.Err != nil {
panic("called Unwrap on error result: " + r.Err.Error())
}
return r.Value
}
func parseAge(input string) Result[int] {
age, err := strconv.Atoi(input)
if err != nil {
return Fail[int](errors.New("invalid age: " + input))
}
if age < 0 || age > 150 {
return Fail[int](errors.New("age out of range"))
}
return Ok(age)
}
func main() {
result := parseAge("25")
if result.IsOk() {
fmt.Println("Age:", result.Unwrap()) // Age: 25
}
result = parseAge("abc")
if !result.IsOk() {
fmt.Println("Error:", result.Err) // Error: invalid age: abc
}
}
This pattern is inspired by Rust’s Result<T, E> type. It is not idiomatic Go (Go prefers multiple return values), but it shows the power of generics.
Generic Map (Dictionary)
A type-safe ordered map:
package main
import "fmt"
type Pair[K comparable, V any] struct {
Key K
Value V
}
type OrderedMap[K comparable, V any] struct {
pairs []Pair[K, V]
}
func (m *OrderedMap[K, V]) Set(key K, value V) {
for i, p := range m.pairs {
if p.Key == key {
m.pairs[i].Value = value
return
}
}
m.pairs = append(m.pairs, Pair[K, V]{Key: key, Value: value})
}
func (m *OrderedMap[K, V]) Get(key K) (V, bool) {
for _, p := range m.pairs {
if p.Key == key {
return p.Value, true
}
}
var zero V
return zero, false
}
func (m *OrderedMap[K, V]) Keys() []K {
keys := make([]K, len(m.pairs))
for i, p := range m.pairs {
keys[i] = p.Key
}
return keys
}
func main() {
m := OrderedMap[string, int]{}
m.Set("first", 1)
m.Set("second", 2)
m.Set("third", 3)
val, ok := m.Get("second")
fmt.Println(val, ok) // 2 true
fmt.Println(m.Keys()) // [first second third]
}
Multiple Type Parameters
Functions and types can have multiple type parameters:
package main
import "fmt"
func Zip[T any, U any](a []T, b []U) []Pair[T, U] {
length := len(a)
if len(b) < length {
length = len(b)
}
result := make([]Pair[T, U], length)
for i := 0; i < length; i++ {
result[i] = Pair[T, U]{Key: a[i], Value: b[i]}
}
return result
}
type Pair[T any, U any] struct {
Key T
Value U
}
func main() {
names := []string{"Alex", "Sam", "Jordan"}
scores := []int{95, 87, 92}
pairs := Zip(names, scores)
for _, p := range pairs {
fmt.Printf("%s: %d\n", p.Key, p.Value)
}
// Alex: 95
// Sam: 87
// Jordan: 92
}
When to Use Generics vs Interfaces
This is the most common question about Go generics. Here is a simple guide:
Use generics when:
- You need type-safe collections (Stack, Queue, Set)
- You write utility functions that work on many types (Map, Filter, Contains)
- You want to avoid type assertions and
interface{} - Performance matters (generics avoid boxing/unboxing overhead)
Use interfaces when:
- You define behavior (Reader, Writer, Handler)
- You need polymorphism (different types, same method)
- You work with dependency injection
- The types have methods you want to call
// Use generics: same logic, different types
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
// Use interfaces: different behavior
type Logger interface {
Log(message string)
}
A good rule: if you need operators (<, >, ==, +), use generics. If you need methods, use interfaces.
Common Mistakes
1. Overusing generics.
Not every function needs generics. If a function only works with one type, do not make it generic. Simple code is better than clever code.
// Bad — unnecessary generics
func PrintUser[T User](u T) { ... }
// Good — just use the type
func PrintUser(u User) { ... }
2. Forgetting that type inference does not work for types.
// Error: cannot infer type parameter
stack := Stack{} // Missing type parameter
// Good: specify the type
stack := Stack[int]{}
3. Using any when a stricter constraint would be better.
// Bad — any allows types that cannot be sorted
func Sort[T any](s []T) { ... }
// Good — only sortable types
func Sort[T cmp.Ordered](s []T) { ... }
Source Code
You can find the complete source code for this tutorial on GitHub:
Related Articles
- Go Tutorial #21: API Best Practices — Validation, rate limiting, logging
- Go Tutorial #8: Interfaces and Polymorphism — Go interfaces
- Go Tutorial #6: Arrays, Slices, and Maps — Built-in collection types
What’s Next?
In the next tutorial, Go Tutorial #23: Building CLI Tools with Cobra, you will learn:
- What Cobra is and why it powers Docker, kubectl, and Hugo
- Building commands, subcommands, and flags
- Configuration with Viper
- Building a complete TODO CLI app
This is part 22 of the Go Tutorial series. Follow along to learn Go from scratch.