Go · Language Features

Golang Generics
Part 1 — Overview and Basic Syntax

Write a function without fixing its types up front
— what Go 1.18 changed

type parameter constraint generic struct type inference
The syntax we cover today Go 1.18+
Generics ├─ type parameters [T any] │ ├─ generic functions │ └─ generic structs ├─ constraints any · union · interface └─ type inference usually omittable
Part 2 digs into constraints
We look at Generics, which landed in Go 1.18 (March 2022), starting from "what problem did it come to solve" rather than memorizing syntax. Say up front that Part 1 covers the overview and basic syntax, and that constraints themselves are Part 2. Asking for a show of hands on who wrote pre-1.18 code makes the rest land much better.
01

What Generics Are

When the type gets decided — that single thing is what changes. And why Go held this feature back for twelve years.

Three to four minutes. Keep the definition itself short and spend more time on "why was Go so late." That background is what makes the simplicity of Go Generics read as a choice rather than a shortfall.
conceptdefinition

Generics defer the type decision to the call site

  • The type is fixed when used, not when defined
  • Java · C++ · Rust have had this for years
  • Go got it officially in 1.18 (March 2022)
  • The case for it was one thing — removing duplication
// T is a type that has not been decided yet
func printAny[T any](a T) {
    fmt.Println(a)
}

// decided at the moment of the call
printAny(10)      // T = int
printAny("hello") // T = string
in one line A device that stops you from writing the same logic over and over just because the types differ.
"Generics" sounds heavy but the essence is one line — leave the type slot blank and fill it in later. Do not explain the syntax in detail here. Just note that brackets appear, and move on. Syntax gets proper treatment in chapter 03.
backgroundwhy only now

Go held out for a long time to protect simplicity

Why they deferred it

Go's core philosophy is simplicity. Rob Pike and the other designers deliberately left many features out to keep language complexity down.

Generics were on that list for years

Why they finally added it

It was the most requested feature from the community. Ian Lance Taylor's Type Parameters Proposal was accepted and shipped in 1.18.

The interface{} workaround had clear limits

design direction C++ template metaprogramming and Haskell's higher-kinded types were left out on purpose; only pragmatic type parameters went in.
Plenty of people compare Go Generics to other languages and call them anemic, but that is an explicit choice, not a defect. Nailing this down here makes the later "why can't I do X?" questions easy to answer. Expect: "what happens to pre-1.18 code?" → backward compatibility is fully preserved.
02

Two Pains From the Pre-Generics Era

interface{}, which defers type checking to runtime, and copy-pasted functions that multiply with every type. These two were the real case for adoption.

This chapter is the persuasion segment. Lead with syntax and you get "so what is it good for?"; lead with the pain and the syntax reads as the answer. Budget five to six minutes.
limitsbefore → after

interface{} defers type checking until runtime

// interface{} based — the return is interface{}, so an assertion is required
func foo1(a interface{}) interface{} { return a }

// generics based — the return is T, so no assertion is needed
func foo2[T any](a T) T { return a }

func main() {
    var a, b int = 10, 20
    var c int

    c = foo1(a).(int)  // assertion required — can fail at runtime
    c = foo2(b)         // type decided automatically — safe at compile time
}
what changed One fragment, .(int), disappeared. But what disappeared is not syntax — it is the possibility of a runtime panic.
Point at exactly one line here: `.(int)`. foo1 passes the compiler without it knowing "this is an int," and you pay the cost at runtime when you are wrong. foo2, the compiler knows. If time is short, keep this slide and skip the next two.
asidetype assertion

A type assertion is "unwrap it, and blow up if it's wrong"

  • The operation that pulls a concrete type out of interface{}
  • Written as value.(Type)
  • Wrong type means a runtime panic
  • The second return value ok makes it safe
var val interface{} = "hello"

s := val.(string) // OK
i := val.(int)    // panic!

// the safe form
s, ok := val.(string) // true
i, ok := val.(int)    // false, 0
with Generics The type is fixed at compile time, so the assertion itself becomes unnecessary.
If the audience already knows this, spend thirty seconds. If some do not, stress that `i := val.(int)` compiles fine — that is the heart of the problem. Add that the `, ok` form only prevents the panic; the type-branching code stays.
summarythree costs

The flexibility of interface{} has a price tag

Weak type safety

A wrong assertion surfaces as a runtime panic, not a compile error.

If tests miss it, you meet it after deploy

Casting overhead

You repeat .(Type) every time you use the value.

Call sites get noisy

Poor IDE support

The return type is interface{}, so completion and type checking are blocked.

The editor cannot help you

one shared cause All three come from not telling the compiler the type.
Do not read the three cards one by one — say only the closing callout: three symptoms, one cause. A war story helps here (an outage caused by a type-assertion panic in production, for instance).
limitscopy-paste hell

Writing the same function three times for three types

func minInt(a, b int) int {
    if a < b { return a }
    return b
}

func minInt16(a, b int16) int16 {
    if a < b { return a }   // logic is completely identical
    return b
}

