skip to content

When does Go infer a generic function's type arguments, and when must you write F[int] yourself?

level: juniorimportance: must knowfreq 68%

answer

  1. the call site is the only evidence
  2. arguments in, not results out
  3. nothing to unify against
  4. a prefix of the list may be written
  5. Zero[int]() when T is only returned

basics

~20 s

Go infers a generic function's type arguments from the ordinary arguments you pass at the call site. A type parameter that appears only in the result is not determined by any argument, so you must write it explicitly, as in Zeroint.

solid answer

~40 s

At a generic call, the compiler matches the types of the ordinary arguments against the types written in the parameter list and solves for each type parameter. So `Map(nums, strconv.Itoa)` with `nums []int` infers `T = int` from `[]T` and `U = string` from `func(T) U`, and you write no type arguments at all. Inference only has the arguments to work from: it does not read the function body, and for a call it does not look at what you assign the result to. That is why `func Zero[T any]() T` must be called as `Zero[int]()` — nothing at the call site says what `T` is. You may also supply type arguments partially, but only as a prefix of the list: `F[int](x)` is legal, and the remaining ones are inferred.

code

go · 18 lines
go
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 Zero[T any]() T {
	var z T
	return z
}

func demo() {
	strs := Map([]int{1, 2, 3}, strconv.Itoa) // T=int, U=string, both inferred
	n := Zero[int]()                          // T appears only in the result
	_, _ = strs, n
}

go deeper

for a junior

Be ready to say, in one sentence, that type arguments come from the values you pass, and to show the case that breaks: a function whose type parameter appears only in the result must be called as Fint.

for a middle

An interviewer expects you to describe the matching itself — argument type against written parameter type, solving for each type parameter — and to know that partial type argument lists must be a prefix.

for a senior

Show that you treat inferability as an API property: reviewing a helper, you check whether every type parameter is reachable from a parameter, because otherwise every call site pays the cost forever.

for a principal

Frame it as a signature-design constraint on shared packages: a helper that cannot infer pushes type arguments into hundreds of call sites, and changing the parameter order later to fix it is a breaking change.

## What type-argument inference is A generic function in Go declares type parameters in square brackets before its ordinary parameters: ```go func Map[T, U any](s []T, f func(T) U) []U ``` `T` and `U` are type parameters. To actually run the function the compiler must *instantiate* it — pick a concrete type for every type parameter. You can always write those types yourself (`Map[int, string](nums, strconv.Itoa)`), but idiomatic Go almost never does, because the compiler works them out. That process is type-argument inference. ## How it works: unification against the argument types Inference is purely a call-site activity. The compiler takes the *type* of each ordinary argument you passed and matches it, structurally, against the *written type* of the corresponding parameter, collecting equations as it goes: - argument `nums` has type `[]int`; the parameter is written `[]T`; therefore `T = int`. - argument `strconv.Itoa` has type `func(int) string`; the parameter is written `func(T) U`; with `T` already `int`, therefore `U = string`. Every type parameter now has a value, so the call compiles with nothing written in brackets. This structural matching is called unification, and it reaches arbitrarily deep: `map[string][]T`, `func(...T) (U, error)` and `chan T` all yield equations the same way. Two things the compiler does *not* do are just as important: - **It does not read the function body.** Whatever `Map` does internally is irrelevant; only the signature participates. - **For a call, it does not look at the assignment context.** Writing `var out []string = Zero()` does not tell the compiler that the type parameter is `string`. (A generic function used as a *value* rather than called — assigned to a variable of a known function type — is a different situation, and there the function type can supply the type arguments.) ## The rule that follows: results are not evidence Because only arguments feed inference, a type parameter that appears **only in the result** can never be inferred: ```go func Zero[T any]() T { var z T return z } n := Zero[int]() // required n := Zero() // compile error: cannot infer T ``` The same trap appears in constructor-shaped helpers (`func New[T any]() *Container[T]`) and in conversion helpers whose output type is the whole point. The error message names the offending parameter — `cannot infer T` — which is the fastest way to find it in a long signature. If you are designing the function, the fix is usually not "tell callers to write the type arguments"; it is to reshape the signature so the type parameter shows up in a parameter, for example by taking a value of that type, or by taking a function that produces it. ## Partial type arguments You are not limited to "all or nothing". A call may supply a *prefix* of the type argument list and let inference finish the job: ```go func Convert[To, From any](v From) To x := Convert[string](42) // To written, From inferred as int ``` The order in the declaration therefore matters for usability: put the type parameters that cannot be inferred first, so callers can supply just those. You cannot skip one and supply a later one; the supplied list must be a prefix. ## Untyped constants If a type parameter is only reached through untyped constant arguments, the constant's default type is used — `Min(1, 2)` gives `T = int`, not `float64`. A typed argument in the same call wins over the constants. ## Generic types are a separate story Instantiating a *generic type* — `Stack[int]`, `map[string]Result[T]` — is not a call, so there are no arguments and there is nothing to infer from. Generic types always carry explicit type arguments; only generic *functions* get inference. ## What an interviewer is checking That you know inference is evidence-driven rather than magical: arguments in, type arguments out. Candidates who have only used `slices` and `maps` helpers often believe Go "just figures it out", and are then surprised by the first `cannot infer` error. The one-sentence model to hold is: *if no argument mentions the type parameter, the caller must name it.*

  • Can a call supply some type arguments and infer the rest?
    Yes, but only as a prefix of the type parameter list. Given `func Convert[To, From any](v From) To`, you can write `Convert[string](42)` and let `From` be inferred as `int`. You cannot skip `To` and supply `From`. This is why a type parameter that callers must always name should be declared first.
  • Does the variable you assign a generic call's result to affect inference?
    No. For a call, inference uses only the ordinary arguments, so `var s []string = Zero()` still fails. The assignment context does matter in a different case: when the generic function itself is used as a value and assigned to a variable of a known function type, that function type can supply the type arguments.
  • What does the compiler error look like when inference fails?
    The compiler names the type parameter it could not solve, in the form `cannot infer T`, along with the call and the declaration position. That name tells you exactly where to look: find `T` in the signature, and if it appears only in the result you either write the type argument or add a parameter that mentions `T`.

Inference is like reading a recipe's ingredient list to work out what dish you are making. If a step only appears in the finished plate and never in the ingredients, you have to be told.

saying these in an interview costs you the question

  • Says Go infers the type argument from the variable the result is assigned to
  • Claims every generic call must spell out its type arguments
  • Thinks the compiler inspects the function body to guess the type
  • Believes writing Zero[int]() is a style choice rather than required
  • Assumes a generic type like Stack[int] can drop its type argument too