Skip to main content
โšก Calmops

Generics in Go: Type Parameters and Constraints

Generics in Go: Type Parameters and Constraints

Introduction

Go 1.18 introduced generics, allowing you to write reusable code that works with different types while maintaining type safety. This guide covers type parameters, constraints, and practical patterns for using generics effectively.

Core Concepts

What are Generics?

Generics allow you to write functions and types that work with multiple types while maintaining compile-time type safety. Instead of using interface{} and type assertions, you can use type parameters.

Type Parameters

Type parameters are placeholders for types, written in square brackets:

// T is a type parameter
func Print[T any](value T) {
	fmt.Println(value)
}

Constraints

Constraints specify what types can be used for a type parameter:

// T must be an integer type
func Sum[T int | int32 | int64](values []T) T {
	var sum T
	for _, v := range values {
		sum += v
	}
	return sum
}

Good: Basic Generics

Generic Functions

package main

import (
	"fmt"
)

// โœ… GOOD: Generic function with any type
func Print[T any](value T) {
	fmt.Printf("Value: %v\n", value)
}

// โœ… GOOD: Generic function with constraint
func Max[T interface{ ~int | ~float64 }](a, b T) T {
	if a > b {
		return a
	}
	return b
}

// โœ… GOOD: Generic slice operations
func Contains[T comparable](slice []T, value T) bool {
	for _, v := range slice {
		if v == value {
			return true
		}
	}
	return false
}

// โœ… GOOD: Generic map operations
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 main() {
	Print(42)
	Print("hello")
	
	fmt.Println(Max(10, 20))
	fmt.Println(Max(3.14, 2.71))
	
	fmt.Println(Contains([]int{1, 2, 3}, 2))
	fmt.Println(Contains([]string{"a", "b", "c"}, "b"))
}

Generic Types

package main

import (
	"fmt"
)

// โœ… GOOD: Generic struct
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
	}
	
	item := s.items[len(s.items)-1]
	s.items = s.items[:len(s.items)-1]
	return item, true
}

// โœ… GOOD: Generic pair type
type Pair[T, U any] struct {
	First  T
	Second U
}

func (p *Pair[T, U]) Swap() *Pair[U, T] {
	return &Pair[U, T]{
		First:  p.Second,
		Second: p.First,
	}
}

func main() {
	// Integer stack
	intStack := &Stack[int]{}
	intStack.Push(1)
	intStack.Push(2)
	intStack.Push(3)
	
	for {
		val, ok := intStack.Pop()
		if !ok {
			break
		}
		fmt.Println(val)
	}
	
	// String stack
	strStack := &Stack[string]{}
	strStack.Push("a")
	strStack.Push("b")
	
	// Pair
	pair := &Pair[int, string]{First: 42, Second: "answer"}
	fmt.Printf("Pair: %v\n", pair)
}

Good: Constraints

Using Constraints

package main

import (
	"fmt"
)

// โœ… GOOD: Numeric constraint
type Number interface {
	~int | ~int32 | ~int64 | ~float32 | ~float64
}

func Sum[T Number](values []T) T {
	var sum T
	for _, v := range values {
		sum += v
	}
	return sum
}

// โœ… GOOD: Comparable constraint
func Unique[T comparable](slice []T) []T {
	seen := make(map[T]bool)
	var result []T
	
	for _, v := range slice {
		if !seen[v] {
			seen[v] = true
			result = append(result, v)
		}
	}
	
	return result
}

// โœ… GOOD: Custom constraint
type Stringer interface {
	String() string
}

func PrintAll[T Stringer](items []T) {
	for _, item := range items {
		fmt.Println(item.String())
	}
}

// โœ… GOOD: Multiple constraints
type Ordered interface {
	~int | ~int32 | ~int64 | ~float32 | ~float64 | ~string
}

func Min[T Ordered](a, b T) T {
	if a < b {
		return a
	}
	return b
}

