---
title: 'When Generics'
source: 'https://academia.sh/en/courses/go-type-system/when-generics'
course: 'Interfaces, Errors and Generics'
language: en
updated: '2026-09-14T10:59:34+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Writes the same total computation with the empty interface, with generics, and with an interface contract, and measures each writing's type-assertion and declaration-line count"
    - "Shows, by measuring element size, that generics' gain disappears once the type parameter is pinned to an interface"
    - "Gathers, in the closing table, the distribution of the course's sixteen bindings across declaration, use site, and value"
---

# When Generics

The same total-area computation is built three ways: with the empty interface, three type assertions; with generics, zero, but T is pinned to a single concrete type; with an interface contract, also zero, but different concrete types can sit in the same slice. The limiting measurement shows that once T is pinned to an interface (to accept a heterogeneous collection), generics' gain disappears: element size rises from 8 bytes to 16 bytes, exactly like an ordinary interface value. The course found 4 of sixteen bindings in the declaration, 4 at the use site, 8 inside the value.

The Generic Data Structures lesson showed that genericity stayed inside the source across three
lessons: the type parameter was either inferred or written explicitly in the declaration, but it
never entered the value. The interfaces topic measured the exact opposite — the dynamic type was
carried inside the value and could only be read by asking the value. This closing lesson places
these two writings, and the interface contract standing between them, side by side by **doing
the same job three times**: a total-area computation is built with the empty interface, with
generics, and with an interface contract, and in all three, where the binding is built is
counted.

This was the course's third claim: generics and interfaces do the same job in two separate
places. In generics the binding is built in the declaration and at the use site, it never enters
the value; in an interface the binding enters the value. This lesson measures this difference for
the first time by directly running it, writing the same computation three times.

The previous three lessons always measured generics on their own — never placed side by side
with an interface. This left a debt in the course's general framework: the claim "in generics
the binding never enters the value" was, without a comparison being made, only an observation.
This lesson closes that debt: the same job is written with both generics and an interface and
measured in the same run, and the difference turns from a claim into a number (8 bytes against
16 bytes).

## The Same Job, Three Writings

