Skip to main content

Generics

For developers with .NET (C#) experience, this guide explains Go generics (introduced in Go 1.18): type parameters, constraints, comparable, and how they differ from C# generics. The sample code assumes Go 1.21 or later (it uses the cmp and slices packages).

1. What This Document Covers​

  • Writing type parameters and constraints (func F[T any](...), type Stack[T any] struct{...})
  • When to use cmp.Ordered / comparable / any
  • Constraints based on type sets, such as ~int and int | string
  • Design and implementation differences from C# generics
  • When to use and when to avoid generics

2. .NET Mapping​

.NET (C#)Go
List<T> where T : IComparablefunc F[T cmp.Ordered](...)
where constraintsConstraint interfaces
where T : class / structNo equivalent constraint (enumerate types with a type set if needed)
where T : new()No equivalent (get the zero value with var zero T)
default(T)var zero T
Operators on T (INumber<T> etc. since .NET 7)Constraints with a type set (e.g. ~int | ~float64) allow + and < directly
Key of Dictionary<TKey, TValue>K in map[K]V must be comparable
Reified at runtime (type information is kept at runtime)Monomorphized per GC shape at compile time, with type information passed via dictionaries (GC shape stenciling)
Covariance / contravariance (IEnumerable<out T>)None (invariant)
Generic methods (void M<T>())Functions can be generic, but methods cannot declare additional type parameters
LINQ (Select / Where)The slices / maps packages, or your own Map / Filter

3. Common Pitfalls for C# Developers​

Constraints are written as interfaces

In C#, you list constraints such as where T : IComparable<T>. In Go, the constraint itself is an interface type. It can define not only methods but also the set of types that may be used (a type set).

// Constraint via methods (similar to C# where T : IStringer)
type Stringer interface {
String() string
}

// Constraint via a type set (no equivalent in C#)
type Number interface {
~int | ~int64 | ~float64
}

~int means "every type whose underlying type is int." A custom type such as type UserID int satisfies ~int. Plain int without the ~ accepts only int itself.

Differences between any, comparable, and cmp.Ordered

ConstraintAllowed operationsExample
anyAssignment, passing as an argument, etc. No operatorsElement type of a container
comparable== / !=, use as a map keySet[T], duplicate detection
cmp.Ordered< <= > >= (plus ==)Sorting, max / min

You cannot use == or < on a type parameter constrained by any. In C# you can call Equals on an object, but in Go any operation not permitted by the constraint is a compile error.

Methods cannot declare additional type parameters

Only functions and types can have type parameters. A method of a generic type can use only the type parameters declared on the receiver type.

type Stack[T any] struct{ items []T }

// Compile error: methods cannot have their own type parameters
// func (s *Stack[T]) Map[U any](f func(T) U) []U { ... }

// Write it as a top-level function instead
func MapStack[T, U any](s *Stack[T], f func(T) U) []U { /* ... */ return nil }

For things like C# extension methods or Select<TSource, TResult>, the standard approach in Go is to write a function.

Use var zero T instead of new T() or default(T)

func Zero[T any]() T {
var zero T // 0 for int, "" for string, nil for a pointer
return zero
}

There is no equivalent of the where T : new() constraint. If you need to create instances, accept a factory function as an argument.

No covariance or contravariance

A []int cannot be assigned to []any. There is also no conversion like treating IEnumerable<Derived> as IEnumerable<Base> in C#. If needed, convert the elements one by one.

Runtime type information is not directly available

In C# you can get T at runtime with typeof(T), but in Go a type parameter cannot be used as a value. If you want to branch on the type, convert the value to any and use a type switch. However, overusing this undermines the point of using generics.

func Describe[T any](v T) string {
switch x := any(v).(type) {
case int:
return fmt.Sprintf("int: %d", x)
case string:
return "string: " + x
default:
return fmt.Sprintf("other: %v", x)
}
}

Type inference and explicit type arguments

When type arguments can be inferred from the function arguments, you can omit them. You need to specify them explicitly when, for example, T appears only in the return type.

Max(1, 2) // T is inferred as int
Zero[int]() // cannot be inferred, so specify it explicitly

Implementation strategy and performance

Go does not fully monomorphize per type. Instead, types with the same memory representation (GC shape) share code, and type-specific information is passed through a dictionary. As a result, using generics is not always faster than using interfaces. Check performance-critical code with benchmarks.

4. Code Examples​

A function that returns the maximum value (cmp.Ordered)

C#:

static T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) > 0 ? a : b;

var m = Max(3, 5); // 5

Go:

import "cmp"

func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}

