---
title: 'Generic Data Structures'
source: 'https://academia.sh/en/courses/go-type-system/generic-data-structures'
course: 'Interfaces, Errors and Generics'
language: en
updated: '2026-09-14T10:59:34+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures that two instantiations of the same generic structure show up in %T as two separate dynamic types and cannot substitute for each other'
    - 'Compares the number of type assertions a container written with the empty interface requires against the number the generic structure requires'
    - "Bounds, by asking the compiler, that a generic type's comparability depends on both its own field structure and its type parameter"
---

# Generic Data Structures

A generic data structure's type parameter is written explicitly in the caller's declaration: Stack[int] and Stack[string] show up in %T as two separate dynamic types, and one cannot substitute for the other. The measurable counterpart of type safety: using an element in the same structure written with the empty interface needs a type assertion, in the generic structure it needs zero. The limiting measurement shows that comparability depends both on the container's own field structure and on the type parameter itself.

The Type Constraints lesson showed that a binding is built in two layers: the operations a
constraint allows sit in the declaration, which concrete type those operations will work for is
built at the use site. This lesson measures the same two layers over a data structure, but with
one difference: a generic function's type parameter was **inferred** at the call site, it was
never written in the source at all. A generic data structure's type parameter, by contrast, is
written **explicitly** in the caller's own declaration — whoever writes `Stack[int]` places
`[int]` with their own hand, the compiler does not read it from an argument. Same question, two
different answers: the binding is still built outside the source (outside the function's own
definition), but this time not by inference, by a declaration the caller writes.

The source of this difference sits in the same place as the difference between a function call
and a type declaration. In a function call, arguments already exist — the compiler can look at
their type and infer `T`. In a type declaration, there is no argument yet at all; `Stack[int]`
is not a value, it is a **type name**, and there is no argument at all for the compiler to infer
which type parameter that type name is instantiated with. The compiler needs something to read
in order to infer; in a generic function this is the call's arguments, in a generic type there
is no such argument — the caller itself has to write it.

## Instantiation Sits in the Caller's Declaration

A generic data structure is a type declaration that adds a type parameter list right after its
name: `type Stack[T any] struct{ items []T }`. This single source builds a **type-safe
container** pattern usable both for `Stack[int]` and for `Stack[string]` — but before it can be
used, it first has to be **instantiated**, and this instantiation is always written in the
using side's own declaration: in a variable definition, a field type, or a return type.

`Stack`'s methods do not redefine their own type parameter; the `(s *Stack[T])` receiver
expression **carries** the `T` already opened in the type's own declaration into the method.
`Push(v T)` and `Top() (T, bool)` therefore open no new square brackets — the `T` they use is
already bound to `int` the moment `Stack[int]` is instantiated, together with `Stack` itself.
Instantiation happens once, in the type's own declaration; every method bound to that type
shares this single instantiation.

```go
// stack.go — the instantiation of a generic data structure sits in the caller's declaration
package main

import "fmt"

type Stack[T any] struct{ items []T }

func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }

func (s *Stack[T]) Top() (T, bool) {
	var zero T
	if len(s.items) == 0 {
		return zero, false
	}
	return s.items[len(s.items)-1], true
}

func main() {
	var intStack Stack[int]
	intStack.Push(3)
	intStack.Push(5)

	var stringStack Stack[string]
	stringStack.Push("apple")

	i, _ := intStack.Top()
	s, _ := stringStack.Top()

	fmt.Printf("%-24s type=%-16T value=%v\n", "intStack", intStack, i)
	fmt.Printf("%-24s type=%-16T value=%v\n", "stringStack", stringStack, s)
	fmt.Println("arithmetic on the result:", i+1)
}
```

```
intStack                 type=main.Stack[int]  value=5
stringStack              type=main.Stack[string] value=apple
arithmetic on the result: 6
```

