Generics in Go

Rainer Stropek | @rstropek

Introduction

Rainer Stropek

  • Passionate developer since 25+ years
    Β 
  • Microsoft MVP, Regional Director
    Β 
  • Trainer, Teacher, Mentor
    Β 
  • πŸ’• community

Current Situation

{

}

Current Situation

  • Go should be simple πŸ‘† no generics
  • But what about generic data structures?
    • E.g. lists, graphs
  • Options
    • Generate code (e.g. go generate, genny)
    • Use empty interface interface{} (e.g. ElementΒ in Go's double-linked list)
package main

import (
	"container/list"
	"fmt"
)

type person struct {
	name string
	age  int
}

func main() {
	values := list.New()

	var value interface{}
	value = "FooBar"
	values.PushBack(value)
	value = 42
	values.PushBack(value)
	value = person{name: "FooBar", age: 42}
	values.PushBack(value)

	for v := values.Front(); v != nil; v = v.Next() {
		fmt.Println(v.Value)
	}
}

Problem With interface{}?

package main

import "fmt"

type hero struct {
	name   string
	canFly bool
}

// Add the required method for the fmt.Stringer interface.
func (h hero) String() string {
	return h.name
}

func main() {
	var something interface{} = hero{name: "Homelander", canFly: true}
	h, ok := something.(hero) // Use type assertation to check if something is a hero
	if ok {
		fmt.Println(h.name)
	}

	// Use type assertation to check if something fulfills the
	// requirements of the fmt.Stringer interface.
	var hStringer fmt.Stringer = something.(fmt.Stringer)
	fmt.Println(hStringer.String())
}

We frequently need runtime type assertations

πŸ‘† Generics are a useful extension to Go

  • More type-safe code, fewer runtime errors
  • Fewer runtime type assertations
  • Happier 😊 developers

Where are we?

Type Parameters

[

]

Type Parameters in Functions

package main

import "fmt"

// Print prints all elements of the given slice to stdout
func Print[T any](s []T) {
	for _, v := range s {
		fmt.Println(v)
	}
}

func main() {
	// Call print with explicit type parameter
	Print[int]([]int{1, 2, 3})
	Print[string]([]string{"Foo", "Bar"})

	// Let go figure out the type parameter using type inference
	Print([]int{1, 2, 3})
	Print([]string{"Foo", "Bar"})
}

What is any?

  • another way of interface{}
  • Only usable with type params

Multiple Type Parameters

package main

import "fmt"

// Print prints all elements of the given slice to stdout
func Print[T any](s []T) {
	for _, v := range s {
		fmt.Println(v)
	}
}

func Map[T1, T2 any](items []T1, mapFunc func(T1) T2) []T2 {
	result := make([]T2, len(items))
	for index, item := range items {
		result[index] = mapFunc(item)
	}
	return result
}

func main() {
	Print(Map([]int{1, 2, 3},
		func(item int) bool { return item%2 == 0 }))
}

Type inference limitations

Type Parameters and Channels

package main

import "fmt"

func concat[T any](c1, c2 <-chan T) <-chan T {
	r := make(chan T)
	go func(c1, c2 <-chan T, r chan<- T) {
		defer close(r)
		for v := range c1 {
			r <- v
		}

		for v := range c2 {
			r <- v
		}

	}(c1, c2, r)
	return r
}
func main() {
	c1 := make(chan string, 2)
	c2 := make(chan string, 2)

	go func() {
		c1 <- "Hello"
		c1 <- ", "
		close(c1)

		c2 <- "World"
		c2 <- "!"
		close(c2)
	}()

	c3 := concat(c1, c2)
	for elem := range c3 {
		fmt.Print(elem)	// Will result
				// in "Hello, World!"
	}
}

Generic Function Types

// Generic predicate
// A function that takes any type T and returns a bool
type predicate[T any] func(item T) bool;

// Generic iterator function type
// A function that returns a reference to a type T
type iteratorFunc[T any] func() *T;

Type Constraints

~

~

Type Constraints

package main

import "fmt"

type hero struct {
	name   string
	canFly bool
}

// Add the required method for the fmt.Stringer interface.
func (h hero) String() string {
	return h.name
}

type joiner interface {
	join(string, fmt.Stringer) string
}

type commaJoiner struct{}

func (commaJoiner) join(agg string, s fmt.Stringer) string {
	// This function is for demonstrating generics. It is a
	// naive implementation of string concatination. In practice,
	// prefer e.g. strings.Builder to minimize memory copying.

	if len(agg) > 0 {
		return agg + ", " + s.String()
	}

	return s.String()
}

func join[S fmt.Stringer](items []S, j joiner) string {
	var result string
	for _, item := range items {
		// We can pass `item` to `join()`
		// as the second argument because S
		// has a `fmt.Stringer` constraint.
		result = j.join(result, item)
	}
	return result
}

func main() {
	heroes := []hero{
		{name: "Homelander", canFly: true},
		{name: "Starlight", canFly: false},
	}
	fmt.Print(join(heroes, commaJoiner{}))
}

Constraining types
using interfaces

Type Sets

package main

import "fmt"

// The adder interface contains datatypes that define the + operator.
type adder interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | 
	~uint32 | ~uint64 | ~float32 | ~float64 | ~string
}

