Playing with Go and Generics
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.