Golang

Let’s find out if Golang is also subject to the same type erasure as Java or retains type information like Microsoft .NET. Please consider the following program:

  1. package main
  2. import (
  3. "fmt"
  4. "runtime"
  5. )
  6. type List[T any] []T
  7. func (a *List[T]) add(val T) {
  8. *a = append(*a, val)
  9. }
  10. func printLen[T any](list List[T]) {
  11. fmt.Println(len(list))
  12. }
  13. func main() {
  14. var ints List[int]
  15. ints.add(1)
  16. ints.add(2)
  17. ints.add(3)
  18. var strs List[string]
  19. strs.add("Hello")
  20. strs.add("world")
  21. printLen(ints)
  22. printLen(strs)
  23. runtime.Breakpoint()
  24. }

Just like the Java and .NET examples, the above program defines two variables using a very hacky, generic List:

  • ints: a list of int values
  • strs: a list of string values

What do ints and strs look like at runtime? Follow the instructions below to find out:

  1. Launch the container:

    1. docker run -it --rm go-generics-the-hard-way
  2. Load the above program into the Golang debugger:

    1. dlv debug ./05-internals/golang/main.go
  3. Continue until the predefined breakpoint is hit:

    1. continue
    1. 3
    2. 2
    3. > main.main() ./05-internals/golang/main.go:48 (PC: 0x495c0d)
    4. 43:
    5. 44: printLen(ints)
    6. 45: printLen(strs)
    7. 46:
    8. 47: runtime.Breakpoint()
    9. => 48: }
  4. Now that they are loaded into memory, print information about the ints and strs variables:

    1. locals
    1. ints = main.List[int] len: 3, cap: 4, [...]
    2. strs = main.List[string] len: 2, cap: 2, [...]

    In addition to being defined at compile-time as List[int] and List[string], the variables ints and strs maintain their full type information at runtime as List[int] and List[string].

  5. More than that though, Go does not retain information about the generic “template” at runtime. We can verify this by looking at what types exist using the debugger:

    1. types main\.List
    1. *main.List[go.shape.int_0]
    2. *main.List[go.shape.string_0]
    3. *main.List[int]
    4. *main.List[string]
    5. main.List[go.shape.int_0]
    6. main.List[go.shape.string_0]
    7. main.List[int]
    8. main.List[string]

    Each of those types are generated by the compiler, whereever List[T] was instantiated. The types with go.shape are arguments for generic functions, which we will see in the next example.

  6. Print the functions:

    1. funcs go\.shape
    1. main.(*List[go.shape.int_0]).add
    2. main.(*List[go.shape.string_0]).add
    3. main.printLen[go.shape.int_0]
    4. main.printLen[go.shape.string_0]

    Once again, only the generic types that were instantiated are maintained. And again, the go.shape types are arguments for generic functions.

  7. Type quit to exit the debugger.

  8. Type exit to stop and remove the container.

In other words, generics in Go do retain their type information at runtime, and in fact Go does not know about the generic “template” at runtime — only how it was instantiated.

However, just because .NET and Go retain that type information at runtime, does it mean they know how to use it to prevent incompatible values from being added to lists? What about Java? Find out in the next section!


Next: Runtime type safety