Generics (Go 1.18+)
Introduced in Go 1.18, generics allow you to write reusable code that works across multiple types using type parameters. They eliminate code duplication while maintaining type safety.
Type Parameter Basicsβ
Generic functions use type parameters in the form [T constraint].
package main
import "fmt"
// Before generics: separate function needed for each type
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
func minFloat(a, b float64) float64 {
if a < b {
return a
}
return b
}
// Generic: one function for multiple types
// T is a type parameter, the part after | is the constraint
func Min[T int | float64 | string](a, b T) T {
if a < b {
return a
}
return b
}
// Multiple type parameters
func Map[T, U any](slice []T, f func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = f(v)
}
return result
}
func Filter[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
func Reduce[T, U any](slice []T, initial U, f func(U, T) U) U {
result := initial
for _, v := range slice {
result = f(result, v)
}
return result
}
func main() {
fmt.Println(Min(3, 7)) // 3
fmt.Println(Min(3.14, 2.72)) // 2.72
fmt.Println(Min("banana", "apple")) // apple
nums := []int{1, 2, 3, 4, 5}
// Map: int β string
strs := Map(nums, func(n int) string {
return fmt.Sprintf("item_%d", n)
})
fmt.Println(strs) // [item_1 item_2 item_3 item_4 item_5]
// Filter: evens only
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
fmt.Println(evens) // [2 4]
// Reduce: sum
sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
fmt.Println(sum) // 15
}
Constraintsβ
Constraints define the conditions a type parameter must satisfy.
package main
import "fmt"
// Custom constraint definitions
type Number interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
type Ordered interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64 | string
}
// ~ for underlying type constraint
type Integer interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
// Custom types with int underlying type are included
type MyInt int
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
func Keys[K comparable, V any](m map[K]V) []K {
keys := make([]K, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
func Values[K comparable, V any](m map[K]V) []V {
vals := make([]V, 0, len(m))
for _, v := range m {
vals = append(vals, v)
}
return vals
}
func main() {
ints := []int{1, 2, 3, 4, 5}
floats := []float64{1.1, 2.2, 3.3}
fmt.Println(Sum(ints)) // 15
fmt.Println(Sum(floats)) // 6.6
// MyInt satisfies ~int constraint
myInts := []MyInt{10, 20, 30}
fmt.Println(Sum(myInts)) // 60
strs := []string{"a", "b", "c", "d"}
fmt.Println(Contains(strs, "c")) // true
fmt.Println(Contains(strs, "z")) // false
fmt.Println(Contains(ints, 3)) // true
m := map[string]int{"a": 1, "b": 2, "c": 3}
fmt.Println(Keys(m)) // [a b c] (order varies)
fmt.Println(Values(m)) // [1 2 3] (order varies)
}
Generic Typesβ
Generics apply to structs and interfaces as well as functions.
package main
import (
"errors"
"fmt"
)
// Generic stack
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, error) {
if len(s.items) == 0 {
var zero T
return zero, errors.New("stack is empty")
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, nil
}
func (s *Stack[T]) Peek() (T, error) {
if len(s.items) == 0 {
var zero T
return zero, errors.New("stack is empty")
}
return s.items[len(s.items)-1], nil
}
func (s *Stack[T]) Len() int { return len(s.items) }
func (s *Stack[T]) IsEmpty() bool { return len(s.items) == 0 }
// Generic Optional (Maybe pattern)
type Optional[T any] struct {
value *T
}
func Some[T any](v T) Optional[T] {
return Optional[T]{value: &v}
}
func None[T any]() Optional[T] {
return Optional[T]{}
}
func (o Optional[T]) IsPresent() bool { return o.value != nil }
func (o Optional[T]) Get() (T, bool) {
if o.value == nil {
var zero T
return zero, false
}
return *o.value, true
}
func (o Optional[T]) OrElse(defaultVal T) T {
if o.value == nil {
return defaultVal
}
return *o.value
}
// Generic Result type (error handling)
type Result[T any] struct {
value T
err error
}
func Ok[T any](v T) Result[T] { return Result[T]{value: v} }
func Err[T any](e error) Result[T] { return Result[T]{err: e} }
func (r Result[T]) IsOk() bool { return r.err == nil }
func (r Result[T]) Unwrap() T { return r.value }
func (r Result[T]) Error() error { return r.err }
func main() {
// Generic stack
intStack := &Stack[int]{}
intStack.Push(1)
intStack.Push(2)
intStack.Push(3)
for !intStack.IsEmpty() {
v, _ := intStack.Pop()
fmt.Print(v, " ")
}
fmt.Println()
strStack := &Stack[string]{}
strStack.Push("Go")
strStack.Push("Python")
strStack.Push("Rust")
top, _ := strStack.Peek()
fmt.Println("Top:", top)
// Optional
name := Some("John")
if n, ok := name.Get(); ok {
fmt.Println("name:", n)
}
empty := None[string]()
fmt.Println("default:", empty.OrElse("anonymous"))
// Result
divide := func(a, b float64) Result[float64] {
if b == 0 {
return Err[float64](errors.New("division by zero"))
}
return Ok(a / b)
}
r1 := divide(10, 3)
r2 := divide(10, 0)
if r1.IsOk() {
fmt.Printf("10/3 = %.4f\n", r1.Unwrap())
}
if !r2.IsOk() {
fmt.Println("error:", r2.Error())
}
}
Generic Interfaces and Type Setsβ
package main
import (
"fmt"
"math"
)
// Use interface as constraint
type Stringer interface {
String() string
}
type Numeric interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
// Generic function calling interface method
func PrintAll[T Stringer](items []T) {
for _, item := range items {
fmt.Println(item.String())
}
}
// Generic math functions
func Abs[T Numeric](v T) T {
if v < 0 {
return -v
}
return v
}
func Clamp[T Numeric](v, min, max T) T {
if v < min {
return min
}
if v > max {
return max
}
return v
}
func Distance[T Numeric](x1, y1, x2, y2 T) float64 {
dx := float64(x2 - x1)
dy := float64(y2 - y1)
return math.Sqrt(dx*dx + dy*dy)
}
// Generic Pair
type Pair[A, B any] struct {
First A
Second B
}
func NewPair[A, B any](a A, b B) Pair[A, B] {
return Pair[A, B]{First: a, Second: b}
}
func (p Pair[A, B]) String() string {
return fmt.Sprintf("(%v, %v)", p.First, p.Second)
}
func (p Pair[A, B]) Swap() Pair[B, A] {
return Pair[B, A]{First: p.Second, Second: p.First}
}
type Point[T Numeric] struct {
X, Y T
}
func (p Point[T]) String() string {
return fmt.Sprintf("Point(%v, %v)", p.X, p.Y)
}
func main() {
fmt.Println(Abs(-42)) // 42
fmt.Println(Abs(-3.14)) // 3.14
fmt.Println(Clamp(15, 0, 10)) // 10
fmt.Println(Clamp(-5, 0, 10)) // 0
fmt.Println(Distance(0, 0, 3, 4)) // 5
p1 := NewPair("hello", 42)
p2 := NewPair(3.14, true)
fmt.Println(p1) // (hello, 42)
fmt.Println(p1.Swap()) // (42, hello)
fmt.Println(p2) // (3.14, true)
intPoint := Point[int]{X: 3, Y: 4}
floatPoint := Point[float64]{X: 1.5, Y: 2.5}
fmt.Println(intPoint) // Point(3, 4)
fmt.Println(floatPoint) // Point(1.5, 2.5)
points := []Point[int]{{1, 2}, {3, 4}, {5, 6}}
PrintAll(points)
}
Practical Example: Generic Cacheβ
package main
import (
"fmt"
"sync"
"time"
)
type CacheEntry[V any] struct {
value V
expiresAt time.Time
}
func (e CacheEntry[V]) IsExpired() bool {
return time.Now().After(e.expiresAt)
}
type Cache[K comparable, V any] struct {
mu sync.RWMutex
entries map[K]CacheEntry[V]
ttl time.Duration
}
func NewCache[K comparable, V any](ttl time.Duration) *Cache[K, V] {
c := &Cache[K, V]{
entries: make(map[K]CacheEntry[V]),
ttl: ttl,
}
go c.cleanup()
return c
}
func (c *Cache[K, V]) Set(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
c.entries[key] = CacheEntry[V]{
value: value,
expiresAt: time.Now().Add(c.ttl),
}
}
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
entry, ok := c.entries[key]
if !ok || entry.IsExpired() {
var zero V
return zero, false
}
return entry.value, true
}
func (c *Cache[K, V]) Delete(key K) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.entries, key)
}
func (c *Cache[K, V]) cleanup() {
ticker := time.NewTicker(c.ttl / 2)
defer ticker.Stop()
for range ticker.C {
c.mu.Lock()
for k, entry := range c.entries {
if entry.IsExpired() {
delete(c.entries, k)
}
}
c.mu.Unlock()
}
}
func main() {
// Cache with string keys and int values
intCache := NewCache[string, int](5 * time.Second)
intCache.Set("count", 42)
intCache.Set("total", 100)
if v, ok := intCache.Get("count"); ok {
fmt.Println("count:", v)
}
// Cache with int keys and struct values
type UserData struct {
Name string
Email string
}
userCache := NewCache[int, UserData](10 * time.Second)
userCache.Set(1, UserData{Name: "John Doe", Email: "john@example.com"})
if user, ok := userCache.Get(1); ok {
fmt.Printf("user: %s (%s)\n", user.Name, user.Email)
}
if _, ok := userCache.Get(999); !ok {
fmt.Println("user not found")
}
}
Avoiding Generic Overuseβ
package main
import "fmt"
// Bad: generics unnecessary here
func BadPrint[T any](v T) {
fmt.Println(v) // any is sufficient, no need for generics
}
// Good: type safety genuinely needed
func GoodMin[T int | float64](a, b T) T {
if a < b {
return a
}
return b
}
// When interface is more appropriate
type Printable interface {
String() string
}
func PrintObject(p Printable) { // interface is clearer here
fmt.Println(p.String())
}
// Clear win for generics: same logic, different types
func MapSlice[T, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
func main() {
// Type inference at compile time
nums := []int{1, 2, 3, 4, 5}
doubled := MapSlice(nums, func(n int) int { return n * 2 })
fmt.Println(doubled) // [2 4 6 8 10]
strs := MapSlice(nums, func(n int) string {
return fmt.Sprintf("#%d", n)
})
fmt.Println(strs) // [#1 #2 #3 #4 #5]
fmt.Println(GoodMin(3, 7)) // 3
fmt.Println(GoodMin(3.14, 2.72)) // 2.72
}
Key Summary
- Type parameter
[T constraint]: write reusable code for multiple typescomparable: types that support==/!=comparison (used for map keys, etc.)any: no constraint (interface{}alias)~T: includes all types with T as the underlying type- Generic types work on structs and interfaces too
- When an interface is sufficient, prefer interface over generics
- Type inference means you usually don't need to specify type parameters explicitly