func aggregate[T adder](items []T, seed T) T {
	result := seed
	for _, item := range items {
		// Generic method can use += because of the type list in the adder constraint.
		result += item
	}
	return result
}

// Index fulfills the adder constraint because it's underlying type uint is part of adder's type list.
type index uint

func main() {
	fmt.Println(aggregate([]int{1, 2, 3, 4}, 32))  // results in 42
	fmt.Println(aggregate([]string{"4", "2"}, "")) // results in "42"
	fmt.Println(aggregate([]index{21, 21}, 0))     // results in 42

	// The next line fails because hero cannot satisfy adder
	// fmt.Println(aggregate([]hero{{name: "Homelander", canFly: true},},
	//                       hero{name: "Starlight", canFly: false}))
}

~ means: predeclared type
Β  Β or underlying type

Type Sets

  • Interfaces can have type sets and methods
  • Two new predeclared names
    • comparable (permits use of == and !==
    • any
  • Future: constraints package with additional standard constraints

Generic Types

[

]

Generic Types

package main

import "fmt"

type vectorElement interface {
	~int | ~uint | ~float32 | ~float64
}

type vector2d[T vectorElement] struct {
	X T
	Y T
}

func newVector2d[T vectorElement](x T, y T) vector2d[T] {
	return vector2d[T]{X: x, Y: y}
}

func (v1 vector2d[T]) add(v2 vector2d[T]) vector2d[T] {
	return vector2d[T]{X: v1.X + v2.X, Y: v1.Y + v2.Y}
}

func main() {
	v1 := newVector2d(10, 10)
	v2 := newVector2d(11, 11)
	v3 := v1.add(v2)
	fmt.Println(v3)
}

Type set

Generic type

Generic function

No generic interfaces

Some Fun With
Generic Functions

Generator, Iterator

package main
import "fmt"

// Generic iterator function type
type iteratorFunc[T any] func() *T

// Generic function for iteration
func next[T any](iterator iteratorFunc[T]) *T { return iterator() }

// Generic function executing a given function for each item in iterator
func forEach[T any](iterator iteratorFunc[T], body func(T)) {
	for ptr := next(iterator); ptr != nil; ptr = next(iterator) {
		body(*ptr)
	}
}

func numbersIterator(max int) iteratorFunc[int] {
	current := 0
	return func() *int {
		if current >= max {
			return nil
		}

		result := current
		current++
		return &result
	}
}

func main() {
	// Print numbers between 0 and 10 (excl.)
	forEach(numbersIterator(10), func(n int) { fmt.Println(n) })
}

Filtering

...

// Generic predicate
type predicate[T any] func(item T) bool

// Generic function filtering based on a given predicate
func filter[T any](iterator iteratorFunc[T], p predicate[T]) iteratorFunc[T] {
	return func() *T {
		var item *T
		for item = next(iterator); item != nil && !p(*item); item = next(iterator) {
		}
		return item
	}
}

...

func main() {
	// Print even numbers between 0 and 10 (excl.)
	forEach(
		filter(
			numbersIterator(10),
			func(n int) bool { return n%2 == 0 }),
		func(n int) { fmt.Println(n) })
}

Filtering

...

// Generic function that generates an iterator from a given slice
func iteratorFromSlice[T any](items []T) iteratorFunc[T] {
	return func() *T {
		if len(items) < 1 {
			return nil
		}

		firstItem := &items[0]
		items = items[1:]
		return firstItem
	}
}

type user struct {
	name string
	age  int
}

...

func main() {
	users := []user{user{name: "Foo", age: 42}, user{name: "Bar", age: 43}, user{name: "FooBar", age: 44},}

	// Print each user's name where the user name starts with Foo.
	forEach(filter(iteratorFromSlice(users), func(u user) bool { return strings.HasPrefix(u.name, "Foo") }),
		func(u user) { fmt.Printf("User is %s\n", u.name) })
}

Q&A

Rainer Stropek | @rstropek