Alexander Nnakwue Software Engineer. React, Node.js, Python, and other developer tools and libraries.

The past, present, and future of Go generics

9 min read 2758

The Past, Present, And Future Of Go Generics

Introduction

According to Wikipedia, generic programming, also known as parametric polymorphism in other programming languages, is a way of reducing function duplication by writing “common functions” with support for multiple type parameters/arguments. In this case, the type parameters can be factored out or, better still, isolated, and the functions can still run irrespective of the type arguments we pass.

As a feature of the Go programming language, generics were not included in the first release version (Go1.x), nor in the design of the language. However, after much debate via draft proposals and design documents among the Go community — and hard work from the core team and contributors — support for generics is gradually becoming a reality.

The authors of the language had to weigh the pros and cons of this feature in a programming language like Go, whose target was for networked system software and, thus, initially opted out for features like concurrency, scalable builds, and so on.

Today, every function we write in Go must apply only to a particular type. However, with generics, we might be able to write a min() function that works for both integer and floating-point types without having to explicitly write them based on the types.

Generic data structure and algorithms would provide a lot of advantages, which include flexibility and code reusability; they would come in handy when, e.g., writing algorithms that can then be applied on different type arguments. In essence, this would allow developers to use previously debugged, optimized, and efficient packages or libraries, thus allowing for greater scalability and easier handling of a growing codebase.

With that in mind, in this post, we are going to cover:

  • An overview of generic programming — here we will briefly summarize the history and motivation of Go generics
  • What adding generics would mean for the language – here we will talk about why generics would be a useful feature for Go
  • Why Go generics are important and why we need it
  • What other options were available before generics, and why support for generics was not added to the language earlier

Before we begin, readers must understand the basics of the Go programming language. Specifically, readers should have an understanding of the Go type system, including type assertions and type inference, as well as interfaces and their usage in Go programs.

Overview of Go generics

From draft proposal to design

According to the popular blog post released on the official Go blog last year, generics support has always been one of the top problems to fix in the language.

The initial design draft for this feature, which has been refined and updated several times over the years, introduced contracts, whose aim was to validate a set of type arguments to a function, as well as what methods can be called on those types.

The draft design introduced the idea of a named contract, where a contract is like a function body illustrating the operations that type must support. In summary, the goal was for the implementation to be able to constrain the possible types that can be used. An example extracted from the draft shows how to declare that values of type T must be comparable:

We made a custom demo for .
No really. Click here to check it out.

contract Equal(t T) {
        t == t
}

Then, to require a contract, we give its name after the list of type parameters:

type Set(type T Equal) []T

Finally, to use the defined contract in a function signature:

// Find returns the index of x in the set s,
// or -1 if x is not contained in s.
func (s Set(T)) Find(x T) int {
        for i, v := range s {
                if v == x {
                        return i
                }
        }
        return -1
}

Note: In the function definition above, we can see that we are making use of the defined contract, which specifies that the types must be comparable.

The summary of the contract design draft outlines that:

  • Functions and types can have type parameters, which are defined using optional contracts
  • Contracts describe the methods required and the built-in types permitted for a type argument
  • Contracts describe the methods and operations permitted for a type parameter
  • Type inference will often permit omitting type arguments when calling functions with type parameters

For more details on the earlier design draft, wherein the idea of contracts was extensively discussed and experimented with, have a look here.

Why the draft design changed, and where we are today

The problem with contracts was that the design seemed to introduce more complexities to the language. According to the latest draft design, since type lists appeared only in contracts rather than on interface types, many people had a hard time understanding this difference.

Therefore, the approach was simplified to use only interface types because it also turned out that contracts could be represented as a set of interfaces.

In the most recent design draft, the idea of contracts has been dropped entirely — a major update, to be sure. Therefore, we will not dwell so much on it in this article. For more details on the contract draft proposal, check the document here.

A summary of this design draft, which emphasizes type parameters or parameterized types, is outlined as follows.

Functions can have an additional type parameter list

We introduce the list with the keyword type — i.e., func F(type T)(p T) { ... }. An example extracted from the design draft shows the kind of function that can be permitted in order to support generic programming:

