In Go 1.18, generics are implemented through code specialization

December 17, 2021

The news of the time interval is that Go 1.18 Beta 1 is available, with generics. Barring something terrible being discovered about either the design or the current implementation of Go generics, this means that they will be part of Go 1.18 when it's released in a few months. Further, because we're already in the beta phase, it's highly unlikely that how generics are implemented will change substantial before Go 1.18.

(The Go developers generally won't want to do a major code change between beta and release, so if there turn out to be significant enough problems with the current implementation, it's more likely that generics will be pulled from Go 1.18 entirely, even at this late stage.)

One of the practical questions around generics in Go has been how they would be implemented. The generics design proposal was careful to not close out any options. To quote from its short section on implementation:

We believe that this design permits different implementation choices. Code may be compiled separately for each set of type arguments, or it may be compiled as though each type argument is handled similarly to an interface type with method calls, or there may be some combination of the two.

The first option listed, generating and compiling code separately for each set of type arguments, is generally called specialization or monomorphization. The second option would likely be a combination of how interfaces are implemented and how the Go runtime implements maps efficiently without generics, depending on what operations you were doing in the generic code.

With the release of Go 1.18 beta, we have a pretty firm answer. At least in the initial implementation in Go 1.18, generics in Go will be implemented with specialization. When you use generics in Go, the Go compiler effectively creates a non-generic version of the code that has the specific types you're using, and compiles that. If you use generic code with two or three or four difference types (or sets of types), you get two or three or four different versions of the code.

Update: This is partially wrong. Generics aren't necessarily specialized in practice, and the implementation design was not based around specialization. The current implementation in Go 1.18 is described in the design proposal Generics implementation - GC Shape Stenciling; see the comments for more detail.

One way to see this for yourself is with the use of the compiler explorer, which has the latest Go development version as one of its compiler options. That lets you write generics examples like this silly example and see what they compile to. This example has a very simple, silly generic function and some uses of it:

func Compit[T comparable](a, b T) bool {
    return a == b

func CompInt(a, b int) bool {
    return Compit(a, b)

func CompFloat(a, b float64) bool {
    return Compit(a, b)

func CompString(a, b string) bool {
    return Compit(a, b)

These compile to three different sets of amd64 assembly code, each using the relevant CPU instructions (and for strings, runtime functions) to compare the different sorts of values. In addition, CompInt and CompFloat actually receive their arguments in different registers, because Go's amd64 register based calling convention puts integer arguments in different registers than floating point ones. This is a common decision in calling conventions; many C calling conventions also behave this way, which can lead to some interesting effects.

In theory, if you use the same generic code with different declared types that have the same underlying type, the Go compiler may not need to generate a new function for each declared type. In other words, suppose that you had:

type Tank int
func CompTank(a, b Tank) bool {
    return Compit(a, b)

In theory CompInt and CompTank could be the same function, not merely two functions with the same machine code. In practice I don't know if the Go compiler will ever do this, and there are a number of situations where it can't do this because the declared type is in some way used by the generic code. For example:

func Report[T any](s ...T) {
   for _, v := range s {
      fmt.Printf("%T %#v\n", v, v)

(Taken from the Go tip playground current example.)

If you invoke Report() on arguments of type Tank instead of just int, it had better be able to report their type correctly (and it does).

Update: In practice, I now believe these sorts of functions can be compiled in Go 1.18 to use common code, depending on what types are involved. See the implementation proposal and the comments for how this works.

PS: An interesting thing happens with inferred typing of generics and constants. Suppose that you have the following two lines:

Report(Tank(10), 20)
Report(30, Tank(40))

You cannot mix two different types in the same invocation of Report(), because it has only a single type for all of its arguments. So these two cases sort of look as invalid as 'Report( int(1), Tank(2) )'. But untyped constants are special in inferred typing; if it makes type inference work, they take on the type of the typed argument instead of their default of, say, int. So in both lines, both arguments are of type Tank.

PPS: You can appear to mix types in arguments to Report() with the trick:

Report[any]("abc", 10)

As we know from the draft release notes, the new predeclared identifier 'any' is an alias for 'interface{}'. Our syntax here is forcing a type T of interface{}, which of course anything can be converted to automatically, and then the real types show through in the end.

Written on 17 December 2021.
« User runtime directories on modern Linux, aka /run/user/<uid>
Our never-used system for user-provided NFS accessible storage »

Page tools: View Source, Add Comment.
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Fri Dec 17 00:19:18 2021
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.