Skip to main content
โšก Calmops

Generic Functions and Types in Go

Generic Functions and Types in Go

Introduction

Generic functions and types allow you to write code that works with any type while maintaining compile-time type safety. This guide covers practical patterns for building reusable generic components.

Core Concepts

Generic Functions

Generic functions use type parameters to work with multiple types:

func Process[T any](items []T) []T {
	// Works with any type T
}

Generic Types

Generic types (structs, interfaces) can hold or work with any type:

type Box[T any] struct {
	value T
}

Good: Generic Functions

Utility Functions

package main

import (
	"fmt"
	"sort"
)

// โœ… GOOD: Generic slice operations
func Reverse[T any](slice []T) []T {
	result := make([]T, len(slice))
	for i, v := range slice {
		result[len(slice)-1-i] = v
	}
	return result
}

// โœ… GOOD: Generic find function
func Find[T any](slice []T, predicate func(T) bool) (T, bool) {
	for _, item := range slice {
		if predicate(item) {
			return item, true
		}
	}
	var zero T
	return zero, false
}

// โœ… GOOD: Generic map function
func Map[T, U any](slice []T, fn func(T) U) []U {
	result := make([]U, len(slice))
	for i, item := range slice {
		result[i] = fn(item)
	}
	return result
}

// โœ… GOOD: Generic reduce function
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
	result := initial
	for _, item := range slice {
		result = fn(result, item)
	}
	return result
}

// โœ… GOOD: Generic filter function
func Filter[T any](slice []T, predicate func(T) bool) []T {
	var result []T
	for _, item := range slice {
		if predicate(item) {
			result = append(result, item)
		}
	}
	return result
}

func main() {
	// Reverse
	nums := []int{1, 2, 3, 4, 5}
	fmt.Println(Reverse(nums))
	
	// Find
	found, ok := Find(nums, func(n int) bool { return n > 3 })
	fmt.Printf("Found: %d (%v)\n", found, ok)
	
	// Map
	doubled := Map(nums, func(n int) int { return n * 2 })
	fmt.Println(doubled)
	
	// Reduce
	sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
	fmt.Printf("Sum: %d\n", sum)
	
	// Filter
	evens := Filter(nums, func(n int) bool { return n%2 == 0 })
	fmt.Println(evens)
}

Generic Sorting

package main

import (
	"fmt"
	"sort"
)

// โœ… GOOD: Generic sort function
type Ordered interface {
	~int | ~int32 | ~int64 | ~float32 | ~float64 | ~string
}

func SortSlice[T Ordered](slice []T) {
	sort.Slice(slice, func(i, j int) bool {
		return slice[i] < slice[j]
	})
}

// โœ… GOOD: Generic sort with custom comparator
func SortWith[T any](slice []T, less func(T, T) bool) {
	sort.Slice(slice, func(i, j int) bool {
		return less(slice[i], slice[j])
	})
}

func main() {
	nums := []int{3, 1, 4, 1, 5, 9}
	SortSlice(nums)
	fmt.Println(nums)
	
	strs := []string{"banana", "apple", "cherry"}
	SortSlice(strs)
	fmt.Println(strs)
	
	// Custom sort
	people := []struct {
		name string
		age  int
	}{
		{"Alice", 30},
		{"Bob", 25},
		{"Charlie", 35},
	}
	
	SortWith(people, func(a, b struct {
		name string
		age  int
	}) bool {
		return a.age < b.age
	})
	fmt.Println(people)
}

Good: Generic Types

Generic Data Structures

package main

import (
	"fmt"
	"sync"
)

// โœ… GOOD: Generic queue
type Queue[T any] struct {
	mu    sync.Mutex
	items []T
}

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

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

// โœ… GOOD: Generic cache
type Cache[K comparable, V any] struct {
	mu    sync.RWMutex
	items map[K]V
}

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

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

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

// โœ… GOOD: Generic tree node
type TreeNode[T any] struct {
	Value T
	Left  *TreeNode[T]
	Right *TreeNode[T]
}

func (n *TreeNode[T]) InOrder(fn func(T)) {
	if n == nil {
		return
	}
	n.Left.InOrder(fn)
	fn(n.Value)
	n.Right.InOrder(fn)
}

func main() {
	// Queue
	q := &Queue[int]{}
	q.Enqueue(1)
	q.Enqueue(2)
	q.Enqueue(3)
	
	for {
		val, ok := q.Dequeue()
		if !ok {
			break
		}
		fmt.Println(val)
	}
	
	// Cache
	cache := NewCache[string, int]()
	cache.Set("answer", 42)
	cache.Set("pi", 3)
	
	if val, ok := cache.Get("answer"); ok {
		fmt.Printf("answer = %d\n", val)
	}
	
	// Tree
	root := &TreeNode[int]{
		Value: 2,
		Left: &TreeNode[int]{Value: 1},
		Right: &TreeNode[int]{Value: 3},
	}
	
	root.InOrder(func(val int) {
		fmt.Printf("%d ", val)
	})
}

