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
~intandint | 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 : IComparable | func F[T cmp.Ordered](...) |
where constraints | Constraint interfaces |
where T : class / struct | No 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
| Constraint | Allowed operations | Example |
|---|---|---|
any | Assignment, passing as an argument, etc. No operators | Element type of a container |
comparable | == / !=, use as a map key | Set[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). anyrequires nothing,comparableallows==and use as amapkey, andcmp.Orderedallows 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)andnew T()are unavailable - Methods cannot declare additional type parameters
- There is no covariance or contravariance
- Use
var zero Tinstead ofdefault(T)
- Types are not reified at runtime, so
- 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"
- When an interface is sufficient (if you can abstract by behavior, as with
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.