func minFloat64(a, b float64) float64 {
    if a < b { return a }   // the same logic again...
    return b
}
the real problem Fixing a bug means fixing all three. It grows linearly with every type you support.
Mentioning that the Go standard library carried plenty of these traces resonates. The real-world pain is not the duplication itself but forgetting one when you fix something. This slide is the hook into the next chapter — "let's cut this down to one."
03

Basic Generics Syntax

From declaring type parameters to generic functions, constraints, and generic structs. We follow how the min duplication we just saw collapses into one.

The main body of the talk. Budget ten minutes or more. Do not enumerate syntax — attach "what earlier problem does this solve" to every slide as you move.
syntaxanatomy

Type parameters are declared inside square brackets

1
func name[T constraint](param T) T
The brackets come right after the function name, before the parentheses
2
T
The type parameter's name. By convention a single uppercase letter
3
constraint
The condition limiting which types may appear. any is the loosest
only one thing to memorize Parentheses take values; brackets take types.
Go used brackets instead of Java's <T> because of parsing ambiguity (angle brackets collide with comparison operators). Someone always asks, so have it ready. The callout is the whole slide.
syntaxgeneric function

any accepts everything, and so can do nothing

  • any is the constraint that allows every type
  • Fits work that ignores the type, like printing or passing along
  • In exchange, operations like < and + are unavailable
  • If you need operations, you must narrow the allowed types
// any = every type allowed
func printAny[T any](a T) {
    fmt.Println(a)
}

func main() {
    printAny(10)      // T = int
    printAny(3.14)    // T = float64
    printAny("hello") // T = string
}
the rule The looser the constraint, the more types you accept and the less you can do inside.
This trade-off is the whole of constraint design. Get a feel for it here and Part 2 is far easier. Mention in one line that `any` was added in Go 1.18 as an alias for `interface{}`.
solutionunion type

Three mins collapse into one

