Write a function without fixing its types up front
— what Go 1.18 changed
When the type gets decided — that single thing is what changes. And why Go held this feature back for twelve years.
// 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
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
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
interface{}, which defers type checking to runtime,
and copy-pasted functions that multiply with every type. These two were the real case for adoption.
// 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 }
.(int), disappeared. But what disappeared is not syntax — it is the possibility of a runtime panic.
interface{}value.(Type)ok makes it safevar 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
A wrong assertion surfaces as a runtime panic, not a compile error.
If tests miss it, you meet it after deploy
You repeat .(Type) every time you use the value.
Call sites get noisy
The return type is interface{}, so completion and type checking are blocked.
The editor cannot help you
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 }
From declaring type parameters to generic functions, constraints, and generic structs.
We follow how the min duplication we just saw collapses into one.
any is the loosestany is the constraint that allows every type< and + are unavailable// 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 }
// 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 }
any does not guarantee the < operation. To compare, you must accept only comparable types.
interface keyword makes a named constrainttype IntegerType interface { int | int16 | int32 | int64 } type Float interface { float32 | float64 } // composition works too type ComparableNumbers interface { IntegerType | Float }
func minComparableNumbers[T ComparableNumbers](a, b T) T
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")
IntNode and StringNode separately. Data-structure duplication disappears.
func (n *Node[T]) Push(v T) *Node[T]
Type parameters declared on the struct are free to use in methods
func (n *Node[T]) Push[F any](f F)
Compile error. Go does not permit this syntax
// 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]
F and T are allowed to differ. That is what lets one function also do []int → []string.
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.
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
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
pair[int, string](...).
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
| Aspect | Before Generics | After Generics |
|---|---|---|
| Supporting many types | interface{} + assertions | Type parameters [T any] |
| Type safety | Risk of runtime panic | Verified at compile time |
| Code duplication | One function per type | A single generic function |
| Data structures | One struct per type | Generic struct [T any] |
| IDE support | limited | Full type inference |
T, so the type is settled at compile time. → Slide 06 · 08any does not guarantee the < operation. To compare, narrow it with a union type so only comparable types are accepted. → Slide 12 · 13toSlice[int](). → Slide 21pair[int, string](...). → Slide 20Leave the type slot open with brackets, and let the constraint decide how wide that slot stays.
Call sites stay almost unchanged, thanks to inference
any · comparable · union (|) · tilde (~) · designing custom constraints.
The questions we left open today get answered there