**GE9.** The line `var intStack Stack[int]` is itself a **declaration** stating that `T` will be
`int` — compared with the `firstElement([]int{7, 2, 9})` call from the Generic Functions
lesson, the difference is plain: there, what `T` was got **inferred** from the argument's type,
and nowhere in the source did `int` ever appear written at all; here, `int` sits right in the
line, visible to the eye. `%T` confirms this: `intStack` and `stringStack` were produced from
the same source, but their dynamic types at run time are `main.Stack[int]` and
`main.Stack[string]` — two entirely separate types, just as `[4]int` and `[3]int` are separate
types (Arrays and Slices, M09/K01). In the common measurement, this binding counts as one of the
declaration examples under the name "instantiation of a generic type."

It is also worth noting that the line `var intStack Stack[int]` contains no call at all — this
is a zero-value declaration; no `New`-like constructor is called. As the Zero Values lesson
(M09/K01) measured, a struct's zero value is built from its own fields' zero values: the
`items` field here starts as a `nil` slice. This shows the difference between instantiation and
being given an initial value: writing `Stack[int]` binds `T`, but it does not fill `items`; the
first `Push` call runs `append` on a `nil` slice, and as the Variadic Functions lesson
(M09/K01) measured, this works without a problem — appending to a `nil` slice grows it, it does
not panic.

## The Measurable Counterpart of Type Safety

The two instantiations being separate types has a consequence: one cannot **substitute** for the
other. The measurement below counts this, and the cost of writing the same job with the empty
interface, by asking the compiler.

```go
// safety.go — the measurable counterpart of type safety: how many type assertions are needed
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

func compiles(source string) bool {
	dir, err := os.MkdirTemp("", "trial")
	if err != nil {
		return false
	}
	defer os.RemoveAll(dir)
	os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module trial\n\ngo 1.24\n"), 0o644)
	os.WriteFile(filepath.Join(dir, "code.go"), []byte(source), 0o644)
	cmd := exec.Command("go", "build", "./...")
	cmd.Dir = dir
	cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
	return cmd.Run() == nil
}

func classifySource(source string) string {
	if compiles(source) {
		return "compiled"
	}
	return "did not compile"
}

func main() {
	substitution := `package trial
type Stack[T any] struct{ items []T }
func F() {
	var a Stack[int]
	var b Stack[string]
	b = a
	_ = b
}`

	anyNoAssertion := `package trial
type AnyStack struct{ items []any }
func (s *AnyStack) Push(v any) { s.items = append(s.items, v) }
func (s *AnyStack) Top() any { return s.items[len(s.items)-1] }
func F() {
	var s AnyStack
	s.Push(3)
	t := s.Top()
	_ = t + 1
}`

	anyWithAssertion := `package trial
type AnyStack struct{ items []any }
func (s *AnyStack) Push(v any) { s.items = append(s.items, v) }
func (s *AnyStack) Top() any { return s.items[len(s.items)-1] }
func F() {
	var s AnyStack
	s.Push(3)
	t, ok := s.Top().(int)
	_ = ok
	_ = t + 1
}`

	genericNoAssertion := `package trial
type Stack[T any] struct{ items []T }
func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
func (s *Stack[T]) Top() T { return s.items[len(s.items)-1] }
func F() {
	var s Stack[int]
	s.Push(3)
	t := s.Top()
	_ = t + 1
}`

	fmt.Printf("%-50s %s\n", "case", "result")
	fmt.Printf("%-50s %s\n", "Stack[string] = Stack[int] (substitution)", classifySource(substitution))
	fmt.Printf("%-50s %s\n", "any stack, arithmetic with 0 type assertions", classifySource(anyNoAssertion))
	fmt.Printf("%-50s %s\n", "any stack, arithmetic with 1 type assertion", classifySource(anyWithAssertion))
	fmt.Printf("%-50s %s\n", "generic stack, arithmetic with 0 type assertions", classifySource(genericNoAssertion))
}
```

