Hi there! I'm Shrijith Venkatrama, founder of Hexmos. Right now, I’m building LiveAPI, a tool that makes generating API docs from your code ridiculously easy.

Go generics, introduced in Go 1.18, let you write flexible, type-safe code without duplicating logic for every type. If you’re curious about generics or want hands-on examples, this post dives into five practical use cases that show their power in real-world scenarios.

Each example includes complete, ready-to-run code with clear explanations. We’ll cover utilities, data structures, and more, keeping things developer-friendly and scannable. Let’s get started.

Why Generics Matter in Go

Before generics, Go developers used interface{} or copied code for different types. Interface{} sacrificed type safety, and duplication was error-prone. Generics fix this by letting you write reusable code that the compiler checks for correctness.

Generics are like blueprints: define the logic once, and apply it to any type. For the full backstory, see the Go generics proposal.

Let’s explore five examples.

Example 1: A Generic Sum Function

Summing numbers in a slice is a common task. Without generics, you’d need separate functions for int, float64, etc. A generic Sum function handles them all.

Here’s the code:

package main

import "fmt"

type Number interface {
    int | int64 | float32 | float64
}

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

func main() {
    ints := []int{1, 2, 3, 4}
    fmt.Println(Sum(ints)) // Output: 10

    floats := []float64{1.5, 2.5, 3.5}
    fmt.Println(Sum(floats)) // Output: 7.5
}

Key points:

  • The Number interface constrains T to numeric types (int, int64, float32, float64).
  • The function works with any slice of numbers, keeping type safety.
  • It’s simple but shows how generics reduce boilerplate.

Example 2: A Generic Slice Filter

Filtering slices is useful for tasks like finding valid records or active users. Let’s create a generic Filter function that keeps elements matching a condition.

Here’s the code:

package main

import "fmt"

func Filter[T any](slice []T, predicate func(T) bool) []T {
    result := []T{}
    for _, v := range slice {
        if predicate(v) {
            result = append(result, v)
        }
    }
    return result
}

func main() {
    numbers := []int{1, 2, 3, 4, 5, 6}
    evens := Filter(numbers, func(n int) bool { return n%2 == 0 })
    fmt.Println(evens) // Output: [2 4 6]

    words := []string{"cat", "dog", "elephant", "rat"}
    longWords := Filter(words, func(s string) bool { return len(s) > 3 })
    fmt.Println(longWords) // Output: [elephant]
}

Key points:

  • [T any] means no constraints—it works with any type.
  • The predicate function makes it flexible for any condition.
  • Returns a new slice to avoid modifying the original.

This shows generics enabling functional patterns in Go.

Example 3: A Generic Queue Data Structure

Queues are great for tasks like task scheduling or message processing. Let’s build a generic queue that can hold any type, from integers to structs.

Here’s the implementation:

package main

import "fmt"

type Queue[T any] struct {
    items []T
}

func (q *Queue[T]) Enqueue(item T) {
    q.items = append(q.items, item)
}

func (q *Queue[T]) Dequeue() (T, bool) {
    if len(q.items) == 0 {
        var zero T
        return zero, false
    }
    item := q.items[0]
    q.items = q.items[1:]
    return item, true
}

func main() {
    intQueue := Queue[int]{}
    intQueue.Enqueue(1)
    intQueue.Enqueue(2)
    if item, ok := intQueue.Dequeue(); ok {
        fmt.Println(item) // Output: 1
    }

    strQueue := Queue[string]{}
    strQueue.Enqueue("hello")
    strQueue.Enqueue("world")
    if item, ok := strQueue.Dequeue(); ok {
        fmt.Println(item) // Output: hello
    }
}

Key points:

  • Queue[T any] uses a type parameter for its items.
  • Enqueue and Dequeue are type-safe and reusable.
  • Handles empty queues with a zero value and boolean.

This demonstrates generics in data structures. Learn more about queues here.

Example 4: A Generic Merge Function

Merging two sorted slices is useful for tasks like combining datasets. Let’s create a generic Merge function that works with any comparable type, like numbers or strings.

Here’s the code:

package main

import "fmt"

// Merge merges two sorted slices using a comparator function.
func Merge[T any](a, b []T, less func(T, T) bool) []T {
    result := make([]T, 0, len(a)+len(b))
    i, j := 0, 0

    for i < len(a) && j < len(b) {
        if less(a[i], b[j]) {
            result = append(result, a[i])
            i++
        } else {
            result = append(result, b[j])
            j++
        }
    }

    result = append(result, a[i:]...)
    result = append(result, b[j:]...)
    return result
}

func main() {
    nums1 := []int{1, 3, 5}
    nums2 := []int{2, 4, 6}
    mergedNums := Merge(nums1, nums2, func(x, y int) bool { return x < y })
    fmt.Println(mergedNums) // Output: [1 2 3 4 5 6]

    strs1 := []string{"apple", "cherry"}
    strs2 := []string{"banana", "date"}
    mergedStrs := Merge(strs1, strs2, func(x, y string) bool { return x < y })
    fmt.Println(mergedStrs) // Output: [apple banana cherry date]
}
  • Type parameters can't use <, <=: Go's comparable constraint only supports == and !=; relational operators require custom logic.

  • Comparator function (less) is passed in: The Merge function uses a user-provided less(T, T) bool to compare elements, enabling sorting without native operator support.

  • Generic merge logic: The algorithm interleaves elements from two sorted slices into one, maintaining order using the comparator.

  • Works with any ordered type: By changing the less function, you can merge slices of int, string, or custom types.

Example 5: A Generic Key-Value Store

Key-value stores are common in caches, configs, or databases. Let’s build a generic key-value store that supports any key and value types.

Here’s the implementation:

package main

import "fmt"

type KVStore[K comparable, V any] struct {
    data map[K]V
}

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

func (s *KVStore[K, V]) Set(key K, value V) {
    s.data[key] = value
}

func (s *KVStore[K, V]) Get(key K) (V, bool) {
    value, ok := s.data[key]
    return value, ok
}

func main() {
    store1 := NewKVStore[string, int]()
    store1.Set("age", 30)
    if value, ok := store1.Get("age"); ok {
        fmt.Println(value) // Output: 30
    }

    store2 := NewKVStore[int, string]()
    store2.Set(1, "one")
    if value, ok := store2.Get(1); ok {
        fmt.Println(value) // Output: one
    }
}

Key points:

  • Uses two type parameters: K (comparable) and V (any).
  • NewKVStore initializes the store cleanly.
  • Type-safe methods make it versatile.

This shows generics in complex, multi-type structures.

Comparing the Examples

Here’s a table to summarize the examples:

Example Use Case Type Parameters Constraints Complexity
Sum Function Sum numbers 1 (T) Number interface Low
Slice Filter Filter data 1 (T) none (any) Medium
Queue FIFO structure 1 (T) none (any) Medium
Merge Function Merge sorted slices 1 (T) comparable Medium
KV Store Key-value storage 2 (K, V) K: comparable, V: none High

This table helps you pick the right tool for your task.

Final Thoughts on Go Generics

Go generics make your code reusable and type-safe, from simple utilities like Sum to complex structures like KVStore. Try them in small functions first, then scale up to data structures or algorithms. Experiment with these examples in your projects or dig into the Go blog on generics for more.

What’s your go-to generics use case? Drop your thoughts or experiments in the comments.