Type Parameters

Type parameters let one function work for many types, keeping the real type at the call site instead of interface{}.

Syntaxfunc Name[T any](arg T) T

Most functions fix the type of every value they take. A function that sums a slice of int cannot be handed a slice of float64. Until Go 1.18 there were two ways round that. Copy the function once per type, or accept interface{}, which holds a value of any type but forgets which type it was, so the caller has to convert the result back. Type parameters are the third way. The function takes a type as an input, alongside its values.

Writing one

Type parameters go in square brackets after the function name. Each one has a name and a constraint, which says which types are allowed. any allows every type.

func First[T any](s []T) T { return s[0] }

Inside the body T is an ordinary type. s is a slice of T, and the result is a T. any is a predeclared alias for interface{}. The two spellings mean the same type, and either one works as a constraint.

Two ways to call it

You can supply the type argument yourself, or let the compiler work it out from the arguments. These two lines do the same thing.

a := First[int]([]int{3, 4}) // type argument written out
b := First([]int{3, 4})      // the same call, type inferred

What trips people up

any is the loosest constraint, and that limits the body. You may only do what every type permits, so +, < and == are not available. Narrowing the constraint is the next lesson.

Example

Example · go
package main

import "fmt"

// Map builds a new slice by calling f on every element of s.
// T is the type going in, U is the type coming out.
func Map[T, U any](s []T, f func(T) U) []U {
	out := make([]U, 0, len(s))
	for _, v := range s {
		out = append(out, f(v))
	}
	return out
}

func main() {
	words := []string{"go", "is", "typed"}

	// Both type arguments written out.
	long := Map[string, int](words, func(w string) int { return len(w) })

	// The same call with the types left to the compiler, which reads
	// them from words and from the function literal.
	short := Map(words, func(w string) int { return len(w) })

	fmt.Println(long)
	fmt.Println(short)

}

// Output:
// [2 2 5]
// [2 2 5]

When to use it

  • Replace a MinInt, MinFloat64 and MinDuration family with one Min function.
  • Write one Filter that serves a slice of accounts and a slice of order IDs.
  • Return a typed value from a slice helper so callers never write a type assertion.
  • Build a Ptr helper for the *string and *int fields an API request struct needs.

More examples

Filter keeps the element type

One Filter serves every element type. The important part is the return type: it is []T, not []interface{}, so live[0].ID compiles and no conversion is needed. Reach for this shape whenever you catch yourself writing filterAccounts, filterOrders and filterInvoices with identical bodies.

Example · go
package main

import "fmt"

type Account struct {
	ID     string
	Active bool
}

// Filter returns the elements for which keep reports true.
// The result is a []T, so the caller gets real Accounts back.
func Filter[T any](s []T, keep func(T) bool) []T {
	out := make([]T, 0, len(s))
	for _, v := range s {
		if keep(v) {
			out = append(out, v)
		}
	}
	return out
}

func main() {
	accounts := []Account{{ID: "a1", Active: true}, {ID: "a2"}}
	live := Filter(accounts, func(a Account) bool { return a.Active })
	fmt.Println(len(live), live[0].ID)

}

// Output:
// 1 a1

A pointer to a literal

A one-line generic function that removes a real annoyance. Structs that distinguish "not set" from "set to zero" use pointer fields, and filling those fields from literals otherwise needs a named variable for each one. You will see this helper in most codebases that build request or config structs.

Example · go
package main

import "fmt"

// Ptr returns a pointer to v. Go has no way to take the address of a
// literal, so &"eu-west-1" is not legal; this works for any type.
func Ptr[T any](v T) *T { return &v }

type Config struct {
	Region  *string
	Retries *int
}

func main() {
	c := Config{Region: Ptr("eu-west-1"), Retries: Ptr(3)}
	fmt.Println(*c.Region, *c.Retries)

}

// Output:
// eu-west-1 3

When inference has nothing to read

Two edges of inference in one file. Zero shows the case that always needs an explicit type argument, because the arguments carry no clue about T. Double shows that a generic function can be used as a plain function value once the target type pins T down, which is how generic functions are passed to callbacks and stored in maps of handlers.

Example · go
package main

import "fmt"

// Zero returns the zero value of T. T appears only in the result, so
// there is nothing for the compiler to read: the call must say [int].
func Zero[T any]() T {
	var v T
	return v
}

// Double accepts any type whose underlying type is int or float64.
func Double[T ~int | ~float64](v T) T { return v + v }

func main() {
	fmt.Println(Zero[int](), Zero[string]() == "")

	// Assigning a generic function to a variable of a matching function
	// type infers T. That has worked since Go 1.21, and Go 1.27 widened
	// it to every assignment and conversion context.
	var f func(int) int = Double
	fmt.Println(f(21))

}

// Output:
// 0 true
// 42

Discussion

  • Be the first to comment on this lesson.