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
- Go Generics Proposal: https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md
- Go 1.18 Release Notes: https://golang.org/doc/go1.18
- Generics Tutorial: https://go.dev/doc/tutorial/generics
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