Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 15 minutes
  • Examples run with Go 1.27.1 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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 for fares, PNRs and seat counts Go · filter.go
// 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:

Interactive · stage by stage How one call to Filter becomes an ordinary function Step through the first call of filter.go. Watch T get its type from the arguments, then the compiler replace it and check it.
How one call to Filter becomes an ordinary function, step 6 of 6. Showing Filter(fares, keep), Find T (T = int, from seats), Replace T everywhere (func([]int, …) []int), Check the constraint (int64 satisfies any), Compile and call (returned [12 3]). In focus: Find T, Replace T everywhere.Filter(fares, keep)fares is a []int64Find TT = int, from seatsReplace T everywherefunc([]int, …) []intCheck the constraintint64 satisfies anyCompile and callreturned [12 3]

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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].
  4. 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.
  5. 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.
  6. 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 Go · mixed_call.go
// 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

  1. Question 1 of 2 filter.go declares func Filter[T any](xs []T, keep func(T) bool) []T. What does fmt.Printf("%T\n", Filter[int64]) print?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: func([]int64, func(int64) bool) []int64

    Writing the type argument instantiates Filter: T becomes int64 throughout, the constraint any is checked, and the result is an ordinary func([]int64, func(int64) bool) []int64.

  2. Question 2 of 2 fares is a []int64. Why does Filter(fares, func(p int) bool { return p > 50000 }) not compile?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: fares makes T int64, so the test must be a func(int64) bool, not a func(int) bool

    In one call a type parameter has one type. fares fixed T as int64, so every other use of T in that call must be int64 too.

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 what a call writes Go · inference.go
// 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 Go · cannot_infer.go
// 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:

Interactive · stage by stage How Go fills in T, and when it cannot Step through three calls without type arguments. Typed values decide first, untyped constants after them, and a T still unknown stops the build.
How Go fills in T, and when it cannot, step 7 of 7. Showing A call without [T] (Zero[string]()), Typed values (nothing mentions T), Untyped constants (none), Every T known? (no: cannot infer T), Instantiate and check (not reached). In focus: write, A call without [T].no: write [T] yourselfA call without [T]Zero[string]()Typed valuesnothing mentions TUntyped constantsnoneEvery T known?no: cannot infer TInstantiate and checknot reached

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. Instantiated. With T known, Max[int] is checked against Number and called. inference.go printed int 3 for this call.
  4. Max(2, 3.5). This time both arguments are untyped constants, so no typed value says anything about T.
  5. 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.
  6. 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.
  7. 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 in a table of rules Go · rules.go
// 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:

rules.go under Go 1.26.8 Python · old_go_session.py
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

  1. Question 1 of 2 Max[T Number](a, b T) T is called as Max(2, 3.5), with Number allowing int, int64 and float64. Which type does T get?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: float64

    Typed values come first and untyped constants after them. Here only constants are given, so T is the default type of their combined kind, float64.

  2. Question 2 of 2 func Zero[T any]() T fails as fmt.Println(Zero()) with in call to Zero, cannot infer T. Which line compiles?

    Choose one answer.

    A hint

    Nothing in the call mentions T, so where else could the type come from?

    Show the answer: Question 2 of 2

    Answer: fmt.Println(Zero[int]())

    A type parameter that only the result mentions cannot be inferred from the call, so the call writes it: Zero[int]().

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: == under any, and slices under comparable Go · not_comparable.go
// 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 on strings, then on decoded JSON Go · uniq.go
// 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

  1. Question 1 of 2 func Uniq[T comparable](xs []T) []T uses T as a map key. Which calls compile?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • Uniq([]any{"upi", 50})
    • Uniq([]struct{ Coach string; No int }{{"S4", 23}})
    • Uniq([]string{"upi", "card"})

    comparable allows the types whose values == can compare, and since Go 1.20 also interface types, whose values are compared while the program runs.

  2. Question 2 of 2 uniq.go called Uniq on a []any decoded from ["upi", ["card", "netbanking"]] and recovered: runtime error: hash of unhashable type []interface {}. What caused it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The nested array was decoded to a []interface {}, which cannot be a map key, and Uniq used every item as a key

    comparable was satisfied by any at compile time, but one value inside held a slice, and hashing a slice key panics at run time.

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:

