Intro to generics
(At the time of writing) Go does not have support for user-defined generics, but the proposal has been accepted and will be included in version 1.18.
However, there are ways to experiment with the upcoming implementation using the go2go playground today. So to work through this chapter you'll have to leave your precious editor of choice and instead do the work within the playground.
This chapter will give you an introduction to generics, dispel reservations you may have about them and, give you an idea how to simplify some of your code in the future. After reading this you'll know how to write:
    A function that takes generic arguments
    A generic data-structure

Setting up the playground

In the go2go playground we can't run go test. How are we going to write tests to explore generic code?
The playground does let us execute code, and because we're programmers that means we can work around the lack of a test runner by making one of our own.

Our own test helpers (AssertEqual, AssertNotEqual)

To explore generics we'll write some test helpers that'll kill the program and print something useful if a test fails.

Assert on integers

Let's start with something basic and iterate toward our goal
1
package main
2
3
import (
4
"log"
5
)
6
7
func main() {
8
AssertEqual(1, 1)
9
AssertNotEqual(1, 2)
10
11
AssertEqual(50, 100) // this should fail
12
13
AssertNotEqual(2, 2) // so you wont see this print
14
}
15
16
func AssertEqual(got, want int) {
17
if got != want {
18
log.Fatalf("FAILED: got %d, want %d", got, want)
19
} else {
20
log.Printf("PASSED: %d did equal %d\n", got, want)
21
}
22
}
23
24
func AssertNotEqual(got, want int) {
25
if got == want {
26
log.Fatalf("FAILED: got %d, want %d", got, want)
27
} else {
28
log.Printf("PASSED: %d did not equal %d\n", got, want)
29
}
30
}
Copied!
1
2009/11/10 23:00:00 PASSED: 1 did equal 1
2
2009/11/10 23:00:00 PASSED: 1 did not equal 2
3
2009/11/10 23:00:00 FAILED: got 50, want 100
Copied!

Assert on strings

Being able to assert on the equality of integers is great but what if we want to assert on string ?
1
func main() {
2
AssertEqual("CJ", "CJ")
3
}
Copied!
You'll get an error
1
type checking failed for main
2
prog.go2:8:14: cannot use "CJ" (untyped string constant) as int value in argument to AssertEqual
Copied!
If you take your time to read the error, you'll see the compiler is complaining that we're trying to pass a string to a function that expects an integer.

Recap on type-safety

If you've read the previous chapters of this book, or have experience with statically typed languages, this should not surprise you. The Go compiler expects you to write your functions, structs etc by describing what types you wish to work with.
You can't pass a string to a function that expects an integer.
Whilst this can feel like ceremony, it can be extremely helpful. By describing these constraints you,
    Make function implementation simpler. By describing to the compiler what types you work with, you constrain the number of possible valid implementations. You can't "add" a Person and a BankAccount. You can't capitalise an integer. In software, constraints are often extremely helpful.
    Are prevented from accidentally passing data to a function you didn't mean to.
Go currently offers you a way to be more abstract with your types with interfaces, so that you can design functions that do not take concrete types but instead, types that offer the behaviour you need. This gives you some flexibility whilst maintaining type-safety.

A function that takes a string or an integer? (or indeed, other things)