func main() {
	fmt.Println(Sum([]int{1, 2, 3, 4, 5}))
	fmt.Println(Sum([]float64{1.1, 2.2, 3.3}))
	
	fmt.Println(Unique([]int{1, 2, 2, 3, 3, 3}))
	fmt.Println(Unique([]string{"a", "b", "a", "c"}))
	
	fmt.Println(Min(10, 20))
	fmt.Println(Min("apple", "banana"))
}

Good: Advanced Generic Patterns

Generic Interfaces

package main

import (
	"fmt"
)

// โœ… GOOD: Generic interface
type Reader[T any] interface {
	Read() (T, error)
}

type Writer[T any] interface {
	Write(T) error
}

// โœ… GOOD: Generic implementation
type Channel[T any] struct {
	ch chan T
}

func (c *Channel[T]) Read() (T, error) {
	val, ok := <-c.ch
	if !ok {
		var zero T
		return zero, fmt.Errorf("channel closed")
	}
	return val, nil
}

func (c *Channel[T]) Write(val T) error {
	select {
	case c.ch <- val:
		return nil
	default:
		return fmt.Errorf("channel full")
	}
}

// โœ… GOOD: Generic function using interface
func Copy[T any](src Reader[T], dst Writer[T]) error {
	for {
		val, err := src.Read()
		if err != nil {
			return err
		}
		if err := dst.Write(val); err != nil {
			return err
		}
	}
}

func main() {
	ch := &Channel[int]{ch: make(chan int, 10)}
	ch.Write(42)
	val, _ := ch.Read()
	fmt.Println(val)
}

Generic Methods

package main

import (
	"fmt"
)

// โœ… GOOD: Generic methods on types
type Container[T any] struct {
	items []T
}

func (c *Container[T]) Add(item T) {
	c.items = append(c.items, item)
}

func (c *Container[T]) Get(index int) (T, bool) {
	if index < 0 || index >= len(c.items) {
		var zero T
		return zero, false
	}
	return c.items[index], true
}

func (c *Container[T]) Map[U any](fn func(T) U) *Container[U] {
	result := &Container[U]{}
	for _, item := range c.items {
		result.Add(fn(item))
	}
	return result
}

func main() {
	intContainer := &Container[int]{}
	intContainer.Add(1)
	intContainer.Add(2)
	intContainer.Add(3)
	
	// Map to strings
	strContainer := intContainer.Map(func(i int) string {
		return fmt.Sprintf("Number: %d", i)
	})
	
	for i := 0; i < 3; i++ {
		val, _ := strContainer.Get(i)
		fmt.Println(val)
	}
}

Best Practices

1. Use Generics for Code Reuse

// โœ… GOOD: Generic for reusable code
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
}

// โŒ BAD: Separate implementations
func FilterInts(slice []int, predicate func(int) bool) []int {
	// Duplicate code
}

func FilterStrings(slice []string, predicate func(string) bool) []string {
	// Duplicate code
}

2. Keep Constraints Simple

// โœ… GOOD: Simple, clear constraint
type Numeric interface {
	~int | ~float64
}

// โŒ BAD: Overly complex constraint
type ComplexConstraint interface {
	~int | ~int32 | ~int64 | ~uint | ~uint32 | ~uint64 | ~float32 | ~float64 | ~complex64 | ~complex128
}

3. Use Type Parameters Wisely

// โœ… GOOD: Type parameters where needed
func Process[T any](items []T, fn func(T) T) []T {
	result := make([]T, len(items))
	for i, item := range items {
		result[i] = fn(item)
	}
	return result
}

// โŒ BAD: Unnecessary type parameters
func Unnecessary[T any]() {
	// T is never used
}

Resources

Summary

Generics in Go 1.18+ enable you to write reusable, type-safe code. Use type parameters for functions and types that work with multiple types, and use constraints to specify what types are allowed. Generics reduce code duplication and improve maintainability.

Comments