Interactive · answer and compare A type parameter, an interface or one concrete type? Answer three questions about a function you are about to write; the table rules out the ways that do not fit, and says why.
How many element types use the function today?
Does the body only call methods of the values it gets?
Does each type need its own way of doing the work?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Three ways to write a function, against the Go blog's guidance on when to use generics
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

  1. Question 1 of 2 A function reads bytes from a file, a network connection or a test buffer, and calls nothing but Read on what it gets. What should its parameter be?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: An io.Reader

    If all the code does with a value is call its methods, an interface type says that most simply.

  2. Question 2 of 2 Only []int64 fares use a new Total function today. What does the Go blog suggest you write first?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: func Total(fares []int64) int64, and a type parameter later if another type needs the same loop

    A type parameter pays off when the same code serves several types; with one type, the plain function is simpler.

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:

practice/gen.go and practice/gen_test.go

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:

The starter helpers against their tests Python · practice_session.py
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.

  1. Question 1 of 10 Where does a generic Go function declare its type parameters?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: In square brackets right after the function's name, before the ordinary parameters

    A type parameter list is written in square brackets between the name and the ordinary parameters, each name with a constraint.

  2. Question 2 of 10 What is instantiation of a generic function?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: Replacing its type parameters with type arguments and checking each one against its constraint

    Instantiation substitutes the type arguments and checks the constraints; the result is a non-generic function.

  3. Question 3 of 10 Map[T, U any](xs []T, f func(T) U) []U is called as Map(dear, rupees), where dear is a []int64 and rupees is a func(int64) string. What does it return?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: A []string

    Every type parameter that appears in an argument's type can be inferred from that argument: T from xs and U from the result type of f.

  4. Question 4 of 10 func Convert[To, From Number](xs []From) []To. fares is a []int64. Which call returns a []float64?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: Convert[float64](fares)

    A partial type argument list is a prefix of the full list; the type parameters left out at the end must be inferable.

  5. Question 5 of 10 In Go 1.27, what can a type argument be inferred from?

    Choose every answer that is right.

    Show the answer: Question 5 of 10

    Answer:

    • The types of the call's ordinary arguments
    • The default type of untyped constant arguments
    • The function type of a variable that a generic function is assigned to

    Arguments, untyped constants and the function type a generic function is assigned to all feed inference; the variable a call's result goes into does not.

  6. Question 6 of 10 Does var z int = Zero() compile, with func Zero[T any]() T?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: No, it fails with cannot infer T

    var larger func(int64, int64) int64 = Max assigns the function itself, which gives Go a type to read; Zero() assigns a result, which does not.

  7. Question 7 of 10 Which operators may code use on a type parameter constrained by comparable?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: == and != only

    comparable is the constraint for == and !=; under any, not_comparable.go got incomparable types in type set.

  8. Question 8 of 10 Uniq is called with []any{[]int{1}, "x"}: a slice first, then a string. What happens?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: It panics on the first item, because the slice cannot be a map key

    With interface type arguments, comparable is checked while the program runs: a map key holding a slice panics when it is hashed.

  9. Question 9 of 10 What is the Go blog's simple rule for when to add a type parameter?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: When you are about to write the same code again, differing only in the types it uses

    Avoid type parameters until the same code would otherwise be written again for another type.

  10. Question 10 of 10 A package must write values of any type as text, including types without methods, and each kind of type needs different handling, as encoding/json does. Which tool fits?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: Reflection

    Methods missing and behaviour differing by type is the case the blog leaves to reflection, as encoding/json uses.

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.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

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.
  • any allows no operators on T; comparable allows ==, != 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.