How to Use Generic Data Structures in Go

Go 1.18+ introduced generics, allowing you to write reusable data structures that work with any type while maintaining compile-time type safety.

Go 1.18+ introduced generics, allowing you to write reusable data structures that work with any type while maintaining compile-time type safety. You define a type parameter using square brackets after the struct or function name, then use that parameter wherever the concrete type would normally appear.

Here is a practical example of a generic Stack data structure and a Map utility function:

package main

import "fmt"

// Generic Stack definition
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
	}
	idx := len(s.items) - 1
	item := s.items[idx]
	s.items = s.items[:idx]
	return item, true
}

// Generic Map transformation function
func MapValues[K comparable, V any, R any](m map[K]V, fn func(V) R) map[K]R {
	result := make(map[K]R, len(m))
	for k, v := range m {
		result[k] = fn(v)
	}
	return result
}

func main() {
	// Using Stack with integers
	intStack := &Stack[int]{}
	intStack.Push(10)
	intStack.Push(20)
	val, ok := intStack.Pop()
	fmt.Printf("Popped int: %d (ok: %v)\n", val, ok)

	// Using Stack with strings
	strStack := &Stack[string]{}
	strStack.Push("Hello")
	strStack.Push("World")
	valStr, _ := strStack.Pop()
	fmt.Printf("Popped string: %s\n", valStr)

	// Using MapValues to transform a map
	nums := map[string]int{"a": 1, "b": 2}
	doubled := MapValues(nums, func(n int) int { return n * 2 })
	fmt.Printf("Doubled map: %v\n", doubled)
}

Key implementation details to remember:

  1. Type Constraints: Use any (an alias for interface{}) for unrestricted types. For specific behaviors, define custom constraints like type Number interface { int | float64 } and use Stack[Number].
  2. Zero Values: When returning a generic type (like in the Pop method), you must return the zero value of T if the operation fails. Go handles this automatically by declaring var zero T.
  3. Map Keys: If your generic type is used as a map key, you must constrain it with comparable (e.g., K comparable) to ensure the type supports equality checks.

Generics eliminate the need for interface{} and type assertions in your core logic, reducing runtime errors and improving code clarity. Use them for collections, algorithms, and utility functions where the logic is identical regardless of the underlying data type.