Generic Types

Give a struct or map a type parameter to get a container that holds one chosen type, with inference handled at the call site.

Syntaxtype Stack[T any] struct{ items []T }

Type declarations take type parameters too. A generic type is a struct, map, slice or other type with a placeholder for one or more of the types inside it. It gives you a container that holds one chosen type, checked when the program is compiled, with no conversions when values come back out.

Declaring one

type Stack[T any] struct {
	items []T
}

A generic type must be instantiated every time it is named: given its type arguments, as in Stack[int], var s Stack[string] or []Stack[byte]. Stack on its own is not a type.

Methods

The receiver repeats the type parameters, which is what makes them usable in the method body. The constraints are not repeated, because they come from the type declaration, and the names do not have to match it.

func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }

Since Go 1.27 a method may also declare type parameters of its own, after its name, for a type the method introduces. Before Go 1.27 that had to be a package-level function. These two do the same job.

func (s *Stack[T]) Map[U any](f func(T) U) *Stack[U] // Go 1.27
func Map[T, U any](s *Stack[T], f func(T) U) *Stack[U] // any version

Inference at the call site

Type arguments are inferred for function calls, never for a type. A constructor function therefore saves repeating yourself: NewStack(1, 2, 3) gives a *Stack[int], while naming the type directly needs &Stack[int]{}.

Example

Example · go
package main

import "fmt"

// Stack holds values of one type. T is fixed when a Stack is created.
type Stack[T any] struct {
	items []T
}

// NewStack exists so the call site can infer T from its arguments.
func NewStack[T any](items ...T) *Stack[T] {
	return &Stack[T]{items: items}
}

// The receiver repeats the type parameter: Stack[T], not Stack.
func (s *Stack[T]) Push(v T) {
	s.items = append(s.items, v)
}

// Pop reports whether there was anything to return.
func (s *Stack[T]) Pop() (T, bool) {
	if len(s.items) == 0 {
		var zero T // the zero value of whatever T is
		return zero, false
	}
	last := len(s.items) - 1
	v := s.items[last]
	s.items = s.items[:last]
	return v, true
}

func main() {
	nums := NewStack(1, 2, 3) // T inferred as int
	nums.Push(4)
	fmt.Println(nums.Pop())

	var words Stack[string] // no arguments to infer from, so spelled out
	words.Push("ada")
	fmt.Println(words.Pop())

}

// Output:
// 4 true
// ada true

When to use it

  • A Stack or Queue that holds one type, with no type assertions on the way out.
  • A Result[T] value so a function can hand back either a value or an error.
  • A Cache[K, V] whose key and value types are checked when the program is built.
  • A Set[T] built on map[T]struct{} and shared between packages.

More examples

A generic type alias

Shorthand and longhand for the same type. A generic alias, supported fully since Go 1.24, gives a readable name to a shape you write often without introducing a separate type, so values pass freely between code that uses the alias and code that writes the map out in full. Use a defined type instead (type Set[T comparable] map[T]struct{}) when you want methods on it, since an alias cannot have its own.

Example · go
package main

import (
	"fmt"
	"slices"
)

// Go 1.24 allows a type alias to take type parameters. An alias is a
// second name for the same type: Set[string] IS map[string]struct{}.
type Set[T comparable] = map[T]struct{}

func Add[T comparable](s Set[T], v T) { s[v] = struct{}{} }

func main() {
	tags := Set[string]{}
	Add(tags, "go")
	Add(tags, "go")

	// Because it is an alias and not a new type, a plain map variable
	// takes the value with no conversion.
	var same map[string]struct{} = tags

	keys := make([]string, 0, len(same))
	for k := range same {
		keys = append(keys, k)
	}
	slices.Sort(keys) // map order varies, so sort before printing
	fmt.Println(len(same), keys)

}

// Output:
// 1 [go]

Two type parameters, and a generic method

Swap shows the receiver re-declaring type parameters under different names and returning a differently ordered instantiation of its own type. MapSecond shows the Go 1.27 addition: a method introducing a type the receiver knows nothing about. Keep the package-level form in mind as well, because it is what every codebase written before Go 1.27 uses, and it is still required if the method has to satisfy an interface.

Example · go
package main

import "fmt"

// Pair holds two values whose types are chosen separately.
type Pair[A, B any] struct {
	First  A
	Second B
}

// The receiver re-declares the type parameters. The names need not
// match the ones in the type declaration; position is what counts.
func (p Pair[X, Y]) Swap() Pair[Y, X] {
	return Pair[Y, X]{First: p.Second, Second: p.First}
}

// Go 1.27: a method may declare a type parameter of its own. C is new
// here, so before Go 1.27 this had to be a package-level function.
func (p Pair[A, B]) MapSecond[C any](f func(B) C) Pair[A, C] {
	return Pair[A, C]{First: p.First, Second: f(p.Second)}
}

func main() {
	p := Pair[string, int]{First: "qty", Second: 3}
	fmt.Println(p.Swap())
	fmt.Println(p.MapSecond(func(n int) string { return fmt.Sprint(n * 2) }))

}

// Output:
// {3 qty}
// {qty 6}

A value or an error, as one type

A function returning (T, error) cannot be stored in a slice or sent anywhere as one item; a Result[T] can. The pattern is worth knowing for code that collects outcomes from many pieces of work and inspects them later. Note Fail[int]: its type parameter appears only in the return type, so inference has nothing to work from and the type argument is written out.

Example · go
package main

import (
	"errors"
	"fmt"
)

// Result carries either a value or the error that prevented it.
type Result[T any] struct {
	Value T
	Err   error
}

func Ok[T any](v T) Result[T] { return Result[T]{Value: v} }

// T appears only in Fail's result, so callers must supply it.
func Fail[T any](err error) Result[T] { return Result[T]{Err: err} }

func main() {
	rs := []Result[int]{Ok(2), Fail[int](errors.New("timeout"))}
	for _, r := range rs {
		if r.Err != nil {
			fmt.Println("error:", r.Err)
			continue
		}
		fmt.Println("value:", r.Value)
	}

}

// Output:
// value: 2
// error: timeout

Discussion

  • Be the first to comment on this lesson.