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 constrainsT
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
andDequeue
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'scomparable
constraint only supports==
and!=
; relational operators require custom logic.Comparator function (
less
) is passed in: TheMerge
function uses a user-providedless(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 ofint
,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) andV
(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.