Irbisa · Cheatsheet

Generics & Type Parameters

Type parameters, constraints, type sets, inference, GC shape stenciling

Middle Developer · 19 questions

  1. 01

    What do generics give you over any (interface{}), and when is any still the right choice?

    Generics let one piece of code work over many types while the compiler still checks them: a Stack[int] accepts only int, needs no type assertions and stores values inline, while a []any accepts anything and checks nothing until run time.

  2. 02

    How do you declare a generic function and a generic type in Go, say Map, Filter and a Stack[T] with methods?

    Type parameters go in square brackets before the ordinary parameters (func Map[T, U any](xs []T, f func(T) U) []U) and after a type's name (type Stack[T any] struct{ items []T }).

    func Map[T, U any](xs []T, f func(T) U) []U {	out := make([]U, 0, len(xs))	for _, x := range xs {		out = append(out, f(x))	}	return out…
  3. 03

    This Max does not compile. Why, and which constraint fixes it?

    func Max[T any](a, b T) T {	if a > b {		return a	}	return b}

    any promises nothing about T, so the body may use only operations that every type supports, and > is not one of them: slices and structs have no order.

  4. 04

    How do you return the zero value of a type parameter T from a generic function, and why do nil, 0 and T{} not work?

    Declare var zero T and return it: a type parameter has no literal for its zero value, because nil, 0 and "" each fit only some types.

    // Find returns the first match. The bool is the answer; the T is only the payload.func Find[T any](xs []T, ok func(T) bool) (T, bool) {	for _, x := range xs {		if ok(x) {			return x, true		}…
  5. 05

    What does the ~ mean in a constraint like ~string, and why does [T string] reject a type UserID string?

    The ~ means "underlying type": ~string admits every named type built on string, while a bare string admits only string itself.

    type UserID string // wrong: the type set is exactly {string}; UserID is a different named typefunc FirstBad[T string](xs []T) T { return xs[0] } // right: ~string admits every type whose underlying type is string…
  6. 06

    Which inputs does Go use to infer type arguments, and why does a constructor like NewSet() still have to be written NewSet[string]()?

    Inference uses the types of the arguments you pass, never the type the result is assigned to.

    type Set[T comparable] struct{ m map[T]struct{} } func NewSet[T comparable]() *Set[T] { return &Set[T]{m: map[T]struct{}{}} } func Map[S, T any](xs []S, f func(S) T) []T {	out := make([]T, 0, len(xs))…
  7. 07

    Why can type Number interface{ ~int | ~float64 } be used only as a constraint and never as var n Number, and where does cmp.Ordered come from?

    An interface that lists types (a union, or any ~ term) is a constraint only and can never be the type of a value.

    type Integer interface {	~int | ~int8 | ~int16 | ~int32 | ~int64} type Float interface{ ~float32 | ~float64 }…
  8. 08

    Cents and Micros both have String(), yet v.String() does not compile for T interface{ Cents | Micros }. Why, and what may a union term be?

    A type parameter has only the methods its constraint declares, and a union declares types, not methods; the String both members happen to have is invisible.

    type Cents int64type Micros int64 func (c Cents) String() string  { return fmt.Sprintf("%d.%02d", c/100, c%100) }func (m Micros) String() string { return fmt.Sprintf("%d.%06d", m/1e6, m%1e6) }…
  9. 09

    What does this print, and why does the switch need any(v) rather than v.(type)?

    type MyInt int func Describe[T any](v T) string {	switch any(v).(type) {	case int:		return "int"	case fmt.Stringer:		return "stringer"	default:		return "other"	}} // in main:fmt.Println(Describe(42), Describe(MyInt(42)), Describe(time.Second))

    It prints int other stringer.

  10. 10

    Find the bug: OrDefault never falls back to def here, and the next line panics.

    // OrDefault returns def when v is unset.func OrDefault[T any](v, def T) T {	if any(v) == nil {		return def	}	return v} var cfg *Config // not loaded yetc := OrDefault(cfg, &Config{Timeout: 5 * time.Second})fmt.Println(c.Timeout)

    any(v) == nil is true only for a nil interface.

  11. 11

    Index[T comparable](s []T, v T) accepts a []any, compiles, then panics with comparing uncomparable type []int. Why didn't comparable prevent it?

    comparable only promises that == compiles.

  12. 12

    Find the bug: this typed sync.Map wrapper panics the first time each user id is requested.

    // Cache is a typed wrapper over sync.Map.type Cache[K comparable, V any] struct {	m sync.Map} func (c *Cache[K, V]) Put(k K, v V) { c.m.Store(k, v) } func (c *Cache[K, V]) Get(k K) (V, bool) {	v, ok := c.m.Load(k)	return v.(V), ok} user, ok := users.Get(id)if !ok {	user = loadUser(id)}

    On a miss Load returns a nil any, and the single-value assertion v.(V) panics with interface conversion: interface {} is nil, not main.User before the caller ever sees ok == false.

  13. 13

    This leaderboard sort returns rows with equal scores in an arbitrary order. What is wrong with the comparison function?

    type Row struct {	Name  string	Score int} slices.SortFunc(rows, func(a, b Row) int {	if a.Score < b.Score {		return -1	}	return 1})

    It never returns 0, so two rows with the same score each claim to be greater than the other.

  14. 14

    How do you write a range-over-func iterator with iter.Seq, what does yield returning false mean, and where does cleanup go?

    An iter.Seq[V] is a func(yield func(V) bool): you call yield for each value and stop as soon as it returns false, which means the loop body did break or return.

    // Cleanup belongs to the iterator: it opened the file, so it closes it.func Lines(name string) iter.Seq2[string, error] {	return func(yield func(string, error) bool) {		f, err := os.Open(name)		if err != nil {			yield("", err)…
  15. 15

    When is a type parameter the wrong tool, and how do you choose between generics, an interface and plain concrete types?

    Use a type parameter when the code is the same for every type and only the element type changes: containers, slices-style algorithms, operators like +.

  16. 16

    Scale[E ~int32](s []E, c E) []E accepts your type Point []int32 but returns a plain []int32. What signature keeps Point, and why does the call site not change?

    Parameterise the slice as well (func Scale[S ~[]E, E ~int32](s S, c E) S), and the result type is whatever the caller passed.

    type Point []int32 func (p Point) Norm() int32 {	var n int32	for _, v := range p {		n += v * v…
  17. 17

    Before Go 1.27 a method could not declare its own type parameters, as in func (s *Set[T]) Map[U comparable](f func(T) U) *Set[U]. Why, and what changed in Go 1.27?

    Methods are how types satisfy interfaces, and a generic method would need a body for every U while interfaces are checked at run time.

    type Set[T comparable] struct{ m map[T]struct{} } func NewSet[T comparable]() *Set[T] { return &Set[T]{m: map[T]struct{}{}} } // fine: the receiver's own parameterfunc (s *Set[T]) Add(v T) { s.m[v] = struct{}{} }…
  18. 18

    Decode[T Validator] rejects Row because Validate has a pointer receiver, and Decode[*Row] unmarshals into a nil pointer. How do you constrain *T instead?

    Take two type parameters ([T any, PT interface{ *T; Validator }]) so the value stays a T while the methods come from *T, which you reach inside the body as PT(&v).

    type Validator interface{ Validate() error } type Row struct{ N int } // pointer receiver: in *Row's method set, not Row'sfunc (r *Row) Validate() error {…
  19. 19

    How does the Go compiler implement generics (GC-shape stenciling with dictionaries), and when is a generic function slower than the interface version?

    Go compiles a generic function once per GC shape (roughly the underlying memory layout, with all pointer types sharing one shape) and passes each instantiation a hidden dictionary of type information.

    type A struct{ p *int }type B struct{ q *string } func First[T any](xs []T) T { return xs[0] } func Sum[T ~int | ~float64](xs []T) T {…