Generics would be painful to use if every call had to spell out the type arguments. Min[int](3, 5) is verbose; Min(3, 5) reads like normal code. Go's compiler bridges the gap with type inference: it looks at the arguments and the constraints, figures out what each type parameter must be, and lets you skip the brackets. This lesson covers when inference works, when it doesn't, and what to do about the cases where it fails.
The simplest and most common form of inference is function argument inference. When you call a generic function, the compiler matches the types of the values you pass against the types of the parameters, then solves for each type parameter.
Here's a generic Min. Notice the call sites: no brackets.
Three calls, three different types, zero type arguments written out. The compiler sees Min(3, 5), notices both arguments are untyped integer constants that default to int, and binds T = int. For Min(9.99, 4.50), both are untyped float constants that default to float64. For the string call, both arguments are already typed strings.
Inference works because there's a unique solution. Both parameters share the same T, so the compiler must find one type that fits both. When the arguments agree on a type, there's exactly one answer.
Inference fails when no such answer exists. Pass two arguments of different types and the compiler has nothing to pick:
Here's the program that produces that error. The argument types disagree, so the inference fails.
The error message is precise. It tells you the default type of each argument and says they don't match. There's no T that could be both int and string, so inference gives up.
Most uses of generics work entirely through argument inference. You write the function once with a type parameter, and every call site looks like a normal function call.
Argument inference uses the default types of untyped constants. That works for matching pairs, but mixing typed and untyped values reveals a subtlety.
Consider this call:
This compiles. Why? price is a typed float64. The literal 3 is an untyped integer constant. Go's rules say an untyped constant can convert to a typed value at the call site, so 3 becomes 3.0 (a float64), and the compiler binds T = float64. The result is float64.
Flip the order, or use two untyped constants with different default types, and the rules change. Here's the case that surprises people:
The failing program:
You might expect both to be float64 (since 5.0 is a float), or both to be int (since 3 is an int). Neither happens. Untyped constants don't influence each other; each one has its own default type, and inference checks the defaults match. 3 defaults to int, 5.0 defaults to float64, and they don't agree.
There are three ways to fix this. Each one is worth knowing.
Use a typed value for one argument:
Make both arguments share an obvious default. 3.0 and 5.0 both default to float64:
Or pass an explicit type argument and let conversion do the rest:
With [float64] written explicitly, both 3 and 5.0 convert into float64, and the call succeeds. The pattern generalizes: when inference can't pick a unique type from mixed untyped constants, force it with an explicit argument.
Cost: Inference runs at compile time. It doesn't affect runtime speed. The cost when it fails is just a worse error message and a slower edit-compile cycle while you figure out what to write.
When inference can't figure it out, or when readability suffers, you write the type arguments yourself in square brackets after the function name.
The brackets pin the type parameter. After that, the regular conversion rules kick in: untyped constants convert to the declared T, typed values must match exactly.
If a human reading the call site can't tell what T should be, neither can the compiler. Examples where you should consider writing the type out:
| Situation | Why explicit helps |
|---|---|
| Two mismatched untyped constants | Defaults disagree; pick one. |
| You want a wider type than the default | Min[int64](3, 5) instead of int. |
No arguments at all (return-only T) | Compiler has nothing to look at. |
| The call site is far from the declaration | A reader can't tell what T should be. |
You're storing the result in a typed variable but the function returns T | Optional, but documents intent. |
Don't write explicit type arguments out of habit. Min(3, 5) is clearer than Min[int](3, 5). Use the brackets when inference fails or when the call site is ambiguous to a human reader. The standard library's slices and maps packages almost never need explicit type arguments at call sites, because their first parameter is a slice or map whose element type is obvious.
slices.Sort is declared as func Sort[S ~[]E, E cmp.Ordered](x S). It has two type parameters, but you don't write either. The compiler sees []float64, infers S = []float64 and E = float64, and you're done. This is the ergonomic payoff of generics: when inference works, generic code feels indistinguishable from monomorphic code.
Argument inference looks at the types of the values you pass. If a type parameter only appears in the return type, the compiler has nothing to match it against.
This fails to compile:
The compiler can't read your mind. Zero takes no arguments, so there's nothing to look at. You have to say what T is:
The same problem shows up with functions that take only a constraint-typed parameter where the actual type comes from elsewhere. A common example is a generic JSON decoder:
Decode accepts data []byte, but []byte doesn't tell the compiler what T should be. You have to write Decode[Order](data). This is fine, the call site reads clearly because the type is what makes the function useful here.
The lesson: if you find yourself writing a function whose only type parameter appears solely in the return type, callers will always have to spell it out. That's not a bug, it's just a fact of the type system. Sometimes you can rework the API to take a hint argument instead, but if the function genuinely returns a new value of arbitrary type, explicit arguments are the price of doing business.
Sometimes a generic function has more than one type parameter, but they're linked through a constraint. Go can infer one from the other.
A useful example is a Keys function over maps. The function takes a map[K]V and returns a []K. There are two type parameters, K and V, but you only need to know the map's type once for both to be solved.
You called Keys(prices). The compiler looked at prices, saw map[string]float64, matched it against map[K]V, and bound K = string and V = float64. Two type parameters, one argument, both solved.
The general rule: when the argument type contains type parameters, every type parameter inside it gets bound at once. A slice argument of type []T binds T. A map argument of type map[K]V binds both. A function argument of type func(T) U binds both.
Constraint type inference also works through the constraint itself. Here's a more involved case:
First declares two type parameters: S (the slice type, must satisfy SliceOf[E]) and E (the element type). The argument has type []float64. The compiler matches this against S, so S = []float64. Then the constraint says S must be ~[]E, and []float64 is []float64, so E = float64. The second binding is found through the constraint.
This is constraint type inference at work. The compiler doesn't only look at function arguments; it looks at how type parameters relate through constraints and unifies them.
The pipeline above shows what the compiler does for First(prices). The argument type is the entry point. From there, S gets bound to the slice type. The constraint S ~[]E provides a relationship between S and E, which the compiler unifies to bind E. Once every type parameter is bound, the call is ready.
Cost: Constraint type inference adds compile-time work. For deeply nested constraints, the compiler may take noticeably longer to solve. In practice this doesn't matter, but it's a reason to keep constraints simple.
Generic functions enjoy argument inference. Generic types don't. When you construct a value of a generic type, the compiler has nowhere to look for the type parameter, so you have to write it.
Look at the construction: Stack[Order]{}. The brackets are required. Without them, the compiler sees Stack and asks "stack of what?" The empty struct literal doesn't help because there's no element to look at.
Trying to skip the brackets:
This forces a slightly awkward call site. Many Go libraries work around it with a constructor function. Because the constructor's argument has a concrete type, argument inference kicks in and the caller writes less:
NewStack(Order{...}) passes an argument with a concrete type, so the compiler infers T = Order from the argument. No brackets needed. The trade-off: you've written a constructor that just calls &Stack[T]{}, and you've lost the option of building an empty stack without passing at least one element.
A common compromise is to keep the type and offer a constructor only for the common "create from elements" case:
Two construction styles, same type. &Wishlist[string]{} for the empty case, WishlistOf(...) when you have starter items.
The deeper takeaway: generic types are heavier ergonomically than generic functions. If you can express your idea as a function, you usually get cleaner call sites. Use a generic type when you need to bundle state (like the items slice above) and the call sites can absorb the brackets.
Before Go 1.21, an explicit type argument list had to be all-or-nothing. If a function had three type parameters and you wanted to pin the first, you had to spell out all three. Go 1.21 lifted that restriction: you can supply the leftmost arguments and let the compiler infer the rest.
Here's a function with two type parameters where partial arguments help:
Full inference works here because prices tells the compiler T = float64, and the function literal tells it U = string. No brackets needed.
But suppose the transform function is too generic for inference, or you want to be explicit about T while letting the compiler figure out U:
Map[int] pins only T. U gets inferred from strconv.Itoa's signature (func(int) string), so U = string. Before Go 1.21 you'd have had to write Map[int, string](ids, strconv.Itoa). Now the leftmost-only form is enough.
The rule is left-to-right. You can leave out the rightmost arguments, but not the leftmost. Map[, string](...) is not legal Go.
The diagram above walks through what the compiler does for the partial form. It accepts the explicit T, then runs the same inference machinery on the remaining arguments to find U. Once everything is bound, the call goes through.
Partial arguments apply in three situations. First, when a function uses a constraint that the first argument doesn't pin down (like a callback whose argument type is hard to express). Second, when you want to widen a numeric type (force T = int64 even though the literal would default to int). Third, when you're calling a generic function variable where the compiler has less to work with.
Cost: None at runtime. Partial inference is purely a syntactic convenience that produces the same monomorphized code as the full form.
nil is an untyped value. It has no type until something gives it one. That makes it a problem for inference: when you pass nil, the compiler doesn't know what type to bind.
This fails:
nil could be []int, []string, []Order, any slice type. The compiler refuses to guess. The fix is to give the call something with a type, either by passing a typed nil or by writing an explicit type argument:
Both work because the compiler has a type to bind. typed is []int, so T = int. The explicit [string] says T = string, so the untyped nil becomes a typed []string(nil) at the call site.
The same issue shows up with map and channel arguments, and with functions that accept any pointer type. Any time nil is the only thing being passed, expect to need an explicit type argument.
A subtle case: if a function takes both a typed argument and a nil-able one, inference often works because the typed argument is enough.
products starts as a typed nil []string, so the compiler can infer T = string from it even on the first call when the slice is empty. The trick is that var products []string has a declared type, even though its value is nil. An untyped nil literal wouldn't work.
The compiler treats type inference as a small constraint-solving problem. Roughly, it does three things:
int for integers, float64 for floats, string for strings, and so on.If any step fails (a relationship is inconsistent, or a type parameter has no binding and no default to fall back on), the compiler reports "cannot infer T" or "type mismatch" and the call doesn't compile.
The flow above is what happens for every generic call. It explains a lot of the rules in this lesson at once. Two untyped constants of different default types fail because step 2 produces a contradiction. A return-only type parameter fails because step 1 collects no relationship that involves it, and step 3 has no constant to default. nil fails because it contributes no type information. Mixing a typed argument with an untyped constant works because the typed argument binds T directly, and the constant converts.
With this picture, the error messages become readable. "cannot infer T" means step 2 left T unbound and step 3 had nothing to default. "default type X does not match default type Y" means step 2 found a contradiction between two arguments' default types. "type X does not implement constraint Y" means step 2 produced a binding that the constraint rejects.
A practical implication: if a call's inference is fragile (works today, might break tomorrow when somebody changes a literal), write the type arguments out. The brackets are a stable contract. The inference is a convenience that can shift if you adjust an argument.
10 quizzes