Generic Interfaces

package main

import (
	"fmt"
)

// โœ… GOOD: Generic repository interface
type Repository[T any] interface {
	Create(item T) error
	Read(id string) (T, error)
	Update(item T) error
	Delete(id string) error
	List() ([]T, error)
}

// โœ… GOOD: Generic service using repository
type Service[T any] struct {
	repo Repository[T]
}

func (s *Service[T]) GetAll() ([]T, error) {
	return s.repo.List()
}

func (s *Service[T]) GetByID(id string) (T, error) {
	return s.repo.Read(id)
}

// โœ… GOOD: Generic handler
type Handler[T any] struct {
	service *Service[T]
}

func (h *Handler[T]) HandleList() ([]T, error) {
	return h.service.GetAll()
}

func main() {
	// Example usage with concrete type
	type User struct {
		ID   string
		Name string
	}
	
	// Would implement Repository[User]
	// Then create Service[User]
	// Then create Handler[User]
	
	fmt.Println("Generic interfaces enable flexible architectures")
}

Advanced Patterns

Generic Middleware

package main

import (
	"fmt"
	"time"
)

// โœ… GOOD: Generic middleware pattern
type Handler[T any] func(T) (T, error)

func WithLogging[T any](handler Handler[T]) Handler[T] {
	return func(input T) (T, error) {
		fmt.Printf("Processing: %v\n", input)
		start := time.Now()
		
		result, err := handler(input)
		
		fmt.Printf("Completed in %v\n", time.Since(start))
		return result, err
	}
}

func WithRetry[T any](handler Handler[T], maxRetries int) Handler[T] {
	return func(input T) (T, error) {
		var lastErr error
		
		for i := 0; i < maxRetries; i++ {
			result, err := handler(input)
			if err == nil {
				return result, nil
			}
			lastErr = err
		}
		
		var zero T
		return zero, lastErr
	}
}

func main() {
	// Create a handler
	handler := func(input int) (int, error) {
		return input * 2, nil
	}
	
	// Wrap with middleware
	wrapped := WithLogging(WithRetry(handler, 3))
	
	result, _ := wrapped(21)
	fmt.Printf("Result: %d\n", result)
}

Generic Builder Pattern

package main

import (
	"fmt"
)

// โœ… GOOD: Generic builder
type Builder[T any] struct {
	value T
	steps []func(*T) error
}

func NewBuilder[T any](initial T) *Builder[T] {
	return &Builder[T]{value: initial}
}

func (b *Builder[T]) Add(step func(*T) error) *Builder[T] {
	b.steps = append(b.steps, step)
	return b
}

func (b *Builder[T]) Build() (T, error) {
	for _, step := range b.steps {
		if err := step(&b.value); err != nil {
			return b.value, err
		}
	}
	return b.value, nil
}

func main() {
	type Config struct {
		Name  string
		Value int
	}
	
	config, _ := NewBuilder(Config{}).
		Add(func(c *Config) error {
			c.Name = "test"
			return nil
		}).
		Add(func(c *Config) error {
			c.Value = 42
			return nil
		}).
		Build()
	
	fmt.Printf("Config: %v\n", config)
}

Best Practices

1. Name Type Parameters Clearly

// โœ… GOOD: Clear names
func Process[Item any, Result any](items []Item, fn func(Item) Result) []Result {
	// Clear what each type parameter represents
}

// โŒ BAD: Unclear names
func Process[T, U any](items []T, fn func(T) U) []U {
	// What are T and U?
}

2. Use Constraints Appropriately

// โœ… GOOD: Specific constraint
func Sum[T interface{ ~int | ~float64 }](values []T) T {
	// Clear what types are allowed
}

// โŒ BAD: Too broad
func Sum[T any](values []T) T {
	// Can't use + operator on any type
}

3. Avoid Over-Generalization

// โœ… GOOD: Specific when needed
func GetUser(id string) (*User, error) {
	// Specific type is clearer
}

// โŒ BAD: Unnecessary generics
func Get[T any](id string) (T, error) {
	// Adds complexity without benefit
}

Resources

Summary

Generic functions and types enable you to write reusable, type-safe code. Use them for utility functions, data structures, and interfaces that work with multiple types. Keep constraints specific and avoid over-generalization.

Comments