Skip to content

Go Generics — Type Parameters for Reusable Go Functions and Types

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Go Generics. We cover key concepts, practical examples, and best practices to help you master this topic.

Go generics provide type parameters for functions and types with constraint interfaces allowing type-safe reusable code without runtime type assertions.

What You'll Learn

  • Generic functions with type parameters
  • Generic types and methods
  • Type constraints with interfaces
  • Performance considerations

Why It Matters

Generics reduce boilerplate. DodaZIP uses generics for collection utilities. Go 1.18+ generics enable type-safe data structures and algorithms.

Real-World Use

Collection libraries, data processing pipelines, caching layers, validation frameworks.

flowchart LR
    A["Generics"] --> B["Functions"]
    A --> C["Types"]
    A --> D["Constraints"]
    A --> E["Interfaces"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Generic Function

func Min[T constraints.Ordered](a, b T) T {
    if a < b { return a }
    return b
}

func main() {
    fmt.Println(Min(3, 5))
    fmt.Println(Min(2.5, 1.5))
    fmt.Println(Min("alpha", "beta"))
}

Generic Type

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 main() {
    stack := Stack[int]{}
    stack.Push(1)
    stack.Push(2)
    stack.Push(3)
    for {
        val, ok := stack.Pop()
        if !ok { break }
        fmt.Println(val)
    }
}

Generic Map/Filter/Reduce

func Map[T, U any](items []T, fn func(T) U) []U {
    result := make([]U, len(items))
    for i, item := range items {
        result[i] = fn(item)
    }
    return result
}

func Filter[T any](items []T, fn func(T) bool) []T {
    var result []T
    for _, item := range items {
        if fn(item) {
            result = append(result, item)
        }
    }
    return result
}

func Reduce[T, U any](items []T, initial U, fn func(U, T) U) U {
    result := initial
    for _, item := range items {
        result = fn(result, item)
    }
    return result
}

func main() {
    nums := []int{1, 2, 3, 4, 5}
    doubled := Map(nums, func(n int) int { return n * 2 })
    fmt.Println(doubled)

    evens := Filter(nums, func(n int) bool { return n%2 == 0 })
    fmt.Println(evens)

    sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
    fmt.Println(sum)
}

Custom Constraints

type Number interface {
    ~int | ~float64
}

func Sum[T Number](items []T) T {
    var sum T
    for _, item := range items {
        sum += item
    }
    return sum
}

type Stringer interface {
    String() string
    ~struct{}
}

func Print[T Stringer](items []T) {
    for _, item := range items {
        fmt.Println(item.String())
    }
}

Generic Cache

type Cache[K comparable, V any] struct {
    mu    sync.Mutex
    data  map[K]V
}

func NewCache[K comparable, V any]() *Cache[K, V] {
    return &Cache[K, V]{data: make(map[K]V)}
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
    c.mu.Lock()
    defer c.mu.Unlock()
    val, ok := c.data[key]
    return val, ok
}

func (c *Cache[K, V]) Set(key K, value V) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = value
}

func (c *Cache[K, V]) Delete(key K) {
    c.mu.Lock()
    defer c.mu.Unlock()
    delete(c.data, key)
}

Common Mistakes

1. Not Using Type Inference

// Go can often infer type parameters
Min(3, 5)        // OK: inferred
Min[int](3, 5)   // OK: explicit

2. Constraint Without Tilde

type MyInt int
// func Sum[T Number](items []T) T { }
// Without ~, MyInt doesn't satisfy Number

3. Using any When Constraints Help

any is the most permissive constraint but provides no type safety. Use specific constraints.

4. Generic Overuse

Don't use generics where interface or Code Generation is simpler. Generics add complexity.

5. Performance Assumptions

Generics are compiled per type, not boxed. Usually zero overhead, but more compilation.

Practice Questions

1. What is the any constraint? any is equivalent to interface{}. The most permissive constraint.

2. What does comparable constraint provide? Types that support == and !=. Required for map keys.

3. What is the tilde (~) in constraints? ~int includes int and any type whose underlying type is int. Without tilde, only the exact type matches.

4. Can methods have type parameters? Methods cannot have additional type parameters beyond the receiver's type parameters.

Challenge: Implement a generic Binary Search Tree with Insert and Search methods.

Solution
type BST[T constraints.Ordered] struct {
    Value T
    Left  *BST[T]
    Right *BST[T]
}

func (b *BST[T]) Insert(value T) *BST[T] {
    if b == nil { return &BST[T]{Value: value} }
    if value < b.Value { b.Left = b.Left.Insert(value) }
    if value > b.Value { b.Right = b.Right.Insert(value) }
    return b
}

func (b *BST[T]) Search(value T) bool {
    if b == nil { return false }
    if value == b.Value { return true }
    if value < b.Value { return b.Left.Search(value) }
    return b.Right.Search(value)
}

FAQ

{{< faq question="Are generics slower than interface{}?" >}} No. Generics are monomorphized — each type gets its own compiled implementation. Usually faster than interface{} boxing. {{< /faq >}}

{{< faq question="Can I use generics with methods?" >}} Methods can use the receiver's type parameters but cannot add new ones. Use package-level functions for additional type params. {{< /faq >}}

{{< faq question="What constraints does the standard library provide?" >}} constraints package: Ordered, Signed, Unsigned, Integer, Float, Complex. Import golang.org/x/exp/constraints. {{< /faq >}}

{{< faq question="Can I use generics with channels?" >}} Yes. type Chan[T any] chan T. Useful for typed channel utilities. {{< /faq >}}

{{< faq question="How do I check if a generic type implements an interface?" >}} Use type assertions or type switches within the generic function body. {{< /faq >}}

Try It Yourself

package main

import "fmt"

func Identity[T any](value T) T {
    return value
}

func main() {
    fmt.Println(Identity(42))
    fmt.Println(Identity("hello"))
    fmt.Println(Identity(3.14))
}

Expected output:

42
hello
3.14

What's Next

Now that you understand generics, explore Reflection for runtime type inspection.

Topic Description Link
Go Reflection Runtime type inspection {{< ref "32-reflection" >}}
Go CGo C interop {{< ref "33-cgo" >}}
Go Testing Advanced Advanced testing {{< ref "34-testing-advanced" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse Go