liveBook · Manning

Generics

As explained in 100 Go Mistakes and How to Avoid Them

A programming-language feature that allows code and types to be parameterized over type parameters.

Need the same operation for several types? Copying a function creates boilerplate, while accepting any may push type checks to run time. Generics let a function or type use type parameters that are fixed when it is instantiated. section 2.9.1

func getKeys(m any) ([]any, error) {                      #1
    switch t := m.(type) {
    default:
        return nil, fmt.Errorf("unknown type: %T", t)     #2
    case map[string]int:
        var keys []any
        for k := range t {
            keys = append(keys, k)
        }
        return keys, nil
    case map[int]string:
        // Copy the extraction logic
    }
}

The type-switch version repeats key-extraction logic, rejects unsupported maps at run time, and returns []any, leaving callers to inspect or convert the keys. section 2.9.1

func getKeys[K comparable, V any](m map[K]V) []K {   #1
    var keys []K                                     #2
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

One getKeys implementation accepts maps with different key and value types while returning []K. K must be comparable because a map cannot use every type as a key; V can be any. section 2.9.1

A type argument can be inferred from a call such as getKeys(m), or supplied explicitly as getKeys[string](m). Instantiation happens at compile time. Constraints can also narrow accepted types: a custom constraint may list ~int | ~string, and constraints can require methods. Type parameters work on types such as Node[T], but methods cannot declare their own type parameters. section 2.9.1

Choose the abstraction for a concrete need:

  • Use type parameters to reuse data structures or functions across element types, such as linked lists and channel-merging functions. section 2.9.2
  • Use them to factor out behavior when it avoids separate functions for each type, as with a slice and its comparison function. section 2.9.2
  • Keep an interface when the code only calls its methods: a function that just calls Write gains nothing from a type parameter constrained to io.Writer. Reconsider generics when they make the code less clear. section 2.9.2
type SliceFn[T any] struct {    #1
    S       []T
    Compare func(T, T) bool     #2
}
 
func (s SliceFn[T]) Len() int           { return len(s.S) }
func (s SliceFn[T]) Less(i, j int) bool { return s.Compare(s.S[i], s.S[j]) }
func (s SliceFn[T]) Swap(i, j int)      { s.S[i], s.S[j] = s.S[j], s.S[i] }

SliceFn[T] holds both a slice and a comparison function. Its methods satisfy sort.Interface, so the same wrapper can sort slices of different element types through sort.Sort. section 2.9.2

How 100 Go Mistakes and How to Avoid Them covers it

How deep it goes
Explains compile-time instantiation, constraints, generic types, and method limitations; leaves channel merging as a function signature.
Examples
Contrasts type-switched and generic map-key extraction, then sorts a slice through a parameterized comparison wrapper.
What it assumes
Assumes familiarity with Go maps, slices, and interfaces while defining type parameters, constraints, and instantiation.
What you can do after
Build it from scratch: replace duplicated map-key extraction with a constrained generic function, and retain interfaces when only method calls matter.
How it connects
Connects type parameters to interfaces, constraints, maps, slices, channels, and sorting.

Also explained in

Compare how 20 books explain generics →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of 100 Go Mistakes and How to Avoid Them. The section links point to the parts of the book it draws on.