Generics in Go 1.18

January 17, 2022

I spent some time trying the generics types and generic functions in Go 1.18, and I like what they’ve got so far.

Below is a Go 1.17 program which prints the results of the following functions:

  • SumInts(): calculate the the sum of of a slice of int values.
  • SumFloats(): calculate the the sum of of a slice of float32 values.
  • SumComplexes(): calculate the the sum of of a slice of complex64 values.
package main

import "fmt"

// Return sum of all values in slice of ints.
func SumInts(m []int) int {
  var r int

  for _, v := range(m) {
    r += v
  }

  return r
}

// Return sum of all values in slice of float32s.
func SumFloats(m []float32) float32 {
  var r float32

  for _, v := range(m) {
    r += v
  }

  return r
}

// Return sum of all values in slice of complex64s.
func SumComplexes(m []complex64) complex64 {
  var r complex64

  for _, v := range(m) {
    r += v
  }

  return r
}

var (
  // test integers
  ints = []int { 10, 20, 30 }

  // test floating point numbers
  floats = []float32 { 10.0, 20.0, 30.0 }

  // test complex numbers
  complexes = []complex64 { complex(10, 1), complex(20, 2), complex(30, 3) }
)

func main() {
  // print sums
  fmt.Printf("ints = %d\n", SumInts(ints))
  fmt.Printf("floats = %2.1f\n", SumFloats(floats))
  fmt.Printf("complexes = %g\n", SumComplexes(complexes))
}

 

Here’s the same program, written using Go 1.18 generics:

package main

import "fmt"

// Return sum of all numeric values in slice.
func Sum[V ~int|~float32|~complex64](vals []V) V {
  var r V

  for _, v := range(vals) {
    r += v
  }

  return r
}

var (
  // test integers
  ints = []int { 10, 20, 30 }

  // test floating point numbers
  floats = []float32 { 10.0, 20.0, 30.0 }

  // test complex numbers
  complexes = []complex64 { complex(10, 1), complex(20, 2), complex(30, 3) }
)

func main() {
  // print sums using generics w/explicit types
  fmt.Printf("ints = %d\n", Sum[int](ints))
  fmt.Printf("floats = %2.1f\n", Sum[float32](floats))
  fmt.Printf("complexes = %g\n", Sum[complex64](complexes))
}

 

You can use type inference to drop the type parameters in many instances. For example, we can rewrite main() from the previous example like this:

func main() {
  // print sums using generics w/explicit types
  fmt.Printf("ints = %d\n", Sum(ints))
  fmt.Printf("floats = %2.1f\n", Sum(floats))
  fmt.Printf("complexes = %g\n", Sum(complexes))
}

 

Generics can also be used in type definitions. Example:

package main

import "fmt"

// Fraction
type Frac[T ~int|~int32|~int64] struct {
  num T // numerator
  den T // denominator
}

// Add two fractions.
func (a Frac[T]) Add(b Frac[T]) Frac[T] {
  return Frac[T] { a.num + b.num, a.den * b.den }
}

// Multiple fractions.
func (a Frac[T]) Mul(b Frac[T]) Frac[T] {
  return Frac[T] { a.num * b.num, a.den * b.den }
}

// Return inverse of fraction.
func (a Frac[T]) Inverse() Frac[T] {
  return Frac[T] { a.den, a.num }
}

// Return string representation of fraction.
func (a Frac[T]) String() string {
  return fmt.Sprintf("%d/%d", a.num, a.den)
}

func main() {
  // test fractions
  fracs = []Frac[int] {
    Frac[int] { 1, 2 },
    Frac[int] { 3, 4 },
    Frac[int] { 5, 6 },
  }

  // print fractions
  for _, f := range(fracs) {
    fmt.Printf("%s => %s\n", f, f.Mul(f.Add(f.Inverse())))
  }
}

 

Interface type declarations can now be used to define the constraints that a matching type must satisfy. In addition to the ability to specify methods that a matching type must implement, a type constraint specified as an interface may also specify a union of terms indicating the set of matching types.

Type union terms can be tilde-prefixed (example: ~int), which indicates that the underlying type must match the given type.

For example, the Frac type declaration from the previous example could be written like this instead:

// Integral number type.
type integral interface {
  ~int | ~int32 | ~int64
}

// Fraction
type Frac[T integral] struct {
  num T // numerator
  den T // denominator
}

 

There are two new predeclared identifiers:

  • any: An alias for interface {}.
  • comparable: Any type which can be compared for equality with == and !=. Useful for the parameterizing map keys.

There is a new constraints package, which (not yet visible in the online Go documentation as of this writing) that provides a couple of useful unions, but it’s relatively anemic at the moment:

$ go1.18beta1 doc -all constraints
package constraints // import "constraints"

Package constraints defines a set of useful constraints to be used with type
parameters.

TYPES

type Complex interface {
	~complex64 | ~complex128
}
    Complex is a constraint that permits any complex numeric type. If future
    releases of Go add new predeclared complex numeric types, this constraint
    will be modified to include them.

type Float interface {
	~float32 | ~float64
}
    Float is a constraint that permits any floating-point type. If future
    releases of Go add new predeclared floating-point types, this constraint
    will be modified to include them.

type Integer interface {
	Signed | Unsigned
}
    Integer is a constraint that permits any integer type. If future releases of
    Go add new predeclared integer types, this constraint will be modified to
    include them.

type Ordered interface {
	Integer | Float | ~string
}
    Ordered is a constraint that permits any ordered type: any type that
    supports the operators < <= >= >. If future releases of Go add new ordered
    types, this constraint will be modified to include them.

type Signed interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}
    Signed is a constraint that permits any signed integer type. If future
    releases of Go add new predeclared signed integer types, this constraint
    will be modified to include them.

type Unsigned interface {
	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
    Unsigned is a constraint that permits any unsigned integer type. If future
    releases of Go add new predeclared unsigned integer types, this constraint
    will be modified to include them.

 

Using the constraints package, the Frac type from the previous example could be written like this:

import "constraints"

// Fraction
type Frac[T constraints.Signed] struct {
  num T // numerator
  den T // denominator
}

 

And with constraints, the Sum() function from the first example could be defined like this:

// Numeric value.
type Number interface {
  constraints.Integer | constraints.Float | constraints.Complex
}

// Return sum of all numeric values in slice.
func Sum[V Number](vals []V) V {
  var r V

  for _, v := range(vals) {
    r += v
  }

  return r
}

 

Other useful tidbits:

Update (2021-01-19): Minor wording changes, add information about tilde prefixes in type constraints.

Update (2021-02-13): The constraints package was moved to x/exp for Go 1.18. LWN has an excellent summary of Go 1.18.

Update (2021-03-17): Go 1.18 released.