Skip to content
JavaGo
Part V · Generics & Iterators

Generics

Type parameters on functions, types and (since Go 1.27) methods; constraints with interfaces, unions and ~; and what Go generics deliberately leave out.

Section 20 of 31 6 min read

Generics arrived in Go 1.18 (2022). They let you write functions and types that work across many concrete types with full compile-time checking. They are deliberately smaller than Java’s: no wildcards, no variance annotations, no raw types. And, unlike Java, no type erasure: inside a generic function T is a real type, so var zero T and make([]T, n) just work.

Generic functions

Java — generic method
static <T, R> List<R> map(List<T> in, Function<T, R> fn) {
    List<R> out = new ArrayList<>(in.size());
    for (T v : in) {
        out.add(fn.apply(v));
    }
    return out;
}

List<String> topics = map(messages, Message::topic);
List<String> strs = map(List.of(1, 2, 3), String::valueOf);
Go — type parameters after the name
func Map[T, R any](in []T, fn func(T) R) []R {
    out := make([]R, 0, len(in))
    for _, v := range in {
        out = append(out, fn(v))
    }
    return out
}

// Type arguments are inferred from the call
topics := Map(messages, func(m Message) string { return m.Topic })
strs := Map([]int{1, 2, 3}, strconv.Itoa)

Generic types

Java — generic class
record Page<T>(List<T> items, String nextCursor) {
    boolean hasMore() {
        return nextCursor != null;
    }
}

Page<User> users = new Page<>(list, "abc");
Go — generic struct
type Page[T any] struct {
    Items      []T    `json:"items"`
    NextCursor string `json:"next_cursor,omitempty"`
}

// Methods use the type's parameter
func (p Page[T]) HasMore() bool {
    return p.NextCursor != ""
}

users := Page[User]{Items: list, NextCursor: "abc"}

Constraints

Java — bounded type parameters
// Upper bound: T must implement Comparable<T>
static <T extends Comparable<T>> T max(List<T> xs) { ... }

// Numeric code needs Number and a conversion
static <T extends Number> double sum(List<T> xs) {
    double total = 0;
    for (T x : xs) total += x.doubleValue();
    return total;
}
Go — interfaces as constraints
// comparable: supports == and != (can be a map key)
func Index[T comparable](xs []T, want T) int { ... }

// cmp.Ordered: supports < <= > >= (integers, floats and strings)
func Max[T cmp.Ordered](xs []T) T { ... }

// A union constraint lists the allowed types.
// ~int means "int, or any type whose underlying type is int".
type Number interface {
    ~int | ~int32 | ~int64 | ~float32 | ~float64
}

func Sum[T Number](xs []T) T {
    var total T // the zero value of T
    for _, x := range xs {
        total += x // + works: every type in the set supports it
    }
    return total
}

Each constraint is a set of types

The tilde admits types built on them

Fig. 20.1A constraint describes a set of types, and each narrower constraint sits inside a wider one. Unions with ~ match on the underlying type, so type Celsius float64 qualifies wherever ~float64 is listed.

Generic methods (Go 1.27+)

Java — generic instance method
class Cache {
    private final Map<String, Object> items = new HashMap<>();

    <T> Optional<T> get(String key, Class<T> type) {
        return Optional.ofNullable(items.get(key))
            .filter(type::isInstance)
            .map(type::cast);
    }
}
Go 1.27 — methods with their own type parameters
type Cache struct {
    items map[string]any
}

// Allowed since Go 1.27. Earlier versions required a
// package-level function: func Get[T any](c *Cache, key string)
func (c *Cache) Get[T any](key string) (T, bool) {
    v, ok := c.items[key].(T)
    return v, ok
}

n, ok := cache.Get[int]("retries")

Recursive constraints (Go 1.26+)

the Go version of <T extends Comparable<T>>
// A constraint that refers to the type being constrained.
// Before Go 1.26 a generic type could not refer to itself here.
type Ordered[T Ordered[T]] interface {
    Less(T) bool
}

func Min[T Ordered[T]](a, b T) T {
    if b.Less(a) {
        return b
    }
    return a
}

type Version struct{ Major, Minor int }

func (v Version) Less(o Version) bool {
    return v.Major < o.Major || (v.Major == o.Major && v.Minor < o.Minor)
}

oldest := Min(Version{1, 4}, Version{1, 2}) // {1 2}

What Go generics leave out

JavaGo
List<? extends Number> wildcardsNone: use a type parameter or an interface
Covariance: a List<Integer> is not a List<Number>Same in Go: []Dog is never a []Animal
Type erasure: no new T(), no T[]No erasure: var zero T, make([]T, n), any(v).(T) all work
Primitive type arguments need boxingAny type works: Max[int] operates on real ints
Specialisation via overloadingNo overloading or specialisation; use a type switch if you must

When to use generics

Use them for data structures (a typed cache, set or tree) and algorithms over collections (sort, filter, group). Prefer an ordinary interface when you only call methods on the value: func Save(w io.Writer) is simpler than func Save[W io.Writer](w W). A useful rule: write the concrete version first, and reach for type parameters when you find yourself copying it for a second type.

↑ ↓ to navigateEnter to openEsc to close