Cover image

Generic programming for statically typed languages is a tool for building maintainable software. Generics have a lot of advantages and can create infinitely reusable code, they do however, have a history of being overused and misunderstood leading to anti-patterns1. Due to the regularity of misuse the Go team has been reluctant to add generics to the language.

Beginning in February 2022, Go will introduce generics support in its 1.18 release2. The Go team spent many years researching and experimenting with different implementations of generics and their implications for Go. The Go team believes they have found an implementation that is both effective and, more importantly, maintains the simplicity of the language.3

Table Of Contents

History of Generics in Go

One of the first concerns raised by engineers (back in 20094, before the first release of Go) about the Go language was the lack of generics support. For many years5 the community has been asking for generics and, for many years, there has been pushback from the Go language team.

Generics may well be added at some point. We don’t feel an urgency for them, although we understand some programmers do.

- Originally posted to the Go FAQ

Back To Top

My Original Opinion on Generics in Go

I have to admit my skepticism regarding the need for generics in Go. I have been using Go for years now and never thought: “This code would be easier if I had generics.”

I can definitively say – I was wrong.

I began, reluctantly, playing with the new generics support after the official announcement at Gophercon 20212. I built generics libraries in the past and I found the Go generics syntax to be intuitive and easy to understand. As I started building out more than just “test the language feature” code, I was hooked.

