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 parameter T with constraint cmp.Ordered
  • cmp.Ordered means T must support <, >, <=, >= operators
  • values []T — the parameter is a slice of whatever type T is
  • 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:

ConstraintMeaning
anyAny type (same as interface{})
comparableTypes that support == and !=
cmp.OrderedTypes 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:

GO-22 Source Code on GitHub

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.