The other option that Go currently gives is declaring the type of your argument as interface{} which means "anything".
Try changing the signatures to use this type instead.
1
func AssertEqual(got, want interface{}) {
2
3
func AssertNotEqual(got, want interface{}) {
Copied!
The tests should now compile and pass. The output will be a bit ropey because we're using the integer %d format string to print our messages, so change them to the general %+v format for a better output of any kind of value.

Tradeoffs made without generics

Our AssertX functions are quite naive but conceptually aren't too different to how other popular libraries offer this functionality
1
func (is *I) Equal(a, b interface{}) {
Copied!
So what's the problem?
By using interface{} the compiler can't help us when writing our code, because we're not telling it anything useful about the types of things passed to the function. Go back to the go2go playground and try comparing two different types,
1
AssertNotEqual(1, "1")
Copied!
In this case, we get away with it; the test compiles, and it fails as we'd hope, although the error message got 1, want 1 is unclear; but do we want to be able to compare strings with integers? What about comparing a Person with an Airport?
Writing functions that take interface{} can be extremely challenging and bug-prone because we've lost our constraints, and we have no information at compile time as to what kinds of data we're dealing with.
Often developers have to use reflection to implement these ahem generic functions, which is usually painful and can hurt the performance of your program.

Our own test helpers with generics

Ideally, we don't want to have to make specific AssertX functions for every type we ever deal with. We'd like to be able to have one AssertEqual function that works with any type but does not let you compare apples and oranges.
Generics offer us a new way to make abstractions (like interfaces) by letting us describe our constraints in ways we cannot currently do.
1
package main
2
3
import (
4
"log"
5
)
6
7
func main() {
8
AssertEqual(1, 1)
9
AssertEqual("1", "1")
10
AssertNotEqual(1, 2)
11
//AssertEqual(1, "1") - uncomment me to see compilation error
12
}
13
14
func AssertEqual[T comparable](got, want T) {
15
if got != want {
16
log.Fatalf("FAILED: got %+v, want %+v", got, want)
17
} else {
18
log.Printf("PASSED: %+v did equal %+v\n", got, want)
19
}
20
}
21
22
func AssertNotEqual[T comparable](got, want T) {
23
if got == want {
24
log.Fatalf("FAILED: got %+v, want %+v", got, want)
25
} else {
26
log.Printf("PASSED: %+v did not equal %+v\n", got, want)
27
}
28
}
Copied!
To write generic functions in Go, you need to provide "type parameters" which is just a fancy way of saying "describe your generic type and give it a label".
In our case the type of our type parameter is comparable and we've given it the label of T. This label then lets us describe the types for the arguments to our function (got, want T).
We're using comparable because we want to describe to the compiler that we wish to use the == and != operators on things of type T in our function, we want to compare! If you try changing the type to any,
1
func AssertNotEqual[T any](got, want T) {
Copied!
You'll get the following error:
1
prog.go2:15:5: cannot compare got != want (operator != not defined for T)
Copied!
Which makes a lot of sense, because you can't use those operators on every (or any) type.

Is any the same as interface{} ?

Consider two functions
1
func GenericFoo[T any](x, y T)
Copied!
1
func InterfaceyFoo(x, y interface{})
Copied!
What's the point of generics here? Doesn't any describe... anything?
In terms of constraints, any does mean "anything" and so does interface{}. The difference with the generic version is you're still describing a specific type and what that means is we've still constrained this function to only work with one type.
What this means is you can call InterfaceyFoo with any combination of types (e.g InterfaceyFoo(apple, orange)). However GenericFoo still offers some constraints because we've said that it only works with one type, T.
Valid:
    GenericFoo(apple1, apple2)
    GenericFoo(orange1, orange2)
    GenericFoo(1, 2)
    GenericFoo("one", "two")
Not valid (fails compilation):
    GenericFoo(apple1, orange1)
    GenericFoo("1", 1)
any is especially useful when making data types where you want it to work with various types, but you don't actually use the type in your own data structure (typically you're just storing it). Things like, Set and LinkedList, are all good candidates for using any.

Next: Generic data types

We're going to create a [stack](https://en.wikipedia.org/wiki/Stack_(abstract_data_type)) data type. Stacks should be fairly straightforward to understand from a requirements point of view. They're a collection of items where you can Push items to the "top" and to get items back again you Pop items from the top (LIFO - last in, first out).
For the sake of brevity I've omitted the TDD process that arrived me at the following code for a stack of ints, and a stack of strings.
1
package main
2
3
import (
4
"log"
5
)
6
7
type StackOfInts struct {
8
values []int
9
}
10
11
func (s *StackOfInts) Push(value int) {
12
s.values = append(s.values, value)
13
}
14
15
func (s *StackOfInts) IsEmpty() bool {
16
return len(s.values) == 0
17
}
18
19
func (s *StackOfInts) Pop() (int, bool) {
20
if s.IsEmpty() {
21
return 0, false
22
}
23
24
index := len(s.values) - 1
25
el := s.values[index]
26
s.values = s.values[:index]
27
return el, true
28
}
29
30
type StackOfStrings struct {
31
values []string
32
}
33
34
func (s *StackOfStrings) Push(value string) {
35
s.values = append(s.values, value)
36
}
37
38
func (s *StackOfStrings) IsEmpty() bool {
39
return len(s.values) == 0
40
}
41
42
func (s *StackOfStrings) Pop() (string, bool) {
43
if s.IsEmpty() {
44
return "", false
45
}
46
47
index := len(s.values) - 1
48
el := s.values[index]
49
s.values = s.values[:index]
50
return el, true
51
}
52
53
func main() {
54
// INT STACK
55
56
myStackOfInts := new(StackOfInts)
57
58
// check stack is empty
59
AssertTrue(myStackOfInts.IsEmpty())
60
61
// add a thing, then check it's not empty
62
myStackOfInts.Push(123)
63
AssertFalse(myStackOfInts.IsEmpty())
64
65
// add another thing, pop it back again
66
myStackOfInts.Push(456)
67
value, _ := myStackOfInts.Pop()
68
AssertEqual(value, 456)
69
value, _ = myStackOfInts.Pop()
70
AssertEqual(value, 123)
71
AssertTrue(myStackOfInts.IsEmpty())
72
73
// STRING STACK
74
75
myStackOfStrings := new(StackOfStrings)
76
77
// check stack is empty
78
AssertTrue(myStackOfStrings.IsEmpty())
79
80
// add a thing, then check it's not empty
81
myStackOfStrings.Push("one two three")
82
AssertFalse(myStackOfStrings.IsEmpty())
83
84
// add another thing, pop it back again
85
myStackOfStrings.Push("four five six")
86
strValue, _ := myStackOfStrings.Pop()
87
AssertEqual(strValue, "four five six")
88
strValue, _ = myStackOfStrings.Pop()
89
AssertEqual(strValue, "one two three")
90
AssertTrue(myStackOfStrings.IsEmpty())
91
}
92
93
func AssertTrue(thing bool) {
94
if thing {
95
log.Printf("PASSED: Expected thing to be true and it was\n")
96
} else {
97
log.Fatalf("FAILED: expected true but got false")
98
}
99
}
100
101
func AssertFalse(thing bool) {
102
if !thing {
103
log.Printf("PASSED: Expected thing to be false and it was\n")
104
} else {
105
log.Fatalf("FAILED: expected false but got true")
106
}
107
}
108
109
func AssertEqual[T comparable](got, want T) {
110
if got != want {
111
log.Fatalf("FAILED: got %+v, want %+v", got, want)
112
} else {
113
log.Printf("PASSED: %+v did equal %+v\n", got, want)
114
}
115
}
116
117
func AssertNotEqual[T comparable](got, want T) {
118
if got == want {
119
log.Fatalf("FAILED: got %+v, want %+v", got, want)
120
} else {
121
log.Printf("PASSED: %+v did not equal %+v\n", got, want)
122
}
123
}
Copied!

Problems

    The code for both StackOfStrings and StackOfInts is almost identical. Whilst duplication isn't always the end of the world, this doesn't feel great and does add an increased maintenance cost.
    As we're duplicating the logic across two types, we've had to duplicate the tests too.
We really want to capture the idea of a stack in one type, and have one set of tests for them. We should be wearing our refactoring hat right now which means we should not be changing the tests because we want to maintain the same behaviour.
Pre-generics, this is what we could do
1
type StackOfInts = Stack
2
type StackOfStrings = Stack
3
4
type Stack struct {
5
values []interface{}
6
}
7
8
func (s *Stack) Push(value interface{}) {
9
s.values = append(s.values, value)
10
}
11
12
func (s *Stack) IsEmpty() bool {
13
return len(s.values) == 0
14
}
15
16
func (s *Stack) Pop() (interface{}, bool) {
17
if s.IsEmpty() {
18
var zero interface{}
19
return zero, false
20
}
21
22
index := len(s.values) - 1
23
el := s.values[index]
24
s.values = s.values[:index]
25
return el, true
26
}
Copied!
    We're aliasing our previous implementations of StackOfInts and StackOfStrings to a new unified type Stack
    We've removed the type safety from the Stack by making it so values is a slice of interface{}
... And our tests still pass. Who needs generics?

The problem with throwing out type safety

The first problem is the same as we saw with our AssertEquals - we've lost type safety. I can now Push apples onto a stack of oranges.
Even if we have the discipline not to do this, the code is still unpleasant to work with because when methods return interface{} they are horrible to work with.
Add the following test,
1
myStackOfInts.Push(1)
2
myStackOfInts.Push(2)
3
firstNum, _ := myStackOfInts.Pop()
4
secondNum, _ := myStackOfInts.Pop()
5
AssertEqual(firstNum+secondNum, 3)
Copied!
You get a compiler error, showing the weakness of losing type-safety:
1
prog.go2:59:14: invalid operation: operator + not defined for firstNum (variable of type interface{})
Copied!
When Pop returns interface{} it means the compiler has no information about what the data is and therefore severely limits what we can do. It can't know that it should be an integer, so it does not let us use the + operator.
To get around this, the caller has to do a type assertion for each value.
1
myStackOfInts.Push(1)
2
myStackOfInts.Push(2)
3
firstNum, _ := myStackOfInts.Pop()
4
secondNum, _ := myStackOfInts.Pop()
5
6
// get our ints from out interface{}
7
reallyFirstNum, ok := firstNum.(int)
8
AssertTrue(ok) // need to check we definitely got an int out of the interface{}
9
10
reallySecondNum, ok := secondNum.(int)
11
AssertTrue(ok) // and again!
12
13
AssertEqual(reallyFirstNum+reallySecondNum, 3)
Copied!
The unpleasantness radiating from this test would be repeated for every potential user of our Stack implementation, yuck.

Generic data structures to the rescue

Just like you can define generic arguments to functions, you can define generic data structures.
Here's our new Stack implementation, featuring a generic data type and the tests, showing them working how we'd like them to work, with full type-safety. (Full code listing here)
1
package main
2
3
import (
4
"log"
5
)
6
7
type Stack[T any] struct {
8
values []T
9
}
10
11
func (s *Stack[T]) Push(value T) {
12
s.values = append(s.values, value)
13
}
14
15
func (s *Stack[T]) IsEmpty() bool {
16
return len(s.values)==0
17
}
18
19
func (s *Stack[T]) Pop() (T, bool) {
20
if s.IsEmpty() {
21
var zero T
22
return zero, false
23
}
24
25
index := len(s.values) -1
26
el := s.values[index]
27
s.values = s.values[:index]
28
return el, true
29
}
30
31
func main() {
32
myStackOfInts := new(Stack[int])
33
34
// check stack is empty
35
AssertTrue(myStackOfInts.IsEmpty())
36
37
// add a thing, then check it's not empty
38
myStackOfInts.Push(123)
39
AssertFalse(myStackOfInts.IsEmpty())
40
41
// add another thing, pop it back again
42
myStackOfInts.Push(456)
43
value, _ := myStackOfInts.Pop()
44
AssertEqual(value, 456)
45
value, _ = myStackOfInts.Pop()
46
AssertEqual(value, 123)
47
AssertTrue(myStackOfInts.IsEmpty())
48
49
// can get the numbers we put in as numbers, not untyped interface{}
50
myStackOfInts.Push(1)
51
myStackOfInts.Push(2)
52
firstNum, _ := myStackOfInts.Pop()
53
secondNum, _ := myStackOfInts.Pop()
54
AssertEqual(firstNum+secondNum, 3)
55
}
Copied!
You'll notice the syntax for defining generic data structures is consistent with defining generic arguments to functions.
1
type Stack[T any] struct {
2
values []T
3
}
Copied!
It's almost the same as before, it's just that what we're saying is the type of the stack constrains what type of values you can work with.
Once you create a Stack[Orange] or a Stack[Apple] the methods defined on our stack will only let you pass in and will only return the particular type of the stack you're working with:
1
func (s *Stack[T]) Pop() (T, bool) {
Copied!
You can imagine the types of implementation being somehow generated for you, depending on what type of stack you create:
1
func (s *Stack[Orange]) Pop() (Orange, bool) {
Copied!
1
func (s *Stack[Apple]) Pop() (Apple, bool) {
Copied!
Now that we have done this refactoring, we can safely remove the string stack test because we don't need to prove the same logic over and over.
Using a generic data type we have:
    Reduced duplication of important logic.
    Made Pop return T so that if we create a Stack[int] we in practice get back int from Pop; we can now use + without the need for type assertion gymnastics.
    Prevented misuse at compile time. You cannot Push oranges to an apple stack.

Wrapping up

This chapter should have given you a taste of generics syntax, and some ideas as to why generics might be helpful. We've written our own Assert functions which we can safely re-use to experiment with other ideas around generics, and we've implemented a simple data structure to store any type of data we wish, in a type-safe manner.

Generics are simpler than using interface{} in most cases

If you're inexperienced with statically-typed languages, the point of generics may not be immediately obvious, but I hope the examples in this chapter have illustrated where the Go language isn't as expressive as we'd like. In particular using interface{} makes your code:
    Less safe (mix apples and oranges), requires more error handling
    Less expressive, interface{} tells you nothing about the data
    More likely to rely on reflection, type-assertions etc which makes your code more difficult to work with and more error prone as it pushes checks from compile-time to runtime
Using statically typed languages is an act of describing constraints. If you do it well, you create code that is not only safe and simple to use but also simpler to write because the possible solution space is smaller.
Generics gives us a new way to express constraints in our code, which as demonstrated will allow us to consolidate and simplify code that is not possible to do today.

Will generics turn Go into Java?

    No.
There's a lot of FUD (fear, uncertainty and doubt) in the Go community about generics leading to nightmare abstractions and baffling code bases. This is usually caveatted with "they must be used carefully".
Whilst this is true, it's not especially useful advice because this is true of any language feature.
Not many people complain about our ability to define interfaces which, like generics is a way of describing constraints within our code. When you describe an interface you are making a design choice that could be poor, generics are not unique in their ability to make confusing, annoying to use code.

You're already using generics

When you consider that if you've used arrays, slices or maps; you've already been a consumer of generic code.
1
var myApples []Apples
2
// You cant do this!
3
append(myApples, Orange{})
Copied!

Abstraction is not a dirty word

It's easy to dunk on AbstractSingletonProxyFactoryBean but let's not pretend a code base with no abstraction at all isn't also bad. It's your job to gather related concepts when appropriate, so your system is easier to understand and change; rather than being a collection of disparate functions and types with a lack of clarity.
People run in to problems with generics when they're abstracting too quickly without enough information to make good design decisions.
The TDD cycle of red, green, refactor means that you have more guidance as to what code you actually need to deliver your behaviour, rather than imagining abstractions up front; but you still need to be careful.
There's no hard and fast rules here but resist making things generic until you can see that you have a useful generalisation. When we created the various Stack implementations we importantly started with concrete behaviour like StackOfStrings and StackOfInts backed by tests. From our real code we could start to see real patterns, and backed by our tests, we could explore refactoring toward a more general-purpose solution.
People often advise you to only generalise when you see the same code three times, which seems like a good starting rule of thumb.
A common path I've taken in other programming languages has been:
    One TDD cycle to drive some behaviour
    Another TDD cycle to exercise some other related scenarios
Hmm, these things look similar - but a little duplication is better than coupling to a bad abstraction
    Sleep on it
    Another TDD cycle
OK, I'd like to try to see if I can generalise this thing. Thank goodness I am so smart and good-looking because I use TDD, so I can refactor whenever I wish, and the process has helped me understand what behaviour I actually need before designing too much.
    This abstraction feels nice! The tests are still passing, and the code is simpler
    I can now delete a number of tests, I've captured the essence of the behaviour and removed unnecessary detail
Last modified 7mo ago