Generics, briefly and honestly
Go got generics in 1.18, after a decade of arguing about whether it should.
Go got generics in 1.18, after a decade of arguing about whether it should. The result is deliberately less powerful than TypeScript's, and the community consensus is that you should use them rarely. This chapter covers enough to read other people's code and to recognise the handful of cases where writing your own is right.
The syntax
Type parameters go in square brackets before the arguments.
func First[T any](items []T) (T, bool) {var zero Tif len(items) == 0 {return zero, false}return items[0], true}n, ok := First([]int{1, 2, 3}) // T inferred as ints, ok := First([]string{"a", "b"}) // T inferred as string
T is a type parameter and any is its constraint, meaning "any type at all". Inference usually works, so you rarely write First[int](...).
Note var zero T. You cannot return nil for an arbitrary T, because Tmight be int. The zero value is the only thing you can produce without knowing the type, and this little declaration appears in almost every generic function.
If you know JavaScript
TypeScript's function first<T>(items: T[]): T | undefined is the same idea, with a much more capable type system behind it. TypeScript has conditional types, mapped types, template literal types and inference that borders on computation. Go has type parameters and constraints, and that is the lot. If you arrive expecting the TypeScript toolkit you will be disappointed, which is by design: the proposal was accepted on the promise that it would not make Go code hard to read.
Constraints are interfaces
A constraint says what you may do with a value of that type.
// Will not compile: you cannot add two values of an arbitrary type.func Sum[T any](items []T) T {var total Tfor _, v := range items {// invalid operation: operator + not defined on totaltotal += v}return total}
You must constrain T to types that support +. Constraints are written as interfaces containing type sets:
type Number interface {~int | ~int64 | ~float64}func Sum[T Number](items []T) T {var total Tfor _, v := range items {total += v}return total}Sum([]int{1, 2, 3}) // 6Sum([]float64{1.5, 2.5}) // 4.0
The ~ means "any type whose underlying type is this", so a named type still qualifies:
type Celsius float64Sum([]Celsius{20.5, 21.0}) // works because of ~float64
Without the tilde, Celsius would be rejected, which is almost never what you want. Write ~ unless you have a reason not to.
The constraints you will actually use
Two come from the standard library and cover most cases.
import "cmp"func Max[T cmp.Ordered](a, b T) T { // anything with < and >if a > b { return a }return b}
cmp.Ordered is every type supporting <, which is the numeric types andstring. comparable is built in and means "supports ==", which is what map keys require:
func Keys[K comparable, V any](m map[K]V) []K {out := make([]K, 0, len(m))for k := range m {out = append(out, k)}return out}
Generic types
Types can take parameters too.
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 Tif len(s.items) == 0 {return zero, false}v := s.items[len(s.items)-1]s.items = s.items[:len(s.items)-1]return v, true}s := &Stack[string]{}s.Push("a")v, ok := s.Pop()
Watch out
Methods cannot introduce new type parameters. This compiles nowhere:
func (s *Stack[T]) Map[U any](f func(T) U) *Stack[U] { // illegal
If you want a Map that changes the element type, it has to be a package function, not a method:
func MapStack[T, U any](s *Stack[T], f func(T) U) *Stack[U] { ... }
This single restriction is why Go will never have fluent chained collection methods like items.map(...).filter(...). It was a deliberate choice to keep the type checker simple, and it is the main reason generics changed Go code far less than people expected.
What the standard library gives you
Since 1.21 there are generic helpers, and you should use these rather than write your own.
import ("slices"; "maps")slices.Contains(s, v) // boolslices.Index(s, v) // int, -1 if absentslices.Sort(s) // for cmp.Orderedslices.SortFunc(s, cmp) // for anythingslices.Reverse(s)slices.Max(s); slices.Min(s)slices.Clone(s)slices.Equal(a, b)maps.Keys(m) // an iterator, not a slicemaps.Values(m)maps.Clone(m)
Watch out
maps.Keys returns an iterator (iter.Seq[K]), not a slice. To get a slice, collect it:
keys := slices.Collect(maps.Keys(m))
This trips up anybody reading older blog posts, where an earlier experimental version did return a slice.
When to write your own, and when not to
The honest guidance, which matches what the Go team says: *write the concrete version first*. Write the generic version when you have written the same function for the third type and the bodies are identical.
Good reasons:
- A container: a stack, a set, a typed cache.
- A function over any slice or map where the body does not care about the type.
- Removing an
anyand the type assertions that came with it.
Bad reasons:
- "It might be reused later." It usually is not.
- To avoid writing a four-line loop. The loop is clearer.
- To model a hierarchy. Go has interfaces for that, and they are better at it.
If you know JavaScript
The decisive difference from TypeScript: TypeScript's generics are erased at runtime and cost nothing, so using them liberally is free. Go's are compiled, and a generic function is often slower than the concrete one because of how the compiler shares instantiations for pointer-shaped types. If you are reaching for generics for performance, measure. The usual answer is that the concrete function wins.
Interfaces are still usually the answer
The mistake to avoid is reaching for a type parameter where an interface does the job better.
// Generic: the caller must have a concrete type, and the body can only do// what the constraint allows.func PrintAll[T fmt.Stringer](items []T) {for _, v := range items {fmt.Println(v.String())}}// Interface: accepts a mixed slice of different types. Usually better.func PrintAll(items []fmt.Stringer) {for _, v := range items {fmt.Println(v.String())}}
The rule of thumb: if the function's behaviour depends only on methods, use an interface. Use a type parameter when you need the actual type back out, which is what interfaces cannot do.
Project 16: a typed result cache
A small generic container that is a genuinely good use of type parameters: the body does not care what it stores, and the caller gets its type back with no assertion.
package mainimport ("fmt""sync""time")type cached[V any] struct {value Vexpires time.Time}// Cache is generic over both key and value. K must be comparable because// it is a map key; V can be anything at all.type Cache[K comparable, V any] struct {mu sync.RWMutexitems map[K]cached[V]ttl time.Duration}func NewCache[K comparable, V any](ttl time.Duration) *Cache[K, V] {return &Cache[K, V]{items: map[K]cached[V]{}, ttl: ttl}}func (c *Cache[K, V]) Set(k K, v V) {c.mu.Lock()defer c.mu.Unlock()c.items[k] = cached[V]{value: v, expires: time.Now().Add(c.ttl)}}func (c *Cache[K, V]) Get(k K) (V, bool) {c.mu.RLock()e, ok := c.items[k]c.mu.RUnlock()var zero Vif !ok || time.Now().After(e.expires) {return zero, false}return e.value, true}// GetOrLoad is where the design earns itself: the loader returns a V and// the caller receives a V, with no any and no type assertion anywhere.func (c *Cache[K, V]) GetOrLoad(k K,load func(K) (V, error)) (V, error) {if v, ok := c.Get(k); ok {return v, nil}v, err := load(k)if err != nil {var zero Vreturn zero, err}c.Set(k, v)return v, nil}type User struct {ID intName string}func main() {users := NewCache[int, User](time.Minute)counts := NewCache[string, int](time.Minute)calls := 0loadUser := func(id int) (User, error) {calls++return User{ID: id, Name: fmt.Sprintf("user-%d", id)}, nil}u, _ := users.GetOrLoad(1, loadUser)fmt.Println(u.Name, "loader calls:", calls) // user-1 1u, _ = users.GetOrLoad(1, loadUser)fmt.Println(u.Name, "loader calls:", calls) // user-1 1 (cached)// u is a User, not an any. This compiles, and would not if the cache// stored any.fmt.Println("id plus one:", u.ID+1)counts.Set("hits", 42)n, _ := counts.Get("hits")fmt.Println("count doubled:", n*2) // 84, n is an int}
Now prove the value. Rewrite Cache with map[any]any and no type parameters, then try u.ID+1: you need u.(User).ID+1, an assertion that can panic, and the compiler no longer checks that you put a User in and got a User out. That assertion, repeated at every call site, is exactly what generics removed.
Then try to add a Map method that converts a Cache[K, V] into aCache[K, W] and watch the compiler refuse, because methods cannot introduce type parameters. Make it a package function instead and notice how that constraint pushes Go code away from fluent chaining.
Your turn
Add a Keys() []K method, then write a package-level Filter[K comparable, V any](c *Cache[K, V], keep func(V) bool) []V. Then stop and ask whether either of them earns its place, or whether the caller ranging over a plain map would have been clearer. That question, asked honestly every time, is the whole discipline of generics in Go.
Assignment
Commit your work with the message Generics - Assignment One, exactly as written, so the leaderboard can score it.
Try it
Write the generic versions of what you already wrote, then judge them.
In part2-ideas/17-generics:
One. Rewrite chapter six's Filter, Chunk and Dedupe to work for any element type. Dedupe needs comparable, which is the constraint doing real work.
Two. A generic Set[T comparable] with Add, Has and Items.
Three. A Map[T, U any] that changes the element type, and one use of it that would have needed any and a cast before.
Four. Now the honest part. In NOTES.md, name one place in your own code where an interface is the better answer than a type parameter, and say why. A chapter on generics that does not leave you more careful has failed.
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled, suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub