Go Module 11 – Generics, including generic methods (1.27)
Type parameters: generic functions and inference
Write generic functions in Go with type parameters and constraints, see when Go infers the type arguments, and choose generics, an interface or plain code.
What you will learn
- Write a generic function with a type parameter list and call it with several element types
- Predict the type arguments Go infers, and write them where it cannot
- Explain why a comparable type parameter can still panic when its type argument is an interface
- Choose between a type parameter, an interface and one concrete type for a function
Before you start
What you’ll learn · Before you start
What you will learn
- Write a generic function with a type parameter list and call it with several element types
- Predict the type arguments Go infers, and write them where it cannot
- Explain why a comparable type parameter can still panic when its type argument is an interface
- Choose between a type parameter, an interface and one concrete type for a function
Before you start
On this page
A booking service keeps three slices: fares in paise as int64, PNRs as strings and empty seats as int. Filtering
each one by a rule took three copies of the same six-line loop before Go 1.18, one per element type, or one loop over
[]any that the compiler could not check.
A type parameter: A name declared in square brackets with a constraint, such as T in func Filter[T any](xs []T, keep func(T) bool) []T, that stands for a type in a generic function or type. Each use fixes it to one type. lets you write the loop once and still have every call checked. You
will write generic functions, see when Go works out the type arguments by itself and when you must write them, meet
the two constraints Go declares for you, any and comparable, and decide when a type parameter is the wrong tool.
Go 1.27.1 printed the output under each program.
A type parameter turns one loop into a function for every element type
Filter keeps the items of a slice that pass a test. Written once with a type parameter, it serves all three slices, and Map, written the same way, turns fares into text:
// filter.go: one Filter and one Map for fares, PNRs and seat counts.
package main
import (
"fmt"
"strings"
)
// Filter returns the items of xs for which keep returns true, in order.
func Filter[T any](xs []T, keep func(T) bool) []T {
var out []T
for _, x := range xs {
if keep(x) {
out = append(out, x)
}
}
return out
}
// Map returns f applied to each item of xs, in order.
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
}
func rupees(paise int64) string {
return fmt.Sprintf("₹%d.%02d", paise/100, paise%100)
}
func main() {
fares := []int64{54500, 9900, 120000} // in paise
pnrs := []string{"4213567890", "2650011223", "4890123456"}
seats := []int{0, 12, 3}
dear := Filter(fares, func(p int64) bool { return p > 50000 })
fmt.Println(dear)
fmt.Println(Filter(pnrs, func(p string) bool { return strings.HasPrefix(p, "4") }))
fmt.Println(Filter(seats, func(n int) bool { return n > 0 }))
fmt.Println(Map(dear, rupees))
keepFares := Filter[int64]
fmt.Printf("%T\n", keepFares)
} Output
[54500 120000] [4213567890 4890123456] [12 3] [₹545.00 ₹1200.00] func([]int64, func(int64) bool) []int64
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run filter.go
The square brackets after the name hold the type parameter list. [T any] declares T, a name that stands for a type,
and its constraint: The interface after a type parameter's name that says which types it may be, and so which operations the code may use on it: any allows every type, comparable only types whose values == can compare., any, which allows every type. The ordinary parameters then use
T like any other type: xs []T and keep func(T) bool. Map declares two type parameters, [T, U any], because its
result may hold another type than its input: it turned the two dearer fares, int64 values, into strings in
rupees, as the fourth line shows.
Each call fixes T to one type. Replacing T with a type argument: The type that replaces a type parameter in one use of a generic function or type, such as int64 in Filter[int64]. A call either writes it in square brackets or lets Go infer it from the arguments. and checking that type
against the constraint is called instantiation: Replacing the type parameters of a generic function or type with type arguments and checking each argument against its constraint. The result is an ordinary function or type, such as Filter[int64]., and the result is an ordinary
function: the last line printed func([]int64, func(int64) bool) []int64 for Filter[int64]. Step through what the
compiler did for the first call:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The call. main calls Filter(fares, keep), where fares is a []int64 of fares in paise. Filter's type parameter T stands for the element type, so before anything runs the compiler has to decide what T is for this call.
- T from the arguments. Filter's first parameter is []T and fares is a []int64, so T must be int64. The function literal keep, a func(int64) bool, agrees with func(T) bool. Nothing in the call names T; Go inferred it.
- Every T replaced. Replacing every T with int64 turns Filter into an ordinary function, func([]int64, func(int64) bool) []int64. This step is called instantiation, and %T printed exactly that type for Filter[int64].
- The constraint checked. The type argument must satisfy T's constraint. Filter's constraint is any, which every type satisfies, so int64 passes. A tighter constraint, such as int | float64, would refuse a string here, while compiling.
- Compiled and called. The instantiated function is compiled and called like any other. It returned [54500 120000], the two fares above 50,000 paise, which is the first line filter.go printed.
- Another call, another T. Filter(seats, …) goes through the same steps with T = int, because seats is a []int, and returned [12 3]. One declaration served three element types, and each call was checked on its own.
All of this happens while the program compiles, so a call whose arguments disagree about T never runs:
// mixed_call.go: arguments that disagree about T, and a type argument that does not fit.
package main
import "fmt"
// Filter returns the items of xs for which keep returns true, in order.
func Filter[T any](xs []T, keep func(T) bool) []T {
var out []T
for _, x := range xs {
if keep(x) {
out = append(out, x)
}
}
return out
}
func main() {
fares := []int64{54500, 9900}
fmt.Println(Filter(fares, func(p int) bool { return p > 50000 }))
fmt.Println(Filter[string](fares, nil))
} Output (exit status 1)
Printed as an error (standard error)
# command-line-arguments
./mixed_call.go:19:28: in call to Filter, type func(p int) bool of func(p int) bool {…} does not match inferred type func(int64) bool for func(T) bool
./mixed_call.go:20:29: cannot use fares (variable of type []int64) as []string value in argument to Filter[string]
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run mixed_call.go
The first call passed fares, a []int64, with a test written for int. fares made T int64, so the test had to be
a func(int64) bool, and the compiler says exactly that. The second call wrote its type argument, Filter[string],
and then fares cannot be the []string that Filter[string] takes.
Key idea
A type parameter is a placeholder for a type: each call fixes it to one type argument, the compiler checks that type against the constraint, and the call then runs as an ordinary function written for that type.
Checkpoint
Check: Type parameters and instantiation
Go infers the type arguments when the arguments mention them
No call in filter.go wrote a type argument: Go worked each one out from the arguments, which is called type inference: The compiler working out the type arguments of a call from its typed arguments, its constraints and then untyped constants, so the call need not write them. A type parameter that only the result mentions cannot be inferred.. Inference needs something typed to read. inference.go makes five calls, some with type arguments and some without, and prints the type of each result:
// inference.go: what Go infers, and the type arguments a call writes itself.
package main
import "fmt"
// Number lists the types the functions below accept.
type Number interface {
int | int64 | float64
}
// Max returns the larger of a and b.
func Max[T Number](a, b T) T {
if a > b {
return a
}
return b
}
// Convert returns xs with each value converted to To.
func Convert[To, From Number](xs []From) []To {
out := make([]To, len(xs))
for i, x := range xs {
out[i] = To(x)
}
return out
}
// Zero returns the zero value of T.
func Zero[T any]() T {
var z T
return z
}
func main() {
seats := 3
fares := []int64{54500, 9900}
fmt.Printf("%T %v\n", Max(seats, 2), Max(seats, 2))
fmt.Printf("%T %v\n", Max(2, 3.5), Max(2, 3.5))
fmt.Printf("%T %v\n", Convert[float64](fares), Convert[float64](fares))
fmt.Printf("%T %q\n", Zero[string](), Zero[string]())
var larger func(int64, int64) int64 = Max
fmt.Println(larger(54500, 9900))
} Output
int 3 float64 3.5 []float64 [54500 9900] string "" 54500
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run inference.go
| Call | Where T comes from | Printed |
|---|---|---|
Max(seats, 2) |
seats, an int |
int 3 |
Max(2, 3.5) |
the constants’ default type | float64 3.5 |
Convert[float64](fares) |
To written, From from fares | []float64 [54500 9900] |
Zero[string]() |
written in the call | string "" |
var larger func(…) int64 = Max |
the variable’s function type | 54500 |
Typed values count first. In Max(seats, 2), seats made T an int, and the constant 2 then only had to fit an int.
With constants alone, as in Max(2, 3.5), T takes the default type of the constants, and an integer constant with a
floating-point one gives float64, as 2 + 3.5 does. Convert shows a partial type argument list: Convert[float64]
writes the first type parameter, To, and leaves From to be inferred from fares. Type arguments may be left out only
from the end, so a function lists first the type parameters that callers must write.
When nothing typed mentions a type parameter, inference has nothing to read:
// cannot_infer.go: four calls the compiler refuses, and why.
package main
import (
"fmt"
"strconv"
)
// Max returns the larger of a and b.
func Max[T int | int64 | float64](a, b T) T {
if a > b {
return a
}
return b
}
// Zero returns the zero value of T.
func Zero[T any]() T {
var z T
return z
}
// ParseFare reads a fare in paise.
func ParseFare[T int | int64](s string) (T, error) {
n, err := strconv.ParseInt(s, 10, 64)
return T(n), err
}
func main() {
seats := 3
fmt.Println(Zero())
fmt.Println(ParseFare("54500"))
zero := Zero
fmt.Println(zero, Max(seats, 2.5))
} Output (exit status 1)
Printed as an error (standard error)
# command-line-arguments ./cannot_infer.go:31:18: in call to Zero, cannot infer T (declared at ./cannot_infer.go:18:11) ./cannot_infer.go:32:23: in call to ParseFare, cannot infer T (declared at ./cannot_infer.go:24:16) ./cannot_infer.go:33:10: cannot use generic function Zero without instantiation ./cannot_infer.go:34:31: cannot use 2.5 (untyped float constant) as int value in argument to Max (truncated)
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run cannot_infer.go
Zero() and ParseFare("54500") mention T only in their results, so Go cannot infer it, and the call must write it:
Zero[int]() or ParseFare[int64]("54500"). zero := Zero gives Go no type at all for the new variable, and a
generic function cannot be a value until its type arguments are known. The last error is a different one: seats made
T an int, so the constant 2.5 would have had to become an int and lose its .5. Step through how Go decided three of
these calls:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Max(seats, 2). Max[T Number](a, b T) is called with seats, an int variable, and the constant 2. The call writes no type argument, so Go works T out.
- A typed value decides. seats has the type int, so T = int. Typed values count first; the constant 2 then only has to fit an int, and it does.
- Instantiated. With T known, Max[int] is checked against Number and called. inference.go printed int 3 for this call.
- Max(2, 3.5). This time both arguments are untyped constants, so no typed value says anything about T.
- Constants give a default type. 2 is an integer constant and 3.5 a floating-point one. As in the expression 2 + 3.5, the floating-point kind wins, so T takes its default type, float64, and the call printed float64 3.5.
- Zero(). Zero[T any]() T has no parameters at all. No typed value and no constant mentions T, so after both steps T is still unknown.
- Write the type argument. The compiler stops with "in call to Zero, cannot infer T". The fix is to write it, as Zero[string]() does. A typed variable helps only when the generic function itself is assigned, as var larger func(int64, int64) int64 = Max was; the variable a result goes into does not count.
Go 1.27 widened one case. A generic function that is assigned or converted to a function type now gets its type
arguments from that type wherever the assignment happens. rules.go stores two generic functions in a table of fare
rules, a struct field each, then converts Half to func(float64) float64:
// rules.go: generic functions stored in a table of fare rules, and one converted to a function type.
package main
import "fmt"
// Half returns half of x.
func Half[T int | int64 | float64](x T) T { return x / 2 }
// Same returns x unchanged.
func Same[T any](x T) T { return x }
// Rule computes a fare in paise from the base fare.
type Rule struct {
Name string
Apply func(int64) int64
}
func main() {
rules := []Rule{
{Name: "full fare", Apply: Same},
{Name: "half fare", Apply: Half},
}
for _, r := range rules {
fmt.Println(r.Name, r.Apply(54500))
}
halve := (func(float64) float64)(Half)
fmt.Println(halve(3))
} Output
full fare 54500 half fare 27250 1.5
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run rules.go
New in Go 1.27
Go 1.21 already inferred type arguments in a plain assignment, as for larger above. Struct fields in a composite
literal and conversions, as in rules.go, need Go 1.27; Go 1.26.8 refuses all three uses, each with cannot use generic function … without instantiation.
The same file, compiled by Go 1.26.8:
from session import run
# rules.go stores generic functions in a struct literal and converts one to a function type. Go 1.26.8 compiles
# neither: it infers type arguments only where a call or a plain assignment gives them.
run(
"""
$OLD_GO_BIN/go version
$OLD_GO_BIN/go run rules.go
""",
expect_failure=["$OLD_GO_BIN/go run rules.go"],
) Output
$ $OLD_GO_BIN/go version go version go1.26.8 darwin/arm64 $ $OLD_GO_BIN/go run rules.go # command-line-arguments ./rules.go:20:30: cannot use generic function Same without instantiation ./rules.go:21:30: cannot use generic function Half without instantiation ./rules.go:27:35: cannot use generic function Half without instantiation [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 old_go_session.py
Key idea
Go infers a type argument from typed arguments and target types first, and from untyped constants after them; a type parameter that only the result mentions has to be written in the call.
Checkpoint
Check: When Go infers type arguments
Equality needs comparable, and comparable can still panic
any allows every type, and in exchange the code may use almost no operator on a T: no ==, no <, no +.
not_comparable.go tries x == v under any, then calls a function constrained by comparable with a slice of
slices:
// not_comparable.go: == on a type parameter needs comparable, and a slice type is not comparable.
package main
import "fmt"
// Index returns the position of v in xs, or -1.
func Index[T any](xs []T, v T) int {
for i, x := range xs {
if x == v {
return i
}
}
return -1
}
// Uniq returns the items of xs without repeats.
func Uniq[T comparable](xs []T) []T {
seen := make(map[T]bool)
var out []T
for _, x := range xs {
if !seen[x] {
seen[x] = true
out = append(out, x)
}
}
return out
}
func main() {
fmt.Println(Index([]string{"MAS", "SBC"}, "SBC"))
fmt.Println(Uniq([][]string{{"upi"}, {"upi"}}))
} Output (exit status 1)
Printed as an error (standard error)
# command-line-arguments ./not_comparable.go:9:6: invalid operation: x == v (incomparable types in type set) ./not_comparable.go:31:18: []string does not satisfy comparable
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run not_comparable.go
incomparable types in type set: some of the types that any allows, such as slices, maps and functions, have no
==, so the compiler refuses the operator for every T. The constraint comparable allows only types whose values can
be compared, the comparable type: A type whose values can be compared with == and !=: numbers, strings, booleans, pointers, channels, interfaces, and arrays and structs made of comparable types. Map keys need one; slices, maps and functions are not comparable. More in “Arrays are values: length, copies, comparison”, so code constrained by it may use == and !=
and make T a map key, as Uniq does. That is also why its call with [][]string failed: []string does not satisfy comparable.
There is a catch. Since Go 1.20, interface types such as any satisfy comparable as well, because == is defined on
interface values, even though it panics when both values hold the same type and that type has no ==. Data decoded
from JSON shows it:
// uniq.go: Uniq needs ==, so T is comparable; an any that holds a slice still panics.
package main
import (
"encoding/json"
"fmt"
)
// Uniq returns the items of xs without repeats, in the order they first appear.
func Uniq[T comparable](xs []T) []T {
seen := make(map[T]bool)
var out []T
for _, x := range xs {
if !seen[x] {
seen[x] = true
out = append(out, x)
}
}
return out
}
func main() {
fmt.Println(Uniq([]string{"upi", "card", "upi", "netbanking", "card"}))
var methods []any
if err := json.Unmarshal([]byte(`["upi", "card", 50, "upi", 50]`), &methods); err != nil {
fmt.Println(err)
return
}
fmt.Println(Uniq(methods))
defer func() { fmt.Println("recovered:", recover()) }()
if err := json.Unmarshal([]byte(`["upi", ["card", "netbanking"]]`), &methods); err != nil {
fmt.Println(err)
return
}
fmt.Println(Uniq(methods))
} Output
[upi card netbanking]
[upi card 50]
recovered: runtime error: hash of unhashable type []interface {}
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run uniq.go
Uniq kept the first of each payment method, and on []any it handled the string “upi” and the number 50 like any
other values. The second document held a nested array, which encoding/json decodes to a []interface {}, and using
that as a map key panicked: hash of unhashable type []interface {}. The program caught the panic to print it. The
compiler accepted the call because any satisfies comparable; whether each value inside can be compared is known
only while the program runs.
| Type argument | Satisfies comparable? | Can == panic? |
|---|---|---|
string, int64, a struct of such fields |
yes | no |
[]string, map[string]int, a function type |
no: a compile error | not compiled |
any, error or another interface type |
yes, since Go 1.20 | yes, when the values inside hold slices or maps |
Trusting comparable with interface values
Uniq[T comparable] called with a []any compiles and passes tests on strings and numbers, then panics on the
first nested JSON array. Constrain the element type to a concrete type, such as []string, or turn each value into
a comparable key before you compare it.
Checkpoint
Check: any, comparable and the run-time panic
Use a type parameter when the same code serves many types
A type parameter is one way to write code for several types; an interface and a plain function for one type are the others. The Go blog’s advice, in When To Use Generics, is to write code first and add a type parameter when you notice that you are about to write the same code again for another type, as the three copies of Filter’s loop would have been. It names the cases:
- Functions on slices, maps and channels whose work does not depend on the element type, such as Filter, Map and Uniq, and data structures the language does not have, such as a linked list or a tree.
- Code that only calls methods of its values wants an interface: io.Reader serves files, network connections and buffers without any type parameter.
- When each type needs its own implementation, an interface again, because each type brings its own method.
- When the types have no methods and each needs different handling, as every type that encoding/json writes does, reflection is what remains.
Answer the questions about a function you are about to write:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Way to write it | Same code for many types | Code that only calls methods | A different body per type | Simple to read |
|---|---|---|---|---|
| A type parameter Works for: the types its constraint allows | Good one Filter served int64, string and int | Mixed works, but an interface does the same with less | Poor one body for every T: no per-type code | Mixed adds a type parameter list to every signature |
| An interface Works for: types that have its methods | Mixed only when the shared work is a method call | Good io.Reader: each type brings its own Read | Good each type implements the method its own way | Good no type parameters to read |
| One concrete type Works for: that type alone | Poor one copy of the loop per type, as before Go 1.18 | Mixed fine while one type calls it | Mixed one type, so one body | Good the plainest code, while one type uses it |
How the answers decide
- “How many element types use the function today?” One: adds a caveat to A type parameter and An interface. With one type there is nothing to share yet. The blog's advice is to write the plain function and add a type parameter when a second type needs the same code.
- “How many element types use the function today?” Two or more: rules out One concrete type. One concrete type means one copy of the code per type, which is the duplication type parameters remove.
- “Does the body only call methods of the values it gets?” Yes, such as Read or Price: adds a caveat to A type parameter. If all the code does is call methods, the blog says to use an interface type; a type parameter adds a list to read and nothing else.
- “Does each type need its own way of doing the work?” Yes, a file reads differently from a buffer: rules out A type parameter. A generic function has one body for every type argument. Different code for each type is what an interface's methods give.
In short: for one type, plain code; for the same steps on many types, a type parameter; for behaviour that differs by type, an interface.
Checkpoint
Check: Generics, interfaces or plain code
Practise: three helpers for a list of tickets
practice/gen.go has three generic helpers for you to write, and practice/gen_test.go tests them on a list of tickets:
Go (gen.go) · practice/gen.go
// Package practice holds three generic helpers for you to write.
package practice
// Reduce starts from init and calls f with the value so far and each item of xs, in order, and returns the last value.
func Reduce[T, A any](xs []T, init A, f func(A, T) A) A {
return init // write the loop
}
// GroupBy puts the items of xs into lists by the key that key returns; each list keeps the order of xs.
func GroupBy[T any, K comparable](xs []T, key func(T) K) map[K][]T {
return nil // write the loop
}
// Partition returns the items for which keep returns true and the others, each in the order of xs.
func Partition[T any](xs []T, keep func(T) bool) (yes, no []T) {
return nil, nil // write the loop
} Go (gen_test.go) · practice/gen_test.go
package practice
import (
"reflect"
"testing"
)
type ticket struct {
PNR string
Coach string
Paise int64
}
var tickets = []ticket{
{"4213567890", "S4", 54500},
{"2650011223", "B1", 120000},
{"4890123456", "S4", 9900},
}
func TestReduce(t *testing.T) {
total := Reduce(tickets, int64(0), func(sum int64, tk ticket) int64 { return sum + tk.Paise })
if total != 184400 {
t.Errorf("total paise = %d, want 184400", total)
}
pnrs := Reduce(tickets, "", func(s string, tk ticket) string { return s + tk.PNR[:4] })
if pnrs != "421326504890" {
t.Errorf("first four digits of each PNR = %q, want %q", pnrs, "421326504890")
}
}
func TestGroupBy(t *testing.T) {
got := GroupBy(tickets, func(tk ticket) string { return tk.Coach })
want := map[string][]ticket{"S4": {tickets[0], tickets[2]}, "B1": {tickets[1]}}
if !reflect.DeepEqual(got, want) {
t.Errorf("GroupBy by coach = %v, want %v", got, want)
}
if got := GroupBy([]int{}, func(n int) int { return n }); len(got) != 0 {
t.Errorf("GroupBy of no items = %v, want an empty map", got)
}
}
func TestPartition(t *testing.T) {
dear, cheap := Partition(tickets, func(tk ticket) bool { return tk.Paise > 50000 })
if len(dear) != 2 || len(cheap) != 1 || cheap[0].PNR != "4890123456" {
t.Errorf("Partition by fare = %v and %v, want 2 tickets above ₹500 and PNR 4890123456 alone", dear, cheap)
}
} The starter code compiles, and every test fails:
from session import run
# The starter code of practice/ compiles, and every test fails until the three functions are written.
run(
"""
cd practice && go test .
""",
expect_failure=["cd practice && go test ."],
) Output
$ cd practice && go test .
--- FAIL: TestReduce (0.00s)
gen_test.go:23: total paise = 0, want 184400
gen_test.go:27: first four digits of each PNR = "", want "421326504890"
--- FAIL: TestGroupBy (0.00s)
gen_test.go:35: GroupBy by coach = map[], want map[B1:[{2650011223 B1 120000}] S4:[{4213567890 S4 54500} {4890123456 S4 9900}]]
--- FAIL: TestPartition (0.00s)
gen_test.go:45: Partition by fare = [] and [], want 2 tickets above ₹500 and PNR 4890123456 alone
FAIL
FAIL example.com/practice 3.046s
FAIL
[exit status 1]
This output changes from run to run: go test prints how long each test and the whole run took, which differs from one run to the next.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 practice_session.py
Write Reduce, GroupBy and Partition so that go test . passes in the practice folder. A first hint: GroupBy’s map
holds a slice for each key, and append(out[k], x) works even for a key that is not there yet, because a missing
key gives a nil slice.
Then check yourself: the questions come one by one, the weakest idea first.
Practice that adapts to you
Practise this lesson
10 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): What is the difference between a type parameter and a type argument in Go?
A type parameter is the name a generic function or type declares in square brackets, with a constraint, such as T in
func Filter[T any](xs []T, keep func(T) bool) []T. A type argument is the actual type that replaces it in one use:
int64 in Filter[int64], written by the caller or inferred from the arguments. Replacing the parameters with the
arguments and checking each argument against its constraint is instantiation, and the result is an ordinary,
non-generic function.
Two longer questions of the module bank build on this lesson:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A pull request changes func Send(n Notifier, msg string) error into func Send[N Notifier](n N, msg string) error, and its description says that type parameters make the call faster. Review the change: what do you ask for, and what do you suggest?
-
Mid to senior
A generic Dedupe[T comparable](xs []T) []T has passed its tests for a year. After a change feeds it values decoded from JSON into a []any, production logs show runtime error: hash of unhashable type []interface {}. Why did it compile, why does it panic, and how do you fix it?
Key takeaways
- A type parameter list such as
[T any]lets one function serve every type its constraint allows; each call fixes T to one type argument, and the compiler checks the call as if the function were written for that type. - Go infers type arguments from typed arguments first and from untyped constants after them; a type parameter that only the result mentions, or a generic function used as a value with no target type, needs its type argument written, and a partial list gives the leading ones.
- Go 1.27 infers type arguments wherever a generic function is assigned or converted to a matching function type, such as a struct field; Go 1.26.8 refused those uses.
anyallows no operators on T;comparableallows==,!=and map keys, but an interface type argument satisfies it since Go 1.20 and panics when its values hold slices or maps.- Add a type parameter when the same code would otherwise be written again for another type; for code that only calls methods, or that differs by type, use an interface.
Terms in this lesson
- constraint
- The interface after a type parameter's name that says which types it may be, and so which operations the code may use on it: any allows every type, comparable only types whose values == can compare.
- instantiation
- Replacing the type parameters of a generic function or type with type arguments and checking each argument against its constraint. The result is an ordinary function or type, such as Filter[int64].
- type argument
- The type that replaces a type parameter in one use of a generic function or type, such as int64 in Filter[int64]. A call either writes it in square brackets or lets Go infer it from the arguments.
- type inference
- The compiler working out the type arguments of a call from its typed arguments, its constraints and then untyped constants, so the call need not write them. A type parameter that only the result mentions cannot be inferred.
- type parameter
- A name declared in square brackets with a constraint, such as T in func Filter[T any](xs []T, keep func(T) bool) []T, that stands for a type in a generic function or type. Each use fixes it to one type.
References
- Tutorial: Getting started with generics (The Go Authors)
- An Introduction To Generics (The Go Authors)
- When To Use Generics (The Go Authors)
- Everything You Always Wanted to Know About Type Inference (The Go Authors)
- All your comparable types (The Go Authors)
- The Go Programming Language Specification: Type parameter declarations (The Go Authors)
- The Go Programming Language Specification: Instantiations (The Go Authors)
- The Go Programming Language Specification: Type inference (The Go Authors)
- The Go Programming Language Specification: Type constraints (The Go Authors)
- Go 1.21 Release Notes: Changes to the language (The Go Authors)
- Go 1.27 Release Notes: Changes to the language (The Go Authors)
- Package encoding/json: Unmarshal (The Go Authors)
Report a problem with this lesson
Kept only in this browser. Your Learn progress