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

Go generics: Past designs and present release features

10 min read 2821

Go Generics: Past Designs And Present Release Features

Editor’s note: This post was updated on 10 June 2022 to include information about generics in the Go v1.18 release.

Generic programming is also known as parametric polymorphism in other programming languages. It 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 initial 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 has now been added to the language core in the Go v1.18 release earlier this year.

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

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.

What are Go generics?

Every statically typed language has generics in one form or another. Generics offer us a new way to express type constraints in Go code. The overall purpose of generics in Go is to avoid boilerplate code or duplication of logic.

The Go authors had to weigh the pros and cons of generics in a programming language like Go, which was designed for networked system software. As a result, they initially opted out of features like concurrency, scalable builds, and so on.

The goal of adding generics to Go is to be able to write libraries or functions that would work or operate on arbitrary types or values. Therefore, the language should be able to record constraints on type parameters explicitly.

Another frequent request 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.

Problem case: Before generics in Go

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 supported 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.

The empty interface type lets us capture different types via type switches and type assertions. We can 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.

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

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.

Let us look at a quick example of a non-generic function below:

func Sum(args …int) int {
  var sum int
  for i := 0; i < len(args); i++ {
    sum += args[i]
  }
  return sum
}

What happens if we intend to implement this same feature for an int32 or an int64 data type? See below:

func SumInt64(args ...int64) int64 {
        var sum int64
        for i := 0; i < len(args); i++ {
                sum += args[i]
        }
        return sum
}

We would need to implement separate functions to handle each data type. With the release of generics, this is no longer needed.

A brief history of Go generics: From draft proposal to design

According to an official Go blog post from 2019, generics support has always been one of the top problems to fix in the language.

The initial design draft for Go generics, which was refined and updated several times over the years, introduced contracts. The aim of contracts 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 from the draft shows how to declare that values of type T must be comparable:

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
}

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

You can read more about the earlier design draft in an article by Russ Cox, wherein he discusses and experiments with the idea of contracts extensively.

Why the draft design for Go generics changed

The problem with contracts was that the design seemed to introduce more complexities to the language.

According to a draft design from 2019, since type lists appeared only in contracts rather than on interface types, many people had a hard time understanding this difference. The Go team therefore dropped the idea of contracts entirely.

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

Go generics today: Bounded type parameters

The type parameters proposal for adding generic programming to Go emphasizes optional type parameters or parameterized types. Here is an outline of this proposed and accepted design:

  • Type parameters are defined using constraints
  • Interface types act as constraint for type parameters
  • With interface types, we can add additional types which help in limiting the set of types that may satisfy a given constraint.
  • Function and type parameters may use operators, however, this must be allowed by all types satisfying the parameter constraint.
  • Type arguments can be omitted from function calls via type inference.
  • Generics implementation respects the Go 1.x backwards compatibility promise

Before the syntax for generics in Go was added, every function we wrote in Go had to apply only to a particular type.

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

With the release of generic programming in Go, we can now write a min() function that works for both integer and floating-point types without having to explicitly write them based on the types.

The Go generics design basically entails allowing types and function declarations to have optional type parameters. Type parameters are bounded by explicitly defined structural constraints.

Generic syntax in Go

Let’s explore some of the features of generics in Go 1.18.

Functions can have optional type parameters

Let’s start with the keyword type — i.e., func Add\[T any\](a, b T) T. An example below shows the kind of function that is permitted in generic programming:

func Sum[T int](args ...T) T {
        var sum T
        for i := 0; i < len(args); i++ {
                sum += args[i]
        }
        return sum
}

As we can see from the generic function above, [T int] represents the basic syntax for writing generic code in Go. T is the generic type and int is the constraint to that type. The constraint ensures type safety is guaranteed by specifying allowed type parameters.

The challenge here was about 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.

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

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. This parameter list appears before the regular parameters.

func Print(T int | int64)(args ...T) T{
        // same as above
}

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

func main() {
        fmt.Println(Sum([]int{1, 2, 3}...))
        fmt.Println(Sum([]int64{1, 2, 3}...))
}

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 functions 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.