As I look back to my pre-Go days (C#, C++, etc…) I realized a great deal of the code I wrote used generics (lists, collections, etc…). With Go became accustomed to re-writing helper functions or duplicating the A meme of Will
Farrel’s character from Zoolander saying “They’re all the same! Doesn’t anybody
notice this?” owner pattern regularly enough I created a snippet in order to maintain sanity.

The worst problems arose when I needed to fix a bug in a standard pattern, causing cascading changes through each of my codebases due to boilerplate duplication.

Talk about a lot of duplication!

So, I can confidently say, generics in Go have already made me much more efficient and effective. The benefits generics provide: consistency, single-change fixes, and testability, will greatly improve my (and the Go community’s) overall code quality and stability going forward.

Back To Top

First Things First

A meme of Will Farrel’s Anchorman with text reading “Hey Everyone, come and
see how good I look”

Generic support is currently in beta and is not yet available in a stable release form. If want to use generics, or want to follow along with this post, you will need to install go1.18beta16 or gotip. Personally, I recommend using gvm to install the dev branch of go. It’s simple to install (I built it, so I’m biased) and it will install the latest development branch of Go with a simple (gvm next) command.

If you haven’t read my post on Building a Go Version Manager, you should check it out. The post walks through the process of installing gvm7 and using the development branch of Go.

Back To Top

The new “any” Predeclared Identifier

The introduction of generics also adds a small, but not insignificant, new type alias (a.k.a. predeclared identifier)8 as a replacement for the empty interface (interface{}). The new keyword (any) is fully backwards compatible with the empty interface and can be used as a replacement for it.

So, how exactly do generics work in Go?

A meme of an elderly woman asking “How does it
work?” Generics in Go are similar to other statically typed languages. Type parameters9 are configured on a function, or a struct, to indicate its generic nature. One of the primary differences in Go (when compared to other languages like C#, C++, etc…) is the type parameters are not completely without restriction.

Type Parameters and Type Constraints

Type Parameters

In similar fashion to other statically typed languages, type parameters are denoted using a capital letter (i.e. T) wrapped in square brackets [] immediately following the function or struct name (i.e. func f[T any](t T)). The type parameter is then used in the execution of the function, or instantiation of the struct, to indicate the type of the parameter.

 1// Define the function f with a type parameter T
 2func f[T any](t T) {
 3  // ...
 4}
 5
 6// ...
 7
 8// Call the function with a concrete type
 9f[int](1)
10
11// Define a struct with a type parameter T
12type s[T any] struct {
13  t T
14}
15
16// Instantiate the struct with a concrete type
17myInstance := s[int]{t: 1}

Explicit reference to the concrete type when executing a method, or instantiating a class, is not always necessary (more on this when we talk about type inference).

Type Erasure

With Go type parameters the full metadata of a type is available through the reflection library. As stated below by the Go team.

Most complaints about Java generics center around type erasure. This design does not have type erasure. The reflection information for a generic type will include the full compile-time type information.

Go Generics Proposal - August 20, 2021

Ian Lance Taylor & Robert Griesemer

Back To Top

Type Constraints

In languages, like C#, C++, etc…, the type parameters are completely type agnostic, but in Go they are not. The Go team experimented for years to find the right implementation for generics resulting in a constraint-based approach.10

Constraints are used to create a union (denoted by |) of allowed types.

For example, consider the following type constraint:

1type MyConstraint interface {
2  int | int8 | int16 | int32 | int64
3}

The above constraint indicates when used as a type parameter, the type a generic function, or struct, can accept is limited to the types in the union (int, int8, int16, int32, int64).

Example of Type Constraints: Valid and Invalid Usage

 1// Example of a generic function using
 2// the MyConstraint type parameter
 3func MyFunc[T MyConstraint](input T) {
 4  // ...
 5}
 6
 7func main() {
 8  // Valid because type int8 is in the type set
 9  // (int | int8 | int16 | int32 | int64)
10  MyFunc[int8](1)
11
12  // Invalid because type string is not in the type set
13  MyFunc[string]("hello")
14}

In the screenshot below, you can see the error the compiler generates when the MyFunc function is called with a type not in the type set.11

A screenshot of a compiler error generated by the above
code

Back To Top

Benefits of Constraints

One of the benefits of using constraints is they allow you to define a set of allowed types represented by the type parameter. Constraint enforcement helps reduce possible misuse.

Another benefit of type constraints is their explicit nature; helping redirect the thought process for creating generic implementations, away from a type free-for-all towards a pre-defined expected behavior.

Constraints lead to more maintainable, testable, and reusable code.

Caveats when Defining Constraints with Methods

1type MyConstraint interface {
2  Integer | Float | ~string
3  String() string
4}


BEST PRACTICE:
When creating a constraint, that adds a method to the interface with builtin types, ensure the constraint specifies any builtin using the ~ token. If the ~ is missing the constraint can never be satisfied since Go builtin types do not have methods.

Back To Top

Underlying Types and the ~ Operator

One of the newly announced elements of the generic implementation for Go is the ~ token12. With type definition overrides there was need for a way to indicate a type parameter should allow any type within the constraint if the underlying type matched the constraint. The ~ token was added to support this requirement.

Example of Type Re-definition with Generics

 1type MyEnumeration int16
 2
 3const (
 4  MyEnumeration_A MyEnumeration = iota
 5  MyEnumeration_B
 6  MyEnumeration_C
 7)
 8
 9func Process[T int16](value T) {
10  // ...
11}
12
13func test() {
14  Process(MyEnumeration_A)
15}

The example above defines an enumeration type definition for int16 (the underlying type). If we attempt to execute the Process function using the MyEnumeration type compilation will fail.

A screenshot of a compiler error when using the <code>MyEnumeration</code> type in a
generic function with an integer constraint

If we update the constraint on the Process function to T ~int16 then the compiler understands MyEnumeration can be used because its underlying type is int16 (the compiler no longer errors as seen below).

A screenshot of a compiler error when using the <code>MyEnumeration</code> type in the
process function after adding ~

Comparability and Ordering of Constrained Types

The new comparable keyword, in Go 1.18, was added for specifying types that can be compared with the == and != operators.

Comparable types include: structs, pointers, interfaces, channels, and builtin types. comparable can also be embedded in other constraints since it is a constraint.13

From the Go 1.18 documentation:

type - comparable

comparable is an interface that is implemented by all comparable types (booleans, numbers, strings, pointers, channels, interfaces, arrays of comparable types, structs whose fields are all comparable types). The comparable interface may only be used as a type parameter constraint, not as the type of a variable.14

One of the first things I learned, when starting with Go generics, was the any type does not satisfy the comparable constraint.15 any violates the comparability constraint due to the fact that under the hood any is just the empty interface. Empty interface can be a nil, a struct type, a pointer type, a slice, a map, a function literal, etc… Function literals, and maps for example, cannot be compared in non-generic code and the same is true in generic code, but they can be assigned to an empty interface. Thus the inability to use any as a comparable type makes sense.

The operations permitted for the any type are:15

  • 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{}
  • convert a value of type T to type T (permitted but useless)
  • use a type assertion to convert an interface value to the type
  • use the type as a case in a type switch
  • define and use composite types that use those types, such as a slice of that type
  • pass the type to some predeclared functions such as new

– Ian Lance Taylor & Robert Griesemer

The Go team created a package of constraints (constraints) that can be imported and used for the most generic of contraint types. One important constraint is constraints.Ordered, which allows the use of the <, <=, >, and >= operators.

1type Ordered interface {
2        Integer | Float | ~string
3}

The definition of the above Ordered constraint makes sense it would allow the use of the <, <=, >, and >= operators since these types are normally comparable outside of a generic scope.

NOTE: Only operations pertaining to ALL types of a union operation in a type constraint can be used against the type parameter in a generic struct or function.

Back To Top

Type Inference

The statically typed nature of Go created a unique opportunity syntactically when implementing generics. If we examine the process of calling a generic function, or instantiating a generic struct, we see the type of the parameters is pre-defined. Since the type of the parameters is known, a priori, the compiler can infer the type of parameters in most situations.16

Example of Type Inference

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func MyFunc[T any](t T) {
  // ...
}

// ...

// Create a variable with a concrete type
var myVariable int = 1

// Call without type inference
MyFunc[int](myVariable)

// Call with type inference
MyFunc(myVariable)

Type inference makes it possible to exclude the type parameter from the call to a function, or instantiation of a struct, in most cases. In the example above, the call to MyFunc on line 11 explicitly declares the type to be used but in the call on line 14 the type of the parameter is inferred from the type of the variable declared on line 8 (int). In other words, [int] can be safely omitted.

Type Inference Failure

There will be times the compiler is unable to infer the type of a parameter. In the case of inference failure, simply specify the type of the parameter in the function call or struct instantiation.

Type Chaining and Constraint Type Inference

Type Chaining

Type chaning is a technique that allows you to define a type parameter that is a composite of another type parameter. The composite nature of type parameters is useful when you define a secondary type in a generic struct or function.

 1// Example of Type Chaining
 2func ToChan[U ~[]T, T any](t U) <-chan T {
 3  c := make(chan T)
 4
 5  // ...
 6
 7  return c
 8}
 9
10//...
11
12c := ToChan([]int{1, 2, 3})

Constraint Type Inference

In the example above, the type of the parameter U is defined as ~[]T (a composite of T) and the type of the parameter T is defined with a constraint of any. The type of the parameter U can be inferred using the type of the parameter T when the ToChan method is called.17

The compiler knows when ToChan is called the incoming type parameter U must be a slice of T and the type of the parameter T is int. The compiler can determine from the call the type of the parameter U is of type []int.


BEST PRACTICE:
Type parameters should be written “in line”

Creating a Generic Struct

Creating a generic struct in Go is possible with type parameters. For general purpose data structures this allows data to be stored in a generic struct and that is great, however, it is recommended to use a generic method when manipulating the data structure so the function is not tied to the specific struct and is more widely usable.

For general purpose data types prefer a function, rather than writing a constraint that requires a method.

– Ian Lance Taylor

Back To Top

When to Use Generics

Generics are a great tool for creating reusable code, however, they are not always the best tool. The majority of situations do NOT require generics and a guiding principle is to replace duplicated boilerplate code with generic code.18 If the code you are writing can be constrained down to a couple of types, then perhaps use interfaces instead.19

It is essential to have good tools, but it is also essential that the tools should be used in the right way.

– Wallace D. Wattles

Below is an example of when generics are the right tool for the job:

Example: Code Duplication Pre-Generics

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Example of channel conversions without generics
func SToIChan(in <-chan string) <-chan any {
  out := make(chan any)
  go func() {
    defer close(out)
    
    for s := range in {
      out <- s
    }
  }()
  return out
}

func IntToIChan(in <-chan int) <-chan any {
  out := make(chan any)
  go func() {
    defer close(out)
    
    for s := range in {
      out <- s
    }
  }()
  return out
}

type MyStruct struct {
  Data int
  Tags string
}

func StrToIChan(in <-chan MyStruct) <-chan any {
  out := make(chan any)
  go func() {
    defer close(out)
    
    for s := range in {
      out <- s
    }
  }()
  return out
}

Breaking down the functionality of each method above shows this is a prime use case for generics. The code on lines 3-11, 15-23, and 32-40 are all exactly the same. The above methods can be consolidated into a single generic function (see below), reducing the code by 16 lines (and reducing code duplication by a factor of 66%).

Example: Code Consolidation Post-Generics

 1// Example of channel conversion using generics
 2func ToAnyChan[T any](in <-chan T) <-chan any {
 3  out := make(chan any)
 4  go func() {
 5    defer close(out)
 6    
 7    for s := range in {
 8      out <- s
 9    }
10  }()
11  return out
12}
13
14ToAnyChan(mystringchan)
15ToAnyChan(myintchan)
16ToAnyChan(mystructchan)

The next post will dive head first into building useful generics in Go.

Back To Top

What the introduction of Generics means for the Go Language

Working with Go generics for the last six weeks has been a great experience and I think the community is going to quickly adopt them. The reduction in duplicated code is important and the benefits outweigh the cost of rewriting existing duplicated code. In terms of stability and maintainability, we as a community will start to see the improvements quickly.

Reducing the Learning Curve for New Go Engineers

The concurrent design nature of Go can create a high barrier to entry for new and inexperienced engineers. The introduction of generics in Go can help reduce the learning curve by providing access to stable, and tested generic use libraries for concurrent programming.

I am most excited about the genericization of concurrency patterns which I will cover in subsequent posts.

Back To Top