skip to content

Why must a Go generic type such as Stack[T] always be written with explicit type arguments?

level: middleimportance: should knowfreq 48%

answer

  1. no call means no evidence
  2. instantiation is not a call
  3. the constructor decides the ergonomics
  4. a no-argument constructor infers nothing either
  5. Stack[int] spelled out every time

basics

~20 s

Instantiating a generic type is not a function call, so there are no argument values whose types the compiler could match against. Every use of the type must name its type arguments: var s Stack[int], not var s Stack.

solid answer

~40 s

Go infers type arguments only for generic function calls, because a call supplies values whose types can be matched against the written parameter types. A type instantiation such as `Stack[int]` supplies nothing, so the type arguments are always written out — in variable declarations, struct fields, function signatures, composite literals and method receivers alike. The usual ergonomic fix is a constructor function that mentions the type parameter in a parameter: `func StackOf[T any](items ...T) *Stack[T]` lets callers write `StackOf(1, 2, 3)` and get a `*Stack[int]`. Note that a constructor which takes no arguments does not help — `func NewStack[T any]() *Stack[T]` has `T` only in its result, so it must still be called as `NewStack[int]()`. An alias such as `type IntStack = Stack[int]` is the other way to shorten a repeated instantiation.

code

go · 18 lines
go
type Stack[T any] struct {
	items []T
}

func NewStack[T any]() *Stack[T] {
	return &Stack[T]{}
}

func StackOf[T any](items ...T) *Stack[T] {
	return &Stack[T]{items: items}
}

func demo() {
	var a Stack[int]      // type instantiation: always explicit
	b := NewStack[int]()  // T appears only in the result
	c := StackOf(1, 2, 3) // T inferred from the arguments
	_, _, _ = a, b, c
}

go deeper

for a junior

Remember that a generic container is written with its type in brackets every single time — var s Stack[int] — and that leaving the brackets off is a compile error, not a default.

for a middle

Be ready to explain the reason rather than the rule: a type instantiation passes no values, so unification has nothing to match, whereas a call does. Then show the constructor that restores inference.

for a senior

Demonstrate that you review generic containers for their construction path, and that you know each instantiation is a separate type with no assignability between instantiations.

for a principal

Own the readability cost: a deeply parameterised type spreads its type arguments across every signature that touches it, so committing to one in a shared package is a decision other teams inherit and cannot cheaply reverse.

## Two different things get type arguments Go's generics attach type parameters to two kinds of declaration: ```go func Map[T, U any](s []T, f func(T) U) []U // generic function type Stack[T any] struct{ items []T } // generic type ``` Both must be *instantiated* before they mean anything concrete. The asymmetry candidates trip on is that only one of them gets inference: **generic functions do, generic types never do.** ## Why the rule is what it is Inference works by unification against the types of the values passed at a call. A call is the event that provides evidence: ```go Map(nums, strconv.Itoa) // []int vs []T gives T = int ``` Writing `Stack[int]` is not a call. It is a *type expression* — it appears where a type is expected: in `var s Stack[int]`, in a struct field, in a parameter list, in a return type, in `make(chan Stack[int])`, in a composite literal `Stack[int]{}`, and in the receiver of a method (`func (s *Stack[T]) Push(v T)`, where `T` is the receiver's declared type parameter, not an inferred one). None of those positions carries values whose types could be matched. With no evidence, there is no inference, and the language does not guess — `var s Stack` is a compile error saying the generic type needs type arguments. This is not an oversight. Inferring type arguments for generic types has been discussed repeatedly and remains unimplemented; assume you must write them. ## The consequence for API design: constructors Since a generic type never infers, the ergonomics of your package come from the *functions* around it. Compare three shapes: ```go type Stack[T any] struct { items []T } // 1. Zero value: always explicit at the declaration site. var a Stack[int] // 2. Constructor with no arguments: T appears only in the result, // so this infers nothing either. func NewStack[T any]() *Stack[T] { return &Stack[T]{} } b := NewStack[int]() // 3. Constructor that takes values: T is reachable, so it infers. func StackOf[T any](items ...T) *Stack[T] { return &Stack[T]{items: items} } c := StackOf(1, 2, 3) // *Stack[int] ``` Shape 3 is why `slices` and `maps` helpers read so cleanly: their type parameters always appear in a parameter. If you are writing a container other teams will use, ask whether the common construction path passes any values at all. If it does, take them and let inference do the work. If it genuinely does not, accept `NewStack[int]()` rather than inventing a dummy parameter — a fake argument to please inference is worse than an honest type argument. ## Shortening repeated instantiations When one instantiation dominates a package, an alias removes the repetition without hiding anything: ```go type IntStack = Stack[int] ``` Aliases to an already-instantiated generic type have been legal since generics arrived. Separately, aliases may themselves take type parameters (`type Set[T comparable] = map[T]struct{}`), which is useful for naming a generic shape — but that alias, too, is written with type arguments wherever it is used. ## Where this bites in practice - **Struct fields and function signatures** repeat the type arguments everywhere: a `Cache[string, []byte]` threaded through five layers is written five times, and changing the value type is a five-file edit. That is a real argument for wrapping deep instantiations in a named type or alias. - **Nested generics** compound quickly: `Result[map[string]Stack[int]]` is legal and unreadable, and reviewers should push back on it. - **Method sets** are per-instantiation: `Stack[int]` and `Stack[string]` are distinct types with no relationship, so neither is assignable to the other and neither satisfies an interface on behalf of the other. ## What an interviewer is checking Whether you can state the boundary crisply — inference is a property of calls, not of types — and whether you have drawn the design conclusion from it: the readability of a generic container in Go is mostly decided by the constructor you give it.

  • How would you make a generic container pleasant to construct without type arguments?
    Give it a constructor whose parameters mention the type parameter, so inference has something to work with: `func StackOf[T any](items ...T) *Stack[T]` lets callers write `StackOf(1, 2, 3)`. A constructor taking no arguments cannot help, because its type parameter appears only in the result. Where one instantiation dominates, an alias such as `type IntStack = Stack[int]` removes the repetition instead.
  • Are Stack[int] and Stack[string] related types in any way?
    No. Each instantiation is its own distinct type, with its own method set and its own memory layout. Neither is assignable to the other, and there is no covariance: a `[]Stack[int]` is not usable where a `[]Stack[string]` is expected. Code that needs to hold either must go through an interface or take the element type as its own type parameter.
  • Where exactly do the type arguments have to be repeated?
    Everywhere the type is named: variable declarations, struct fields, parameter and result types, composite literals, map and channel element types, and type assertions. Method declarations are the exception — the receiver declares the type parameters (`func (s *Stack[T]) Push(v T)`), so the body uses `T` directly.

saying these in an interview costs you the question

  • Says the compiler infers a generic type's arguments from the first assignment
  • Thinks a no-argument constructor removes the need for type arguments
  • Believes Stack[int] can be assigned to a variable of type Stack[string]
  • Confuses the receiver's declared type parameter with an inferred one
  • Claims writing Stack without arguments defaults the parameter to any