Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Loosening the Reins: Go Generics Get More Flexible

Avatar for kuro kuro
August 06, 2026

Loosening the Reins: Go Generics Get More Flexible

Slides from my talk at GopherCon2026 (Seattle Aug 5, 2026):
https://www.gophercon.com/agenda/session/1880570

Avatar for kuro

kuro

August 06, 2026

More Decks by kuro

Other Decks in Programming

Transcript

  1. Loosening the Reins: Go Generics Get More Flexible GopherCon 2026

    · Aug 5, 2026 Naoki Kuroda The Go gopher was designed by Renée French. 1
  2. type Ordered[T Ordered[T]] interface { Less(T) bool } Go 1.25:

    invalid recursive type Go 1.26: accepted The Go gopher was designed by Renée French. 2
  3. A sorted tree compares each T with another T type

    Tree[T Ordered[T]] struct { root *node[T] } func (t *Tree[T]) Insert(v T) { if v.Less(t.root.value) { /* go left */ } else { /* go right */ } } Each element must be able to compare itself with another value of the same type. 3
  4. Go 1.26 removed an explicit specification restriction From December 2022

    to October 2025, the spec said exactly this: go.googlesource.com/go — Go 1.23.2 specification Go 1.26 removed this rule. 5
  5. The self-type requirement can now live in the constraint Before

    Go 1.26 type Lesser[T any] interface { Less(T) bool } type Tree[T Lesser[T]] struct { ... } Go 1.26 type Ordered[T Ordered[T]] interface { Less(T) bool } type Tree[T Ordered[T]] struct { ... } 6
  6. The path we'll follow 1. Why Go rejected self-referential constraints

    2. How Go 1.26 learned to accept safe cycles 3. How other releases made generics rules more precise 7
  7. Early cycle handling could recurse forever Issue #46461, May 2021

    (before Go 1.18 shipped). When a constraint referred to its own type, the type checker recursed indefinitely and failed. 9
  8. A deadlock led to the original restriction github.com/golang/go/issues/49439 — aarzilli,

    Nov 8, 2021 The deadlock occurred during type inference for type arguments to generic types. CL 361922 fixed the deadlock by restricting self-reference. 10
  9. The triggering inference feature was removed before Go 1.18 Type

    inference for type arguments to generic types was cut from the design before release. The restriction remained until Go 1.26. go-review.googlesource.com/c/go/+/711420 — commit message, Robert Griesemer, 2025 12
  10. By 2024, rejection needed a new justification 2021 · Issue

    #46461 2024 · Issue #68162 2021 2024 The Go gopher was designed by Renée French. 13
  11. The checker could encounter the same cycle differently Accepted type

    Foo[T Bar] interface{} Rejected type Bar interface { type Bar interface { foo() Foo[Bar] } foo() Foo[Bar] } type Foo[T Bar] interface{} Same dependency graph. Different result through Go 1.25. 14
  12. The checker could already represent the self-reference From go/types/decl.go :

    // Set the type parameters before collecting the type constraints because // the parameterized type may be used by the constraints (go.dev/issue/47887). // Example: type T[P T[P]] interface{} The checker could already represent this dependency. 16
  13. How go/types recognizes a cycle go/types tracks each object's checking

    state: // objDecl() in go/types/decl.go // - not in Checker.objPathIdx and type == nil // in Checker.objPathIdx // - not in Checker.objPathIdx and type != nil → → → white (not yet checked) grey (pending) black (done) 17
  14. A reference to an in-progress Ordered reveals the cycle go/types

    tracks each object as white, grey, or black. Encountering a grey object means a cycle. Checking Ordered[T Ordered[T]] : white grey Ordered[T] → same grey object turns grey cycle detected is called Before Go 1.26, validCycle rejected this cycle because it occurred in a type-parameter list. 18
  15. The checker already classified this exact cycle // validCycle() in

    go/types/decl.go tparCycle := false // ...loop over the objects in the cycle... case *TypeName: if check.inTParamList && isGeneric(obj.typ) { tparCycle = true break loop } 19
  16. Go 1.26 accepts cycles classified as tparCycle // validCycle() in

    go/types/decl.go (CL 711420) + // Cycles through type parameter lists are ok. + if tparCycle { + return true + } check.cycleError(cycle, firstInSrc(cycle)) return false 20
  17. After acceptance, checking Ordered can finish Same Ordered[T Ordered[T]] ,

    after CL 711420: white grey Ordered[T] → same grey object black turns grey accepted → checking finishes (done) tparCycle accepts the cycle, so Ordered reaches the black (completed) state. 21
  18. The sorted tree compiles type Ordered[T Ordered[T]] interface { Less(T)

    bool } type Tree[T Ordered[T]] struct { root *node[T] } func (t *Tree[T]) Insert(v T) { if v.Less(t.root.value) { /* go left */ } else { /* go right */ } } t := Tree[netip.Addr]{} 22
  19. Here, the self-reference cannot be split out type ElementList[E Element[E]]

    []E type Element[E Element[E]] interface { Less(E) bool Children() ElementList[E] // E must be Element } github.com/golang/go/issues/68162 — ecryth, Jun 25, 2024 Tree can put the requirement on its own type parameter. Element cannot, because Children() returns ElementList[E] . 23
  20. Ask what problem each rule addresses go.dev/blog/coretypes — "Goodbye core

    types", March 2025 What problem does each rule address? Is the rule scoped to that problem? 25
  21. Go 1.20: Ordinary and generic maps treated any differently Ordinary

    map: var ordinary map[any]string // accepted Generic map, Go 1.18–1.19: type Map[K comparable, V any] map[K]V var generic Map[any, string] // rejected The key type is identical, but only the generic declaration was rejected. 27
  22. Equality on any can panic var x any = 1

    _ = x == x // true x = []int{} _ = x == x // panic Values of type any can be compared, but the comparison may panic. 28
  23. any does not implement comparable any's type set comparable contains

    only comparable strictly comparable types slice int string any also contains slices, map bool struct{ n int } maps, and functions func netip.Addr … []int is in any 's type set but not comparable 's, so any does not implement comparable . 29
  24. Go 1.20 separated satisfaction from implementation Go 1.18–1.19 Go 1.20

    satisfies comparable implements comparable = satisfies comparable implements comparable + any (example) The type set of comparable stayed the same A new exception applies to C = interface{ comparable; E } It requires E to be basic and T (the candidate type argument) to be comparable and implement E any satisfies comparable , but does not implement it. 30
  25. any satisfies comparable ; an unconstrained P does not Does

    a type parameter constrained only by any receive the same treatment? Interface type any type Map[K comparable, V any] map[K]V var _ Map[any, string] // allowed K comparable is interface{ comparable; any } . Type parameter P any func g[P any]() { var _ Map[P, string] // compile error } P may still be instantiated as []int . any satisfies comparable . → The exception does not make an unconstrained type parameter comparable. 31
  26. Generic maps now accept the same any key type type

    Map[K comparable, V any] map[K]V ordinary := map[any]string{} generic := Map[any, string]{} ordinary[[]int{}] = "slice" // panics at run time generic[[]int{}] = "slice" // panics at run time Go 1.20 accepts the same runtime risk that already exists for an ordinary map. 32
  27. What changed in Go 1.20 Comparable type: permits == ,

    which may panic qualified → any has always Strictly comparable: guarantees that == will not panic qualify → any still does not → any still does not Satisfies comparable : may be used as a type argument → any qualifies Implements comparable : fits inside its type set since Go 1.20 → Only the rule for satisfying the constraint changed. 33
  28. Go 1.25: Core types tied operation rules together Go 1.18

    introduced core types (a spec concept, not Go syntax). Here's how it defined close() , in Go 1.18 through 1.24: For an argument ch with core type that is a channel... The specification reused this concept for range , close , send, and copy . 35
  29. Go 1.25 kept the boundary but removed the shared gate

    type Rangeable interface { string | []byte } func f[T Rangeable](v T) { for _, b := range v { _ = b } } Go 1.18 – 1.24 // error: cannot range over v (variable of type T // constrained by Rangeable): no core type Go 1.25 / Go 1.26 (current) // error: cannot range over v (variable of type T // constrained by Rangeable): string and []byte // have different underlying types 36
  30. Go 1.25 gave each operation its own rule Go 1.24

    Go 1.25 core type close send range copy … close send range copy … The range example still fails, but each operation now owns its own rule. 37
  31. Go 1.27 preview: methods could not declare their own type

    parameters type Container[T any] struct { items []T } func (c *Container[T]) Map[U any](f func(T) U) *Container[U] { // ... } // syntax error: method must have no type parameters Through Go 1.26, methods could not declare their own type parameters. 39
  32. The 2021 design tied concrete generic methods to interface dispatch

    Generic concrete method: the receiver type is known statically Generic interface method: the dynamic concrete receiver type is known only at run time, and the required method instantiation may not be known at compile time The 2021 design considered the two capabilities together. The Go gopher was designed by Renée French. 40
  33. The 2021 design rejected all three options Instantiate at link

    time: walk the whole program's call graph impractical Instantiate at run time: use JIT compilation or reflection → → impractical Exclude interface implementation: generic methods never implement interface methods → insufficient benefit over functions Link time Run time No interfaces The Go gopher was designed by Renée French. 41
  34. The 2026 proposal treats concrete methods separately Interface dispatch remains

    unsolved, but the 2026 proposal treats concrete methods as independently useful. github.com/golang/go/issues/77273 — Robert Griesemer, 2026 APIs grouped under the receiver type Discoverability in documentation and tooling Fluent method-call syntax 42
  35. Go 1.27 preview allows type parameters on concrete methods type

    Container[T any] struct { items []T } func (c *Container[T]) Map[U any](f func(T) U) *Container[U] { // ... } var c Container[int] _ = c.Map(strconv.Itoa) // T = int from the receiver; U = string from the argument 43
  36. Map preserves fluent method chaining type Builder[T any] struct{ items

    []T } func (b *Builder[T]) Add(v T) *Builder[T] func (b *Builder[T]) Map[U any](func(T) U) *Builder[U] func (b *Builder[T]) Build() []T result := (&Builder[int]{}).Add(1).Add(2).Add(3). Map(strconv.Itoa).Build() // ["1", "2", "3"] 44
  37. Go 1.27 preview leaves interfaces unchanged Interface method type parameters

    type I interface { M[T any](T) T } // invalid Interface implementation type I interface { M(int) } type S struct{} func (S) M[T any](T) {} var _ I = S{} // invalid Interface methods cannot declare type parameters. Generic methods cannot implement interface methods. 45
  38. Four releases, more precisely scoped rules 1.20: separated constraint satisfaction

    from implementation 1.25: separated the rules for individual operations 1.26: separated safe type-parameter cycles from invalid recursion 1.27 preview: separated concrete generic methods from interface dispatch 46
  39. A restriction should be no broader than the problem it

    prevents. The Go gopher was designed by Renée French. 47
  40. References Go 1.20 Go 1.25 Go 1.26 Go 1.27 preview

    Current Go specification Goodbye core types Go 1.26 release notes All your comparable types Go 1.25 release notes Go 1.26 overview Go 1.27 draft release notes Issue #70128 Go 1.23.2 specification Type parameters design Issues #50646 · #56548 Generic interfaces go/types cycle detection Go FAQ Issues #49085 · #77273 Issues #40882 · #46461 · #47887 · #49439 Issues #65714 · #68162 · #75883 CLs 711420 · 711422 48