Go is a much loved and hated language. Dunking on Go for not having generics has been almost like a sporting event in the global developer community. With the launch of Go generics, the Go hating community will run into a bit of an identity crisis. The lack of generics has been something everybody could rally around. Today not only does Go have generics but their generics is also pretty good. In fact, Go generics is in many ways better than generics in Java and C#.
Let us look at why with a simple example often used to demonstrate generics in Go. In the pre-generics age, we might write a Sum
function in Go like this:
func Sum(numbers int) int {
var total int
for _, x := range numbers {
total += x
}
return total
}
This solution is limiting because we can only sum integers. What if we want to sum floating-point numbers or complex numbers instead? In the before-time, we had to duplicate code. Today, we can wave the magic generics wand and write:
type Number interface {
int16 | int32 | int64 | float32| float64
}
func Sum[T Number](numbers []T) T {
var total T
for _, x := range numbers {
total += x
}
return total
}
In the code example, we are stating that any type which is an integer or floating-point number of different bit-length satisfies the Number
interface. We can implement this code more elegantly by importing ready-made interfaces from the golang.org/x/exp/constraints package:
import "golang.org/x/exp/constraints"
type Number interface {
Integer | Float
}
This code example is a pretty simple and obvious use of generics. Regardless of what programming language I use, working with generics for simple data types like integers and floating-point numbers is my bread and butter. Yet, this obviously useful code isn't even possible to write in Java:
// Won't compile
static <T extends Number> T sum(T[] v) {
T total = 0;
for(int k = 0; k < v.length; k++) {
total += v[k];
}
return total;
}
Until Microsoft releases the next version of C#, I believe you cannot do this in C# either. Both Java and C# have made primitive types second class citizens, which makes obviously useful code impossible to implement without ugly hacks killing performance. The Go code example actually translates into hyperefficient machine code:
sum:
MOVD ZR, R0 // start index at zero
MOVD ZR, R3 // zero out total
JMP compare
addup:
MOVW (R1)(R0<<2), R4
ADD $1, R0, R0 // increment index
ADD R4, R3, R3 // add to total
compare:
CMP R0, R2 // are we at end of loop
BGT addup // branch greater than
MOVD R3, R0 // put total in return reg
RET (R30)
The assembly code is ARM64, generated using the cross-compiler functionality of the Go build tools. You can run this line:
❯ env GOOS=linux GOARCH=arm64 go build -gcflags -S \
generic-sum.go 2> generic-sum.S
Type Erasure in Go and Java
In Java, type information is not preserved after compilation. We refer to this fact as type erasure. The follow code is a simple demonstration of the consequences of type erasure in Java. You can see that despite the fact that Array<Integer>
and Array<Float>
should be treated as different types at runtime, Java cannot pick that up. The class is the same.
public class Array<T extends Number> {
// ...
};
Array<Integer> integers = new Array<Integer>();
Array<Float> floats = new Array<Float>();
// evaluates to true
if (integers.getClass() == floats.getClass()) {
System.out.println("they're equal");
}
The Go type system does not have this problem. It correctly identifies Array[int64]
and Array[float64]
as different types at runtime.
Keep reading with a 7-day free trial
Subscribe to Erik Explores to keep reading this post and get 7 days of free access to the full post archives.