// Print prints the elements of a slice.
// It should be possible to call this with any slice value.
func Print(s []T) { // Just an example, not the suggested syntax.
        for _, v := range s {
                fmt.Println(v)
        }
}

The challenge here was on how the type parameter should be declared, since every identifier has to be declared somehow. The resolution was that type parameters were similar to ordinary non-type function parameters and, therefore, should be listed along with other parameters.

Note: These type parameters (distinguishable from non-type parameters ) can be used by the regular parameters and in the function body.

Types can have a type parameter list

This parameter list, implemented as type M(type T) []T, appears before the regular parameters. It starts with the keyword type, and lists type parameters.

// Print prints the elements of any slice.
// Print has a type parameter T, and has a single (non-type)
// parameter s which is a slice of that type parameter.
func Print(type T)(s []T) {
        // same as above
}

Note: This says that within the function Print, the identifier T is a type parameter, a type that is currently unknown but that will be known when the function is called.

Since Print has a type parameter, any call of Print must provide a type argument.

// Call Print with a []int.
        // Print has a type parameter T, and we want to pass a []int,
        // so we pass a type argument of int by writing Print(int).
        // The function Print(int) expects a []int as an argument.

        Print(int)([]int{1, 2, 3})

        // This will print:
        // 1
        // 2
        // 3

Note: Type arguments are passed as a separate list of arguments.

Each type parameter can have an optional type constraint

If a generic function does not specify a constraint for a type parameter, as is the case for the Print method above, then any type argument is permitted for that parameter.

The only operations that generic function can use with values of that type parameter are those operations that are permitted for values of any type. The operations permitted for any type are:

  • Declare variables of those types
  • Assign other values of the same type to those variables
  • Pass those variables to functions or return them from functions
  • Take the address of those variables
  • Convert or assign values of those types to the type interface{}, and so on

In defining constraints, Go already has a construct that comes pretty close to what is needed for a constraint: an interface type. In this new design, constraints are equivalent to interface types.

Additionally, writing a generic function is like using values of the interface type; the generic code can only use the operations permitted by the constraint (or operations that are permitted for any type).

Here’s another example extracted from the document:

// Stringer is a type constraint that requires the type argument to have
// a String method and permits the generic function to call String.
// The String method should return a string representation of the value.
type Stringer interface {
        String() string
}

Using a constraint

Although generic functions are not required to use constraints, they can be listed in the type parameter list as a meta-type.

// Stringify calls the String method on each element of s,
// and returns the results.
func Stringify(type T Stringer)(s []T) (ret []string) {
        for _, v := range s {
                ret = append(ret, v.String())
        }
        return ret
}

Note: From the function above, the single type parameter T is followed by the constraint that applies to T, in this case Stringer.

Other details on the latest proposal for generics include support for multiple type parameters:

// Print has two type parameters and two non-type parameters.
func Print(type T1, T2)(s1 []T1, s2 []T2) { ... }

Note that each of the type parameters may have their own constraint. Type parameters that do not have a constraint must have the constraint of an empty interface{} type.

Additionally, a single constraint can be used for multiple type parameters. However, the constraint applies to each of the type parameters separately. For more details on this updated draft document including topics around Generic types, we can check the full document here.

Note: Details on early Go designs for generics can be found here. Also, here are previous generics proposals, which are now incorrect as a result of the latest draft design.

The reasoning behind Go generics

What adding generics to Go would mean for the language

Every statically typed language has generics in one form or another. In Go today, every function we write must apply to only a single type, and of course, this is not scalable and leads to code duplication.

For example, it is not possible to write a simple copy function that can operate on any container type, e.g., map. What we have to do is write separate functions, with almost the same signature except for the types, for the different target types.

The goal of adding generics to the language is clear-cut: to be able to write libraries or functions that would work or operate on arbitrary types or values. This therefore means that the language should be able to record constraints on type parameters explicitly. Another of the frequent requests for generics in Go is the ability to write compile-time type-safe containers.

In summary, the aim is to support writing Go libraries that abstract away needless type detail by allowing parametric polymorphism with type parameters. Below, you can see another example extracted from the design document of how we might write a generic function once they’re fully implemented:

func Reverse (type Element) (s []Element) {
    first := 0
    last := len(s) - 1
    for first < last {
        s[first], s[last] = s[last], s[first]
        first++
        last--
    }
}

