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

Generics in Go

By Rainer Stropek

Generics in Go

Go is one of the most important languages when it comes to DevOps. The language is known for its simplicity. However, there is one feature that Go developers painfully miss: Generics. This is about to change, Generics are coming. In this talk, Rainer Stropek speaks about how Generics will work in Go. He speaks about the latest previews and designs and will show a series of demos with Go Generics.

  • 576