Generics (type parameters) were introduced in Go 1.18, allowing you to write functions and types that work with multiple types while maintaining type safety. Generics eliminate code duplication and enable powerful abstractions.
💡 Key Points
- Generics enable type-safe reusable code
- Use square brackets [] for type parameters
- Type constraints specify what operations are allowed
- any is a constraint that accepts any type
- comparable constraint for types that support == and !=
- Can define custom constraints with interfaces
- Generics work with functions, types, and methods
- Type inference often eliminates need to specify types explicitly
Generic Functions
Define functions that work with multiple types:
package main
import "fmt"
// Generic function with type parameter T
func Print[T any](value T) {
fmt.Println(value)
}
// Generic function with constraint
func Min[T comparable](a, b T) T {
// Note: This won't compile as-is because comparable
// doesn't include < operator. See ordered constraint below.
if a < b {
return a
}
return b
}
// Using constraints package for ordered types
import "golang.org/x/exp/constraints"
func MinOrdered[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
// Type inference - Go figures out the type
Print(42)
Print("hello")
Print(3.14)
Print([]int{1, 2, 3})
// Explicit type parameter
Print[string]("world")
// Using ordered constraint
fmt.Println("Min int:", MinOrdered(10, 20))
fmt.Println("Min float:", MinOrdered(3.14, 2.71))
fmt.Println("Min string:", MinOrdered("apple", "banana"))
}
Output:42
hello
3.14
[1 2 3]
world
Min int: 10
Min float: 2.71
Min string: apple
Generic Types
Create data structures that work with any type:
package main
import "fmt"
// Generic stack type
type Stack[T any] struct {
items []T
}
// Methods on generic type
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
}
index := len(s.items) - 1
item := s.items[index]
s.items = s.items[:index]
return item, true
}
func (s *Stack[T]) IsEmpty() bool {
return len(s.items) == 0
}
// Generic map function
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
func main() {
// Stack of integers
intStack := Stack[int]{}
intStack.Push(1)
intStack.Push(2)
intStack.Push(3)
val, ok := intStack.Pop()
fmt.Printf("Popped: %d (ok: %v)\n", val, ok)
// Stack of strings
strStack := Stack[string]{}
strStack.Push("hello")
strStack.Push("world")
str, ok := strStack.Pop()
fmt.Printf("Popped: %s (ok: %v)\n", str, ok)
// Generic map function
numbers := []int{1, 2, 3, 4, 5}
doubled := Map(numbers, func(n int) int { return n * 2 })
fmt.Println("Doubled:", doubled)
strings := Map(numbers, func(n int) string {
return fmt.Sprintf("num_%d", n)
})
fmt.Println("Strings:", strings)
}
Output:Popped: 3 (ok: true)
Popped: world (ok: true)
Doubled: [2 4 6 8 10]
Strings: [num_1 num_2 num_3 num_4 num_5]
Type Constraints
| Constraint | Description | Example |
any | Accepts any type (alias for interface{}) | func Print[T any](v T) |
comparable | Types that support == and != | func Equal[T comparable](a, b T) |
constraints.Ordered | Types that support <, >, etc. | func Max[T constraints.Ordered](a, b T) |
constraints.Integer | All integer types | func Sum[T constraints.Integer](nums []T) |
constraints.Float | All float types | func Avg[T constraints.Float](nums []T) |
| Custom interface | Define your own constraint | type Numeric interface { int | float64 } |
Custom Constraints
| Pattern | Example | Use Case |
| Type union | interface { int | float64 | string } | Allow specific types |
| Method constraint | interface { String() string } | Require specific methods |
| Combined constraint | interface { ~int | ~float64; String() string } | Type union + methods |
| Approximate type | interface { ~int } | Underlying type must be int |
| Embedded constraint | interface { Stringer; comparable } | Combine multiple constraints |
Generic Syntax
| Element | Syntax | Description |
| Type parameter | [T any] | Single type parameter |
| Multiple params | [T, U any] | Multiple type parameters |
| Different constraints | [T any, U comparable] | Each param with own constraint |
| Generic function | func Name[T any](v T) { } | Function with type parameter |
| Generic type | type Box[T any] struct { } | Type with type parameter |
| Method on generic | func (b Box[T]) Get() T { } | Method on generic type |
Common Generic Patterns
| Pattern | Example | Use Case |
| Generic container | type List[T any] struct { } | Lists, stacks, queues |
| Generic algorithm | func Sort[T Ordered](slice []T) | Sorting, searching |
| Map/Filter/Reduce | func Map[T, U any](s []T, f func(T)U) []U | Functional operations |
| Generic pair | type Pair[T, U any] struct { First T; Second U } | Tuple-like structures |
| Generic option | type Option[T any] struct { valid bool; value T } | Optional values |
| Generic result | type Result[T any] struct { value T; err error } | Result with error |
Generics Best Practices
| Guideline | Reason | Example |
| Use when type-safe reuse needed | Avoid code duplication | Generic containers, algorithms |
| Keep constraints simple | Easier to understand and use | Prefer any, comparable |
| Consider interfaces first | May be simpler for behavior | Interface methods vs type params |
| Don't over-generalize | YAGNI principle | Add generics when actually needed |
| Use type inference | Less verbose code | Let Go infer types when possible |
| Name conventions | Clarity and consistency | T for type, K/V for key/value |