m := Max(3, 5) // 5
s := Max("apple", "go") // "go"

Since Go 1.21 there are also built-in max / min functions, which are enough for simple comparisons.

Map / Filter (equivalent to LINQ Select / Where)

C#:

var squares = numbers.Select(n => n * n).ToList();
var evens = numbers.Where(n => n % 2 == 0).ToList();

Go:

func Map[T, U any](s []T, f func(T) U) []U {
result := make([]U, 0, len(s))
for _, v := range s {
result = append(result, f(v))
}
return result
}

func Filter[T any](s []T, pred func(T) bool) []T {
var result []T
for _, v := range s {
if pred(v) {
result = append(result, v)
}
}
return result
}

numbers := []int{1, 2, 3, 4, 5}
squares := Map(numbers, func(n int) int { return n * n })
evens := Filter(numbers, func(n int) bool { return n%2 == 0 })

The standard library slices package also provides slices.Contains, slices.Index, slices.Sort, slices.SortFunc, slices.Max, and more. Check whether the standard library already covers your need first.

A generic type (Stack<T>)

C#:

public class Stack<T>
{
private readonly List<T> _items = new();

public void Push(T item) => _items.Add(item);

public bool TryPop(out T? item)
{
if (_items.Count == 0) { item = default; return false; }
item = _items[^1];
_items.RemoveAt(_items.Count - 1);
return true;
}
}

Go:

type Stack[T any] struct {
items []T
}

func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}

func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
last := len(s.items) - 1
v := s.items[last]
s.items = s.items[:last]
return v, true
}

var st Stack[string]
st.Push("a")
v, ok := st.Pop() // "a", true

A method receiver redeclares the type parameters, as in Stack[T] (the constraints from the type definition apply).

A Set using comparable (equivalent to C# HashSet<T>)

C#:

var set = new HashSet<string>();
set.Add("go");
bool has = set.Contains("go");

Go:

type Set[T comparable] map[T]struct{}

func (s Set[T]) Add(v T) { s[v] = struct{}{} }
func (s Set[T]) Has(v T) bool { _, ok := s[v]; return ok }

set := Set[string]{}
set.Add("go")
has := set.Has("go") // true

Because map keys must be comparable, you add the constraint as Set[T comparable]. Leaving it as T any results in a compile error.

Summation using a type-set constraint (~)

C# (generic math in .NET 7 and later):

static T Sum<T>(IEnumerable<T> xs) where T : INumber<T>
=> xs.Aggregate(T.Zero, (acc, x) => acc + x);

Go:

type Number interface {
~int | ~int64 | ~float64
}

func Sum[T Number](xs []T) T {
var total T
for _, x := range xs {
total += x
}
return total
}

type Price int

Sum([]int{1, 2, 3}) // 6
Sum([]float64{1.5, 2.5}) // 4
Sum([]Price{100, 200}) // 300 (~int also accepts custom types)

5. Summary​

  • Go generics let you add type parameters to functions and types. Constraints are expressed as interfaces, and you can specify not only methods but also type sets (~int | ~float64).
  • any requires nothing, comparable allows == and use as a map key, and cmp.Ordered allows ordering comparisons. Choose the minimum constraint you need.
  • The main differences from C# are as follows:
    • Types are not reified at runtime, so typeof(T) and new T() are unavailable
    • Methods cannot declare additional type parameters
    • There is no covariance or contravariance
    • Use var zero T instead of default(T)
  • When to use generics
    • General-purpose algorithms over slices, maps, and channels (Map / Filter / Reduce, sorting, searching)
    • Containers that differ only in element type (Stack, Set, Queue, caches, etc.)
    • Writing the same logic for multiple numeric or ordered types
  • When to avoid generics
    • When an interface is sufficient (if you can abstract by behavior, as with io.Reader, an interface is more idiomatic Go)
    • Premature abstraction when only one type is ever used
    • When the implementation differs significantly per type (if you end up with type switches everywhere, write separate functions)
    • Using them only because "generics should be faster"

A sensible approach in Go is to start by writing code with concrete types, and switch to generics once the same logic starts to be duplicated across multiple types.

References​