The job measured is simple: computing the total area of shapes in a collection. `Square` and
`Circle` each carry their own `Area()` method; the `Shape` interface lists this single method.
This job was chosen deliberately: the area computation **genuinely varies** by type — a square's
area is the side squared, a circle's area is the radius squared times a constant — but the
summing operation itself is the same for every type. The job all three writings have to
accomplish therefore splits into two parts: the type-specific part (`Area()`'s own body) and the
type-independent part (accumulating the total).

```go
// three.go — the same job in three writings: empty interface, generics, interface contract
package main

import (
	"fmt"
	"unsafe"
)

type Square struct{ Side float64 }

func (s Square) Area() float64 { return s.Side * s.Side }

type Circle struct{ Radius float64 }

func (c Circle) Area() float64 { return 3.14159 * c.Radius * c.Radius }

type Shape interface{ Area() float64 }

// 1) with the empty interface: a type assertion (a type-switch branch) is needed for every element.
func totalAreaAny(s []any) (total float64, assertionCount int) {
	for _, e := range s {
		switch v := e.(type) {
		case Square:
			assertionCount++
			total += v.Area()
		case Circle:
			assertionCount++
			total += v.Area()
		}
	}
	return
}

// 2) with generics: the body is the same for every T, and as long as T stays homogeneous there is no type assertion at all.
func totalAreaGeneric[T Shape](s []T) (total float64) {
	for _, e := range s {
		total += e.Area()
	}
	return
}

// 3) with an interface contract: different concrete types in the same slice, no type assertion at all.
func totalAreaInterface(s []Shape) (total float64) {
	for _, e := range s {
		total += e.Area()
	}
	return
}

func main() {
	fmt.Println("-- same job, three writings --")

	mixed := []any{Square{Side: 2}, Circle{Radius: 1}, Square{Side: 3}}
	tAny, assertions := totalAreaAny(mixed)
	fmt.Printf("empty interface  total=%.4f  type assertions=%d  declaration lines=0\n", tAny, assertions)

	homogeneous := []Square{{Side: 2}, {Side: 3}}
	tGeneric := totalAreaGeneric(homogeneous)
	fmt.Printf("generic          total=%.4f  type assertions=0  declaration lines=1 (constraint)\n", tGeneric)

	contracted := []Shape{Square{Side: 2}, Circle{Radius: 1}, Square{Side: 3}}
	tInterface := totalAreaInterface(contracted)
	fmt.Printf("interface        total=%.4f  type assertions=0  declaration lines=1 (interface)\n", tInterface)

	fmt.Println()
	fmt.Println("-- limiting measurement: the gain disappears once T is pinned to an interface --")
	heterogeneous := []Shape{Square{Side: 2}, Circle{Radius: 1}}
	tDegenerate := totalAreaGeneric(heterogeneous)
	fmt.Printf("totalAreaGeneric[Square](homogeneous)   T=Square  element size=%d bytes  result=%.4f\n",
		unsafe.Sizeof(homogeneous[0]), tGeneric)
	fmt.Printf("totalAreaGeneric[Shape](heterogeneous)  T=Shape   element size=%d bytes  result=%.4f\n",
		unsafe.Sizeof(heterogeneous[0]), tDegenerate)
}
```

```
-- same job, three writings --
empty interface  total=16.1416  type assertions=3  declaration lines=0
generic          total=13.0000  type assertions=0  declaration lines=1 (constraint)
interface        total=16.1416  type assertions=0  declaration lines=1 (interface)

-- limiting measurement: the gain disappears once T is pinned to an interface --
totalAreaGeneric[Square](homogeneous)   T=Square  element size=8 bytes  result=13.0000
totalAreaGeneric[Shape](heterogeneous)  T=Shape   element size=16 bytes  result=7.1416
```

**GE13.** The first three lines compare three sources of the same computation. The empty
interface writing runs a type assertion (a branch of the type switch) for all three of its three
elements and declares no interface at all — the contract is written nowhere in the source, only
the `case` branches inside the `switch` implicitly say which types are expected. The generic
writing runs with zero type assertions, but `homogeneous` contains only `Square` — `T`, as the
Generic Data Structures lesson measured, is pinned to a single concrete type in the caller's own
declaration. The interface writing also runs with zero type assertions, and moreover can carry
`Square` and `Circle` in the **same slice** — `contracted` holds both at once, which
`homogeneous` could not have held.

## The Limiting Measurement: The Gain Disappears Once T Is Pinned to an Interface

**GE14.** The last two lines show the limit of genericity. Because `totalAreaGeneric`'s
constraint is a method list, it can also instantiate `T` with an interface type —
`totalAreaGeneric[Shape](heterogeneous)` compiles and accepts `Square` and `Circle` in a mixed
slice. But its cost is measured: with `T=Square`, every element is 8 bytes (just the `Side`
field itself); with `T=Shape`, every element is 16 bytes — the sum of the type pointer and the
data pointer an interface pair carries. Pinning `T` to an interface breaks genericity's promise
of zero cost: the code is now running something no different at all from `totalAreaInterface`, it
has just hidden this behind a type parameter.

`totalAreaGeneric`'s body stayed letter-for-letter the same across both calls — the line `for _,
e := range s { total += e.Area() }` changed for neither `T=Square` nor `T=Shape`. What changed
was only what `T` was, and this change doubled the element size. This comes from the very
definition of genericity: a generic body, from a single source, runs the **same** operation for
every `T` it is instantiated with. With `T=Square`, this operation calls `Square.Area()`
directly, with no extra layer at all; with `T=Shape`, the same operation now makes an indirect
method call through an interface value — even though not a single line changed in the source,
what happens at run time changes.

This gives a definite answer to the course's question: generics do not always win. If a
collection's elements are all the same concrete type, and the operation performed on them is the
same for every type, generics provide both zero type assertions and zero extra bytes. If the
elements have to carry different concrete types — if behavior genuinely has to vary by type —
two choices remain: pin `T` to an interface and give up genericity's gain, or write an interface
from the start. The second is the more honest writing, because the source already says what is
going to happen — that more than one concrete type will sit in the same place. A generic body,
by definition, stays the **same** for every `T` it is instantiated with; wanting it to behave
differently for different `T`s contradicts genericity itself.

## Course Wrap-Up

Interfaces, Errors and Generics opened with a single question: in Go, a type satisfying an
interface is never written anywhere at all — so where is this binding built? Thirteen lessons
asked this question across three mechanisms: in interfaces (implicit satisfaction, the empty
interface, type assertion, embedding, consumer-side definition), in error values (the error
interface, sentinel errors, wrapping, panic), and in generics (the type parameter, the type
constraint, generic data structures, and this lesson's comparison of all three).

The first claim was confirmed: eight of sixteen bindings are carried inside the value, because a
Go interface value is a pair — both a concrete type and data. The second claim was confirmed:
four bindings are built at the use site, and the source never says anywhere at all that a type
satisfies an interface. The third claim was run directly in this lesson: generics and interfaces
do the same job in two separate places — in generics the binding stays in the declaration and at
the use site, in an interface it enters the value — and this gain of generics disappears the
moment the type parameter is pinned to an interface.

One of these three mechanisms, "use site," carries a warning: the use site is where the compiler
looks, not where the source points. The Generic Functions lesson proved this by asking the
compiler directly — writing the source to a temporary package and reading only whether it
passed; the Generic Data Structures and Type Constraints lessons used the same method. This
difference between an indirect proof (only showing that a contract is not written in the source)
and a direct proof (seeing, by running it, where the compiler stops) is part of the course's own
measurement discipline: if a lesson says "this is built at the use site," it must either call the
compiler and show where it stops, or explicitly write that the proof is indirect.

The course closes without overgeneralizing: these three mechanisms (declaration, use site, value)
do not cover all of Go's type system, they only classify the answers given to the single
question these thirteen lessons asked — where is this binding built? The table below gathers
these thirteen answers into a single row each.

| Lesson | Binding measured | Where it is built (declaration · use site · value) | Limiting measurement |
|---|---|---|---|
| The Interface Concept | A type implicitly satisfying an interface; the small-interface principle | use site (the 4/4/8 distribution was produced here) | The compile-time conformance proof carries the binding into the declaration; without the proof, the compiler stays silent |
| The Empty Interface and `any` | The empty interface carrying type information into the value | value | An interface holding a `nil` pointer is not `nil` |
| Type Assertion and Type Switch | Asking the value: the two-value assertion, the branch a type switch selects | value | The same value can fit both a concrete and an interface branch; the branch chosen depends on write order |
| Interface Embedding | Contract composition | declaration | Embedding merges only the contract, not the implementation |
| Interface Design | The same concrete type satisfying four independently defined consumer interfaces; zero references in its own source | use site | Two separately named interfaces with the same signature become implicitly equivalent; equivalence breaks only once the signature diverges |
| The Error Interface | An error being an implicitly satisfied value | use site + value | A non-`nil` interface can carry a `nil` concrete value |
| Sentinel Errors | Error identity sitting in the value | value | Once identity changes, the caller silently falls into the wrong branch, producing no warning |
| Error Wrapping and Unwrapping | The chain being carried in the value | value | Using `%v` breaks the chain, the text stays the same |
| Panic and Recovery | A panic leaving no trace at all in the signature | (no binding is built) | Recovering does not destroy the panic, it carries it; if the recovered value is not written down, the information is lost entirely |
| Generic Functions | A type parameter inferred at the call site | use site | `==` and `+` cannot be written with an unconstrained `T` |
| Type Constraints | A constraint listing a type set | declaration | An interface with a type-set term is valid only in constraint position |
| Generic Data Structures | Instantiation sitting in the caller's declaration | declaration | Comparability also depends on the container's own field structure |
| When Generics | Where the same job binds across three writings | (comparison of all three) | Once `T` is pinned to an interface, generics' gain disappears |

The three mechanisms measured across the course ultimately reduce to a single habit: asking,
while reading a source, "where is this binding built?" If a declaration line is searched for and
found, the binding is there; if not, two places remain — either a binding the compiler builds at
the use site (the only way to see this is to call the compiler), or a binding that can only be
learned by asking the running value. Asking these three questions in order is a measurable way
of separating what Go's type system says in the source from what it does not, while reading it.

## Summary

- The same total-area computation was built three ways: the empty interface required three type
  assertions, generics and the interface ran with zero type assertions.
- Generics stay zero-cost only when `T` is pinned to a single concrete type; once `T` is pinned
  to an interface, element size rises from 8 bytes to 16 bytes and the gain disappears.
- An interface is needed when behavior genuinely has to vary by type, because a generic body
  stays the same for every `T` it is instantiated with.
- Of the sixteen bindings measured across the course, four were built in the declaration, four
  at the use site, eight inside the value; this distribution classifies not Go's type system,
  only the question these thirteen lessons asked.
- Generics and interfaces do the same job in two separate places: in generics the binding never
  enters the value at all, in an interface it does — and this difference is the measurable
  counterpart of when to choose which.
