Go: Go Generics
Last updated: 2026-08-26
Go 1.18 introduced generics—allowing functions and data structures to be written using type parameters that work for
int,سلسلة, and custom types, thereby completely eliminating the need forinterface{}type assertions.
When you need to write three nearly identical sorting functions for int, سلسلة, and float64, generics allow you to write just one.
1. You will learn
- Generic Function Syntax
- Generic Data Structures
anyandcomparableconstraintsconstraints: Custom constraints- Type Inference
- Comparison of generic and
interface{}
2. A True Story of an Algorithm Engineer
(1) Pain point: Writing a sort دالة for each type
Charlie needs to implement a generic sorting library that supports three types: int, float64, and سلسلة:
"There were no generics in Go prior to version 1.18. I wrote three identical functions—the only difference was the type. Every time I added a new type, I had to copy and paste the code. The maintenance overhead was through the roof."
// Bad code: no generics, copy-paste for each type
func SortInts(slice []int) {
sort.Slice(slice, func(i, j int) bool { return slice[i] < slice[j] })
}
func SortFloat64s(slice []float64) {
sort.Slice(slice, func(i, j int) bool { return slice[i] < slice[j] })
}
func SortStrings(slice []string) {
sort.Slice(slice, func(i, j int) bool { return slice[i] < slice[j] })
}
// Every new type is Ctrl+C / Ctrl+V
(2) Solution in Go 1.18: Generics
// Good code: one دالة supports all ordered types
func Sort[T constraints.Ordered](slice []T) {
sort.Slice(slice, func(i, j int) bool { return slice[i] < slice[j] })
}
// Usage: automatic type inference
ints := []int{3, 1, 2}
Sort(ints)
floats := []float64{3.14, 1.41, 2.72}
Sort(floats)
strs := []سلسلة{"c", "a", "b"}
Sort(strs)
// No need for three functions!
(3) Performance: Before Generics vs. After Generics
| Dimension | interface{} + type assertion | generics |
|---|---|---|
| Amount of code | One copy per type | One copy |
| Type Safety | ❌ Runtime panic | ✅ Compile-time check |
| Performance | Involves boxing/unboxing overhead | ✅ Zero overhead |
| Readability | Numerous type assertions | ✅ Clear |
3. Generic Functions
▶ Example: Basic Generic Functions
⚙️ Prerequisite: Run
go get golang.org/x/exp/constraints
package main
import (
"fmt"
"golang.org/x/exp/constraints"
)
// Generic function: T is the type parameter, any is the constraint (all types)
func Print[T any](value T) {
fmt.Println(value)
}
// Multiple type parameters
func Pair[A, B any](a A, b B) (A, B) {
return a, b
}
// Constrained to ordered types
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
// Explicitly specify type parameters
Print[int](42)
Print[string]("hello")
// Type inference (compiler automatically infers T)
Print(42) // T = int
Print("hello") // T = string
fmt.Println(Max(3, 5)) // T = int → 5
fmt.Println(Max(3.14, 2.72)) // T = float64 → 3.14
fmt.Println(Max("apple", "banana")) // T = string → "banana"
a, b := Pair(1, "one")
fmt.Printf("A=%v (type: %T), B=%v (type: %T)\n", a, a, b, b)
}
(2) Generic Function Syntax
// Syntax: func Name[TypeParameter Constraint](paramList) returnType
func Name[T Constraint](param T) T { ... }
// Type parameter list is enclosed in [] (not angle brackets)
// Constraints can be any / comparable / custom interface
// Return values can use type parameters
4. Type Constraints
▶ Example: Built-in Constraints
package main
import (
"fmt"
"golang.org/x/exp/constraints"
)
// any: all types (equivalent to interface{})
func Identity[T any](value T) T {
return value
}
// comparable: types that support == and !=
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
// constraints.Ordered: types that support < <= > >=
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
fmt.Println(Contains([]int{1, 2, 3}, 2)) // true
fmt.Println(Contains([]سلسلة{"a", "b", "c"}, "d")) // false
fmt.Println(Min(10, 20)) // 10
}
▶ Example: Custom Constraints
package main
import "fmt"
// Custom constraint: interface + type set
type Numeric interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64
}
// ~int means all types whose underlying type is int (including type MyInt int)
// int only matches int itself
type Price float64
func Sum[T Numeric](values []T) T {
var sum T
for _, v := range values {
sum += v
}
return sum
}
func main() {
ints := []int{1, 2, 3, 4, 5}
fmt.Printf("Sum(ints) = %d\n", Sum(ints)) // 15
floats := []float64{1.5, 2.5, 3.0}
fmt.Printf("Sum(floats) = %.1f\n", Sum(floats)) // 7.0
prices := []Price{10.99, 20.99, 5.00}
fmt.Printf("Sum(prices) = %.2f\n", Sum(prices)) // 36.98 (underlying type float64)
}
(3) Constraint Levels
| constraint | Supported Operations | Source |
|---|---|---|
any |
All operations (unconstrained) | Built-in |
comparable |
== != |
Built-in |
constraints.Ordered |
< <= > >= |
golang.org/x/exp |
constraints.Integer |
All integer types | golang.org/x/exp |
constraints.Float |
All floating-point types | golang.org/x/exp |
| Custom | Union Type | interface { ~int | ~سلسلة } |
any is equivalent to interface{}, and comparable is a built-in constraint (no import required). constraints.Ordered, constraints.Integer, and others are located in the golang.org/x/exp/constraints package—this is experimental but has become a de facto standard. Starting with Go 1.21+, some constraints have been moved into the standard library.
5. Generic Data Structures
▶ Example: generic Stack
package main
import "fmt"
// Stack — 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, 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
}
func (s *Stack[T]) Peek() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
return s.items[len(s.items)-1], true
}
func (s *Stack[T]) IsEmpty() bool {
return len(s.items) == 0
}
func (s *Stack[T]) Size() int {
return len(s.items)
}
func main() {
// int stack
intStack := Stack[int]{}
intStack.Push(1)
intStack.Push(2)
intStack.Push(3)
for !intStack.IsEmpty() {
if val, ok := intStack.Pop(); ok {
fmt.Printf("Popped: %d\n", val)
}
}
// string stack
strStack := Stack[string]{}
strStack.Push("hello")
strStack.Push("world")
fmt.Printf("Peek: %s\n", strStack.Peek()) // world
}
▶ Example: Generic Set
package main
import "fmt"
// Set — generic collection (comparable constraint)
type Set[T comparable] struct {
items map[T]struct{}
}
func NewSet[T comparable]() *Set[T] {
return &Set[T]{items: make(map[T]struct{})}
}
func (s *Set[T]) Add(item T) {
s.items[item] = struct{}{}
}
func (s *Set[T]) Remove(item T) {
delete(s.items, item)
}
func (s *Set[T]) Contains(item T) bool {
_, ok := s.items[item]
return ok
}
func (s *Set[T]) Size() int {
return len(s.items)
}
func (s *Set[T]) Items() []T {
result := make([]T, 0, len(s.items))
for item := range s.items {
result = append(result, item)
}
return result
}
// Union — set union (طريقة receivers cannot have additional type parameters)
func Union[T comparable](a, b *Set[T]) *Set[T] {
result := NewSet[T]()
for _, item := range a.Items() {
result.Add(item)
}
for _, item := range b.Items() {
result.Add(item)
}
return result
}
// Intersection — set intersection
func Intersection[T comparable](a, b *Set[T]) *Set[T] {
result := NewSet[T]()
for _, item := range a.Items() {
if b.Contains(item) {
result.Add(item)
}
}
return result
}
func main() {
set1 := NewSet[int]()
set1.Add(1)
set1.Add(2)
set1.Add(3)
set2 := NewSet[int]()
set2.Add(3)
set2.Add(4)
set2.Add(5)
fmt.Println("Set1:", set1.Items())
fmt.Println("Set2:", set2.Items())
fmt.Println("Union:", Union(set1, set2).Items())
fmt.Println("Intersection:", Intersection(set1, set2).Items())
}
Union[T comparable](a, b *Set[T])). Additionally, generic types cannot be used directly in const declarations.
6. Type Inference and Instantiation
▶ Example: Type Inference
package main
import (
"fmt"
"strconv"
)
func Map[T, U any](input []T, fn func(T) U) []U {
result := make([]U, len(input))
for i, v := range input {
result[i] = fn(v)
}
return result
}
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func main() {
// Type inference: T=int, U=string
nums := []int{1, 2, 3, 4, 5}
strs := Map(nums, strconv.Itoa)
fmt.Println(strs) // ["1", "2", "3", "4", "5"]
// Explicitly specify type parameters (when inference fails)
explicit := Map[int, string](nums, strconv.Itoa)
fmt.Println(explicit)
// ❌ Type parameters can only be used for function params/return values, not variables
// var list List[int] ← type instantiation
// Type instantiation: create a concrete type from a generic
var intStack Stack[int]
intStack.Push(10)
}
graph TB
A[Generic function definition<br/>func Max[T Ordered](a, b T) T] --> B{Call Max(3, 5)}
B --> C[Compiler infers T = int]
C --> D[Instantiate Max[int]]
D --> E[int version: func Max(a, b int) int]
B --> F{Call Max(3.14, 2.72)}
F --> G[Compiler infers T = float64]
G --> H[Instantiate Max[float64]]
H --> I[float64 version: func Max(a, b float64) float64]
(2) Generics vs. interface{}
| Comparison | interface{} + type assertion |
generics |
|---|---|---|
| Type Safety | ❌ Runtime panic | ✅ Compile-time check |
| Performance | Boxing/unboxing (escape to heap) | ✅ Zero overhead (compile-time expansion) |
| Amount of code | One copy per type | ✅ One copy of generic code |
| Flexibility | Can store different types in the same slice | ✅ Type is determined at compile time |
| Complexity | Easy to understand | ⚠️ Complex syntax |
7. Complete Example: Generic Sorting Library
▶ Example: Full Implementation
// generic_sort.go
package main
import (
"fmt"
"sort"
"golang.org/x/exp/constraints"
)
// ---------- Sorting functions ----------
// SortSlice sorts a slice of any ordered type
func SortSlice[T constraints.Ordered](slice []T) {
sort.Slice(slice, func(i, j int) bool {
return slice[i] < slice[j]
})
}
// ReverseSort sorts in descending order
func ReverseSort[T constraints.Ordered](slice []T) {
sort.Slice(slice, func(i, j int) bool {
return slice[i] > slice[j]
})
}
// ---------- Search functions ----------
// BinarySearch performs binary search (requires sorted input)
func BinarySearch[T constraints.Ordered](slice []T, target T) (int, bool) {
low, high := 0, len(slice)-1
for low <= high {
mid := low + (high-low)/2
if slice[mid] == target {
return mid, true
} else if slice[mid] < target {
low = mid + 1
} else {
high = mid - 1
}
}
return -1, false
}
// ---------- Aggregation functions ----------
// Filter filters elements
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
}
// Reduce aggregates elements
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
result := initial
for _, v := range slice {
result = fn(result, v)
}
return result
}
type Person struct {
Name string
Age int
}
func main() {
// 1. Sort integers
ints := []int{5, 2, 8, 1, 9, 3}
SortSlice(ints)
fmt.Printf("Sorted ints: %v\n", ints)
// 2. Sort in descending order
ReverseSort(ints)
fmt.Printf("Reverse: %v\n", ints)
// 3. Sort strings
strs := []string{"banana", "apple", "cherry", "date"}
SortSlice(strs)
fmt.Printf("Sorted strings: %v\n", strs)
// 4. Binary search
idx, found := BinarySearch(ints, 5)
fmt.Printf("BinarySearch 5: idx=%d, found=%v\n", idx, found)
// 5. Filter
evens := Filter(ints, func(n int) bool { return n%2 == 0 })
fmt.Printf("Evens: %v\n", evens)
// 6. Reduce
sum := Reduce(ints, 0, func(acc, n int) int { return acc + n })
fmt.Printf("Sum: %d\n", sum)
// 7. Custom type (Person doesn't implement Ordered, can't sort directly)
// Need a custom sort function
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
// Use a closure for custom sorting
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
fmt.Printf("Sorted by age: %v\n", people)
}
sort.Slice itself is not a generic function—it accepts any slices ([]any) and works via reflection. However, when used with closures, it integrates well with generics. True generic sorting comes from slices.Sort (golang.org/x/exp/slices), a new feature introduced in Go 1.21, which is implemented entirely using generics.
❓ FAQ
func Name[T Constraint](param T) T. Type parameters are declared using square brackets [] (not angle brackets). Multiple parameters are allowed: func Map[T, U any](input []T, fn func(T) U) []U.interface { set of types }. Built-in constraints: any (any type), comparable (comparable). Third-party: constraints.Ordered (ordered). Custom: interface { ~int | ~string }.any and interface{}?any is an alias for interface{} (type any = interface{}), and the two are completely equivalent. Go 1.18 introduced generics along with any as a type alias. It is recommended to use any in generic constraints and interface{} in regular code.Max(3, 5) → infers T=int. If inference fails or you want to specify the type explicitly, you can write: Max[int](3, 5). Type parameters cannot be inferred from the return type—at least one parameter must involve a type parameter.func (s *Stack[T]) Push(item T) is valid, but func (s *Stack[T]) Convert[U any]() U is invalid. If additional type parameters are needed, use a regular function instead of a method.Stack[int] and Stack[string] are completely different types at runtime, with no boxing or unboxing. The only cost is a slight increase in compile time and larger binary size.📖 Summary
- Generic function:
func Name[T Constraint](param T) T - Type constraints:
any/comparable/Ordered/ custom - Custom constraint:
interface { ~int | ~string } - Type inference: automatically inferred by the compiler or explicitly specified
- Generic data structures:
Stack[T]/Set[T]/List[T] - Methods cannot have additional type parameters
- Zero runtime overhead (compile-time expansion)
📝 Exercises
-
Basic (Difficulty ⭐): Write a generic function
Find[T comparable](slice []T, target T) intthat returns the index oftargetinslice; iftargetdoes not exist, return -1. Verify that it works for the three types:int,string, andfloat64. -
Advanced (Difficulty ⭐⭐): Implement a generic
Queue[T any](first-in, first-out queue). Requirements: (1) Enqueue, Dequeue, Peek, and IsEmpty methods; (2) Support for any type; (3) Implement a ring buffer to avoid frequent resizing; (4) Verify concurrent safety using-race. -
Challenge (Difficulty ⭐⭐⭐): Implement a generic concurrency-safe cache
Cache[K comparable, V any]. Requirements: (1) Get/Set/Delete/Clear methods; (2) RWMutex protection; (3) TTL expiration mechanism; (4) Support for theOnEvictedcallback (called when a key is deleted or expires); (5) Use generics to ensure that the key type must be comparable.