Constraints are an interface type that function parameters have to fulfill. In a case where we have too many constraints, we can use the any keyword or abstract our type constraint as an interface type.

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 of a constraint below:

type Sumable interface {
        int | int64 | uint32
}

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

func Sum\[T Sumable\](args ...T) T {
        var sum T
        for i := 0; i < len(args); i++ {
                sum += args[i]
        }
         return sum
}

The any constraint

The any constraint is equivalent to an empty interface type interface{}. The problems with this is that an empty interface{} tells us nothing about the data and it is less safer, and requires too many error handling. See example below without type assertions:

func Sum\[T any\](args ...T) T {
        var sum T
        for i := 0; i < len(args); i++ {
                sum += args[i]
        }
        return sum
 }

Note: The above program will compile with an error because the compiler does not know anything about T or if it supports the addition operator.

Custom types in Go generics

Go generics includes support for custom type parameters:

type CustomIntType int
func main() {
        fmt.Println(Sum([]CustomIntType{1, 2, 3}...))
}

Note that each type parameter may have its 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.

Examples of functions using Go generics

Let’s review some examples of how we might write a generic function in Go today.

Sorting a slice of any type

// import the constraint package
func sortSlice\[T constraints.Ordered\](s []T) {
    sort.Slice(s, func(i, j int) bool {
        return s[i] < s[j]
    })
 }

 stringSlice := []string{"o", "a", "b"}
 sortSlice(stringSlice)

 fmt.Println(stringSlice) //[a b o]

intSlice := []int{0, 3, 2, 1, 6}
sortSlice(intSlice)
fmt.Println(intSlice) // [0 1 2 3 6]

The Ordered constraint permits any ordered type; that is, any type that supports the operators less than <, less than or equal to <=, greater than or equal to >=, or greater than >.

Checking if a slice contains a value

As we can see in the example below, calling a generic function looks just like calling any other function:

func contains\[T comparable\](elems []T, v T) bool {
    for _, s := range elems {
        if v == s {
            return true
        }
    }
    return false
}
func main() {
    fmt.Println(contains([]string{“e”,”f”, “g”}, “f”)) // true
    fmt.Println(contains([]int{5, 6, 7}, 8)) // false 
}  

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 since it is statically typed and requires that we write down the exact type of the slice and of the slice elements.

Finally, generics make it easier to write functions once to use everywhere. Functions that work on specific data types, like copy or string data types, can be made generic and used on map types as well. They can even be extended to user defined data types.

Why we need Go 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 the above definition and also entail a programming style where types are abstracted from function definitions and data structures. You may know we could also do this with interfaces and type assertions in Go, but we would 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 and bundle them as a Go package. Then, we can reuse an efficient, fully debugged implementation that works for any value type whenever we want.

When to use generics in Go

You should use Go generics if you are struggling to implement a solution that requires a singularity of behavior for different data types. This means that you want to define a set of behaviors or methods for all the different data types you want to store or pass around in a polymorphic way.

Go generics are also useful if you are reaching for an empty interface type when you do not intend to do some data validation or use any Marshal or Unmarshal operations.

Finally, use Go generics when writing functions that operate on container types like maps, slices, channels, or any other general-purpose data structure.

When not to use generics in Go

You should not use Go generics when calling a method on a type argument. Instead, use an interface type; e.g., replacing the io.Reader and io.Writer interface types

Also, method sets in Go have different implementations, meaning generics would be the wrong choice in this case.

Finally, don’t use Go generics when performing different operations for each type, even within method sets. In this case, use Go’s reflection API instead, as found in the JSON encoding and decoding package (decode.go) file.

Conclusion

Generic data structure and algorithms provide many advantages, including flexibility and code reusability via utility functions or writing algorithms that can then be applied on different type arguments.

Generics 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 and use previously debugged, optimized, and efficient packages or libraries, thus allowing for greater scalability and easier handling of a growing codebase.

Thanks for reading. 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.

2 Replies to “Go generics: Past designs and present release features”

  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