Notice that the body of the function is exactly the same; only the signature has changed. The element type of the slice has been factored out. It’s now named Element, and it is what we call a type parameter.

Calling a generic function looks just like calling any other function:

func ReverseAndPrint(s []int) {
    Reverse(s)
    fmt.Println(s)
}

In a dynamically typed language like Python or JavaScript, we can simply write the function without bothering to specify the element type. This doesn’t work in Go — it is statically typed, as we know, and requires that we write down the exact type of the slice and that of the slice elements.

Finally, generics would make it very easier to write functions once and use everywhere. What we mean here is that functions that work on specific data types like copy on string data types for example can be made generic and used on map types as well. They can even be extended to user defined data types.

Note: For further research and study on the reasons behind Generics in Go, you can also check out this recent article on Go’s official blog. Also check out the next steps towards Go 2, which contains the direction and the next steps for the proposal.

Importance and usefulness of Go generics

Why Go generics are important, and why we need them

According to the latest draft design for generics, generic programming enables the representation of algorithms and data structures in a generic form, with concrete elements of the code (such as types) factored out.

Generics in Go encompass this above definition and also entail a programming style where types are abstracted from function definitions and data structures. If you are quite familiar with Go, you would be aware that we could also do this with interfaces and type assertions, but then we have to write the same methods for all the types.

The idea here is to be able to factor out the element type. This will allow us write the function once, write the tests once, bundle them as a Go package, and use them whenever we want. Won’t it be nice to be able to reuse an efficient, fully debugged implementation that works for any value type?

Language constructs used before the Go generics proposal

What options can we use in place of generics, and why was generics support not added initially?

One of Go’s key distinguishing features is its approach to interfaces, which are also targeted at code reuse. Specifically, interfaces make it possible to write abstract implementations of algorithms.

Go already supports a form of generic programming via the use of empty interface types. For example, we can write a single function that works for different slice types by using an empty interface type with type assertions and type switches.

In essence, the empty interface type lets us capture different types via type switches and type assertions. We can then write functions that use those interface types, and those functions will work for any type we pass as arguments.

But this method does not support code reuse. With interfaces, we have to write the switch cases for every type we want to support. We have to then make use of type assertions and case statements where we check based on the type we want to support. For example, to reverse a slice of any element type, we can pass an empty interface as a parameter, and making use of type assertions, we can get the type we pass as arguments when we call the function.

Although the empty interfaces are a form of generics, they don’t give us everything we want from generics, as we just end up duplicating our code in multiple places without actually eliminating these duplicates.

Note: The reflect package also allows us to write a single function that can output a slice of any given type.

Conclusion

Generics will make Go safer, more efficient to use, and more powerful. This will allow us to implement many problems as functions that would apply to different types. Note that this is not peculiar to Go alone, but to other programming languages as well.

Previously, to write a function that can operate on container data types like slice[] and map types, we needed to make use of interface types with type assertions.

The latest draft updates include extending the Go language to add optional type parameters to types and functions, where these type parameters may be constrained by interface types. There is also a proposal for extending interface types, when used as type constraints, to permit listing the set of types that can be assigned to them.

Finally — and most importantly, in my opinion — the latest design is fully backward-compatible with Go 1. To experiment with all these examples, the go2go playground now supports generics, so please feel free to play around.

On a final note, the feedback gathered from the Go community regarding the draft design for generics will be used to determine how to proceed with the design of this feature. All things been equal, and if nothing has to be changed in the draft design document, the earliest that generics could be added to Go would be the Go 1.17 release, scheduled for August 2021.

Thanks for reading. Please, if you have any questions, comments, or contributions, do not hesitate to reach out in the comment section below or, alternatively, you can message me on Twitter.

Alexander Nnakwue Software Engineer. React, Node.js, Python, and other developer tools and libraries.

One Reply to “The past, present, and future of Go generics”

  1. Just a few small things I found misleading. First you say that contracts have been dropped. They have not been dropped but replaced with constraints – different syntax, different name but the same purpose. You also say that multiple type parameters are now allowed but multiple type parameters have been a part of all the draft designs.

Leave a Reply