Playing with Go and Generics

Diego Garcia
3 min readApr 12, 2021

--

Originally published on https://dgarcia.dev/playing-with-go-and-generics/

Yes, it’s happening! The proposal to add generics to Golang was accepted. It means that in some versions in the future, we’ll be able to code generic solutions without having to use complex workarounds with the empty interface{}. But, how it works?

TLDR: Now there’s a type parameter described with square brackets to define generics types for structs, functions, etc. For instance:

func Print[T any](s []T) {
for _, v := range s {
fmt.Print(v)
}
}

The identifier any means that the type T can be any type

With the previous notation we can use a slice of any type in an explicit way:

func main() {
Print([]string{"Hello, ", "World\n"})
Print([]int{4, 2})
}

And will work like a charm:

Hello, World
42

Let’s take a look at some classics data structures implemented in go with generics.

Queue

We can start with a simple queue implementation:

type Queue[T any] []T

func (q *Queue[T]) enqueue(v T) {
*q = append(*q, v)
}

func (q *Queue[T]) dequeue() (T, bool) {
if len(*q) == 0 {
var v T
return v, false
}
v := (*q)[0]
*q = (*q)[1:]
return v, true
}

When we testing the generic queue with string type:

func main() {
q := new(Queue[string])
q.enqueue("item-1")
q.enqueue("item-2")
q.enqueue("item-3")
fmt.Println(q)
fmt.Println(q.dequeue())
fmt.Println(q.dequeue())
fmt.Println(q)
}

We got the following output:

&[reg-1 reg-2 reg-3]
reg-1 true
reg-2 true
&[reg-3]

Stack

If we try a Stack?:

type Stack[T any] []T

func (s *Stack[T]) push(v T) {
*s = append([]T{v}, (*s)...)
}

func (s *Stack[T]) pop() (T, bool) {
if len(*s) == 0 {
var v T
return v, false
}
v := (*s)[0]
*s = (*s)[1:]
return v, true
}

What about test using int?:

func main() {
s := new(Stack[int])
s.push(1)
s.push(2)
s.push(3)
fmt.Println(s)
fmt.Println(s.pop())
fmt.Println(s.pop())
fmt.Println(s)
}

No big deal:

&[3 2 1]
3 true
2 true
&[1]

Linked List

Maybe a Linked List:

type Node[T any] struct {
data T
next *Node[T]
}

type LinkedList[T any] struct {
head *Node[T]
}

func (l *LinkedList[T]) add(data T) {
node := &Node[T]{data: data}
if l.head == nil {
l.head = node
return
}
current := l.head
for current.next != nil {
current = current.next
}
current.next = node
}

func (l *LinkedList[T]) print() {
current := l.head
for current != nil {
fmt.Println(current.data)
current = current.next
}
}

A bit more of code I agree but there’s no secrets:

func main() {
ll := new(LinkedList[float32])
ll.add(1.01)
ll.add(2.02)
ll.add(3.03)
ll.print()
}

Everything works as expected:

1.01
2.02
3.03

It looks a little strange on the first try, but I can get used to it.

A more restrictive example

We can define a more restrictive set of types that a generic type can use.

type Number interface {
type int, float32
}

func sum[T Number](a, b T) T {
return a + b
}

For int and float32 that function works fine:

func main() {
fmt.Println(sum(10, 30))
fmt.Println(sum[float32](1.5, 4.2))
}

But if we try with strings for example, the code won’t compile:

string does not satisfy Number (string or string not found in int, float32)

Try It Yourself!

You can play with generics in go by yourself by build from the source or in The Go2Go Playground.

--

--