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 ofint
values.SumFloats()
: calculate the the sum of of a slice offloat32
values.SumComplexes()
: calculate the the sum of of a slice ofcomplex64
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 forinterface {}
.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:
- No type erasure.
- The standard library is still backwards compatible, so there is no need to rewrite your existing code.
- There are two new tutorials which explain generics and fuzzing.
- Type constraints section of Go spec.
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.