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. See Go Installation Guide, Go Ecosystem Overview, Go Best Practices for more context.
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
- Go Generics: https://go.dev/doc/tutorial/generics
- Type Parameters: https://go.dev/ref/spec#Type_parameters
- Constraints: https://go.dev/ref/spec#Constraints
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