```
case                                               result
Stack[string] = Stack[int] (substitution)          did not compile
any stack, arithmetic with 0 type assertions       did not compile
any stack, arithmetic with 1 type assertion        compiled
generic stack, arithmetic with 0 type assertions   compiled
```

**GE10.** The first line directly shows that the two instantiations cannot substitute for each
other: `b = a` does not compile, because `Stack[string]` and `Stack[int]` are, to the compiler,
as separate as `[4]int` and `[3]int`. This is the direct consequence of instantiation sitting in
the caller's declaration — the type written in the declaration becomes part of that variable's
permanent identity.

**GE11.** The last three lines compare the same job written two ways. `AnyStack`, written with
the empty interface, returns `any` from `Top()`; trying to add `+1` directly to this value does
not compile, because `any`'s concrete type is not known in the source. The only way out is
writing a **type assertion** (`s.Top().(int)`) — the same mechanism the Type Assertion and Type
Switch lesson measured, appearing here as a burden that has to be written over and over at every
read point of a container. The generic `Stack[int]`, by contrast, returns `int` directly from
`Top()`, and `+1` compiles with no type assertion written at all — this is the measurable
counterpart of type safety: the number of type assertions needed to do the same job is at least
one in the `any` writing, zero in the generic writing.

This difference compounds as the container grows. Every read point of `AnyStack` — `Top`, the
return of a `Bottom` method if one were added, every element of a slice a `Contents` method
might return — has to carry its own type assertion, because the container hides type
information behind `any`, and every reader has to recover this information on its own. In
`Stack[T]`, type information is part of the container's own type; no matter how many new
methods are added, none of them ever has to write a type assertion, because their return types
are already `T` — a concrete type known at compile time. A type assertion is not free either:
if a value of the wrong type was added (which is possible for an `any` container with no
compile error at all), the assertion either panics at run time or silently fails in its
two-value form — in the generic writing this entire class of error never exists at all, because
adding an element of the wrong type is already rejected at compilation.

## The Limiting Measurement: Comparability Depends on Both the Structure and T

Type safety is not free. Whether a generic type can be compared with `==` depends not only on
the type parameter, but also on the container's **own field structure** — and these two factors
work independently.

```go
// comparison.go — a generic type's comparability depends on both its own field structure and T
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

func compiles(source string) bool {
	dir, err := os.MkdirTemp("", "trial")
	if err != nil {
		return false
	}
	defer os.RemoveAll(dir)
	os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module trial\n\ngo 1.24\n"), 0o644)
	os.WriteFile(filepath.Join(dir, "code.go"), []byte(source), 0o644)
	cmd := exec.Command("go", "build", "./...")
	cmd.Dir = dir
	cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
	return cmd.Run() == nil
}

func classifySource(source string) string {
	if compiles(source) {
		return "compiled"
	}
	return "did not compile"
}

func main() {
	stackWithSlice := `package trial
type Stack[T any] struct{ items []T }
func F() bool { var a, b Stack[int]; return a == b }`

	boxComparableT := `package trial
type Box[T any] struct{ Value T }
func F() bool { var a, b Box[int]; return a == b }`

	boxIncomparableT := `package trial
type Box[T any] struct{ Value T }
func F() bool { var a, b Box[[]string]; return a == b }`

	fmt.Printf("%-48s %s\n", "case", "result")
	fmt.Printf("%-48s %s\n", "Stack[int] == Stack[int] (slice field)", classifySource(stackWithSlice))
	fmt.Printf("%-48s %s\n", "Box[int] == Box[int] (T direct, T=int)", classifySource(boxComparableT))
	fmt.Printf("%-48s %s\n", "Box[[]string] == Box[[]string] (T=slice)", classifySource(boxIncomparableT))
}
```

