Skip to main content

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 types
  • comparable: 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