// list the allowed types with | (union type constraint)
func minType[T int | int16 | int32 | int64 | float32 | float64](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func main() {
    fmt.Println(minType(10, 20))                 // int: 10
    fmt.Println(minType(int16(10), int16(20)))  // int16: 10
    fmt.Println(minType(3.14, 1.14))             // float64: 1.14
}
why not any any does not guarantee the < operation. To compare, you must accept only comparable types.
Pulling slide 09 (the three copies) back up for contrast works well. Union syntax itself gets proper treatment in Part 2 including tilde (~), so here just "you list them with |". Being honest that Go 1.21 added built-in min/max, making this example purely didactic, is worth a line.
syntaxcustom constraint

Name a constraint and it becomes reusable

  • Listing types inline every time is wasteful
  • The interface keyword makes a named constraint
  • Constraints can also be composed
  • Here it defines a type set, not a method set
type IntegerType interface {
    int | int16 | int32 | int64
}

type Float interface {
    float32 | float64
}

// composition works too
type ComparableNumbers interface {
    IntegerType | Float
}
using it func minComparableNumbers[T ComparableNumbers](a, b T) T
"Wasn't interface a set of methods?" always comes up. Answer that Go 1.18 widened the meaning of interface to a type set. Worth adding the caveat that this widened interface cannot be used as a variable type — only in constraint position.
syntaxgeneric struct

Structs can take type parameters too

type Node[T any] struct {
    val  T
    next *Node[T]      // it references itself through T as well
}

func NewNode[T any](v T) *Node[T] {
    return &Node[T]{val: v}
}

// usage
node := NewNode(1)          // *Node[int]
node.Push(2).Push(3).Push(4)

strNode := NewNode("hello") // *Node[string]
strNode.Push("world")
what you gain No need to define IntNode and StringNode separately. Data-structure duplication disappears.
Data structures — linked lists, stacks, queues — are the most natural home for Generics. Point out that a type can reference itself recursively, as in `*Node[T]`. Drawing attention to the missing [int] at the NewNode call site sets up chapter 04 (inference) nicely.
carefulrestriction

Methods cannot declare new type parameters

Allowed — using the struct's T

func (n *Node[T]) Push(v T) *Node[T]

Type parameters declared on the struct are free to use in methods

Not allowed — a new T on the method

func (n *Node[T]) Push[F any](f F)

Compile error. Go does not permit this syntax

the workaround When you need a new type parameter, pull it out into a plain function instead of a method.
The wall people hit most often in practice. Java and C# folks assume it works and get stuck. The reason is that the method set would grow without bound per type, making interface satisfaction undecidable — that much is enough if asked. The Map function on the next slide is exactly the "pulled out into a function" case.
appliedtwo type parameters

Two parameters let you express a transformation

// F: input element type, T: output element type
func Map[F, T any](s []F, f func(F) T) []T {
    rst := make([]T, len(s))
    for i, v := range s {
        rst[i] = f(v)
    }
    return rst
}

doubled := Map([]int{1, 2, 3}, func(i int) int {
    return i * 2
})                                   // [2 4 6]

uppered := Map([]string{"Hello", "world"}, strings.ToUpper)
                                     // [HELLO WORLD]
the point What matters is that F and T are allowed to differ. That is what lets one function also do []int → []string.
Functional utilities were simply impossible before Generics. Note that if F and T are always the same type, there is no reason to have two. Mentioning that golang.org/x/exp/slices and maps were built this way, and that much of it landed in the Go 1.21+ standard library, connects it to real work.
04

Type Inference

So far we have barely written [int] at any call site. The compiler works the type out from the arguments — we look at when it can and when it cannot.

Three to four minutes. This is why Generics syntax does not actually feel heavy in practice. Open by having people recall the call sites from earlier slides.
inferenceexplicit vs inferred

With arguments present, you can skip the type

  • The compiler infers type parameters from the arguments
  • Explicit and inferred forms produce exactly the same result
  • When inference works, the inferred form is preferred
  • Be explicit only when the reader cannot see the type
func identity[T any](v T) T { return v }

// explicit type argument
r1 := identity[int](42)
r2 := identity[string]("hello")

// type inference — taken from the arguments
r3 := identity(42)      // T = int
r4 := identity("hello") // T = string
how it feels Look only at the call site and it is indistinguishable from a plain function. That is why Generics do not weigh the code down.
Anyone who has suffered Java generics' angle-bracket noise appreciates this point. To "so when do I write it explicitly?" there are two answers — when inference fails (slide 21), and when being explicit simply reads better.
inferencemultiple parameters

Several parameters are each inferred separately

  • Each type parameter is decided from its matching argument
  • Mixing different types is no problem
  • If one infers and another does not, you must write them all
func pair[T, U any](a T, b U) string {
    return fmt.Sprintf("(%v, %v)", a, b)
}

// all inferable
pair(1, "hello") // T=int, U=string
pair(3.14, true)  // T=float64, U=bool
careful Type parameters cannot be partially specified. To be explicit, write them all, as in pair[int, string](...).
The no-partial-specification rule is a genuine stumbling block. Go has no syntax for writing the leading parameter and inferring the rest. Briefly revisiting slide 17's Map, where both F and T come from arguments, connects well.
inferencefailure cases

Inference comes only from arguments — no arguments, no inference

func toSlice[T any](args ...T) []T {
    return args
}

// inference succeeds — type comes from the arguments
ints := toSlice(1, 2, 3)          // T = int

// inference fails — no arguments, so it must be explicit
emptyInts := toSlice[int]()
emptyStrings := toSlice[string]()

result := toSlice()   // compile error: cannot infer T
three failing cases No arguments · when the return type alone cannot decide it · when argument types are ambiguous
"It does not infer from the return type" is what surprises people most. The Go compiler does not look at the assignment target to decide T. Asking whether anyone has actually seen `cannot infer T` gets a reaction. The fix is always the same — write it in brackets.
summarybefore vs after

What changed, in one table

AspectBefore GenericsAfter Generics
Supporting many typesinterface{} + assertionsType parameters [T any]
Type safetyRisk of runtime panicVerified at compile time
Code duplicationOne function per typeA single generic function
Data structuresOne struct per typeGeneric struct [T any]
IDE supportlimitedFull type inference
how to choose Do not read the table — just one line: does the compiler know the type or not. Everything else follows.
Do not read all five rows; point only at the highlighted "type safety" row. Saying the other four are consequences of that one compresses the table into a sentence.
checkclick to reveal

Check yourself

The type assertion and the runtime panic that rides along with it. The return type is T, so the type is settled at compile time. → Slide 06 · 08
any does not guarantee the < operation. To compare, narrow it with a union type so only comparable types are accepted. → Slide 12 · 13
No. It can only use type parameters declared on the struct; if you need a new one, pull it out into a plain function. → Slide 16
Inference happens only from arguments, and there are none. You have to be explicit, as in toSlice[int](). → Slide 21
Partial specification is not allowed. To be explicit, write them all, as in pair[int, string](...). → Slide 20
Wait five to ten seconds per question before opening it. Q2 and Q4 are where people actually get stuck in practice, so spend more time there. If short on time, skip Q1, Q3, and Q5 and leave them as takeaways.
closingnext part

Next up: a deep dive into Type Constraints

Today's takeaway

Leave the type slot open with brackets, and let the constraint decide how wide that slot stays.

Call sites stay almost unchanged, thanks to inference

Coming in Part 2

any · comparable · union (|) · tilde (~) · designing custom constraints.

The questions we left open today get answered there

references go.dev/doc/tutorial/generics · go.dev/blog/intro-generics · go.dev/ref/spec#Type_parameter_declarations
Part 1 was "how do you use it"; Part 2 is "how wide do you leave it open." Close by flagging that constraint design is the real difficulty of Generics. Leave about three minutes for questions.
Speaker notes
← Article Go Generics · Part 1
01 / 24

Shortcuts

→ · Space · PgDn
Next slide
← · PgUp
Previous slide
Home · End
First · Last
O
Slide overview
N
Speaker notes
T
Light / dark theme
F
Fullscreen
Esc
Close