```
case                                             result
Stack[int] == Stack[int] (slice field)           did not compile
Box[int] == Box[int] (T direct, T=int)           compiled
Box[[]string] == Box[[]string] (T=slice)         did not compile
```

**GE12.** The three lines separate two independent factors. `Stack[T]` can never be compared —
even when `T` is `int` — because `items` is a **slice** field, and as the Maps and Arrays and
Slices lessons (M09/K01) measured, slices cannot be compared with `==`; this restriction comes
from the container's own field structure, not from the type parameter. `Box[T]` builds a
different structure — its `Value` field holds `T` directly, not a slice — and here comparability
is handed entirely over to `T`: `Box[int]` is comparable because `int` is comparable,
`Box[[]string]` is not comparable because this time `T` itself (`[]string`) is a type that
cannot be compared. A generic type's comparability therefore cannot be reduced to a single
question; it requires looking at both the container's own design and the `T` it is filled with.

This also shows why constraints are not enough on their own: turning the `Stack[T any]`
constraint into `Stack[T comparable]` would work for `Box`, but changes nothing at all for
`Stack` — however comparable `T` is, as long as `items` is a slice, `Stack` remains
uncomparable. A type constraint bounds only `T`, not the field layout of the type carrying `T`.

This distinction has a concrete counterpart: the Maps lesson (M09/K01) measured that a map's key
type must be comparable. `Stack[int]` can never be a map key — no matter what `T` is — because
the incomparability comes from the `items` field, and this field stays exactly as it is at every
instantiation. `Box[int]`, on the other hand, can be a key, because its `Value` field holds `int`
directly, and `int` is comparable; `Box[[]string]` cannot, because this time the `T` it carries
is itself incomparable. The same two container patterns answer, in advance, the question "can
this type be a map key" just by looking at the field structure in the source — with no need at
all to try a key at run time.

Across three lessons, genericity stayed inside the source the whole time: the type parameter was
either inferred or written explicitly, but in both cases, someone reading the source, looking in
the right place, could see which concrete type was being used. This is the exact opposite
posture of the implicit satisfaction the Interface Concept lesson measured: there, a type
satisfying an interface was never written anywhere at all; here, which type a container works
with can **always** be read either from the call's argument or from the declaration's square
brackets. `Stack[int]`'s dynamic type can be known without ever asking `%T` at all, because it is
written right in the source itself; an interface value's dynamic type, by contrast, could only
be learned by asking the value. The course's closing lesson places these two postures — always
readable versus learnable only by asking the value — side by side by doing the same job.

This readability is not free: someone reading `Stack[int]` learns that `T` is `int` from the
line itself, but this information cannot be inferred anywhere else — like from an argument, as
in the Generic Functions lesson; it is not possible for the writer to forget to write it,
because the syntax makes it mandatory. The predictability genericity gives comes with this small
writing burden the caller takes on.

## Summary

- A generic data structure's type parameter, unlike a generic function's, is written explicitly
  in the caller's own declaration — it is never inferred.
- `Stack[int]` and `Stack[string]`, even though produced from the same source, are two separate
  dynamic types read at run time with `%T`, and one cannot substitute for the other.
- The measurable counterpart of type safety: writing the same job with the empty interface
  requires at least one type assertion, in the generic writing this count is zero.
- A generic type's comparability depends on two independent factors: the container's own field
  structure (if it carries a slice field, it can never be compared) and — if the container holds
  `T` directly — `T`'s own comparability.
- A type constraint bounds only the type parameter; it cannot change the limits coming from the
  container's own field layout.

## Next Step

Across three lessons, genericity was always measured on one side: the binding was built either
in the declaration or at the use site, and it never entered the value. The interface topic
measured the exact opposite — type information was carried inside the value and could only be
read by asking the value. The course's final lesson places these two writings, and the interface
contract standing between them, side by side by doing the same job three times: how many type
assertions each writing requires, which one stays at compile time and which one falls to run
time, and whether genericity always wins.
