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
- Type inference π
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.
- 752