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:
- Type Constraints: Use
any(an alias forinterface{}) for unrestricted types. For specific behaviors, define custom constraints liketype Number interface { int | float64 }and useStack[Number]. - Zero Values: When returning a generic type (like in the
Popmethod), you must return the zero value ofTif the operation fails. Go handles this automatically by declaringvar zero T. - 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.