Martin Sulzmann
Recent versions of Go support generics.
To get started here is some material provided by the Go development team.
An Introduction To Generics by Robert Griesemer and Ian Lance Taylor
Below you find further material
Why generics
Some basic examples
A brief dicussion of the limitations of Go’s current design
Some implementation details
“Generics” allow to abstract over type parameters.
Thus, we can write more type-safe programs.
Some collection.
We use the most general type interface{}
We may want to have pairs of (string, float32), (string, string), …
Some convenience functions.
Some application.
var s string
var f float32
s = "Erdinger"
f = 0.9
product := pair{s, f}
var price float32
price = second(product).(float32) // Type assertions!
string <= interface{}
float32 <= interface{}
product is of type pair
second
yields a value of “any” type
We use a type assertion to case this value into a value of type
float32
Type assertions may fail!
The above type checks
But will fail at run-time
Generic collection.
S
and T
are type parameters
They can be instantiated for any type
In Go, type paramters are always constrained by a type bound
any = interface{}
Making the above code more type-safe.
var s string
var f float32
s = "Erdinger"
f = 0.9
product := pairG[string, float32]{s, f}
var price float32
price = secondG(product)
There is no need for a type assertion
The above code type checks (statically = compile-time)
This guarantees that we will not fail (due to incompatible) at run-time.
Consider
package main
import (
"fmt"
)
type pair struct {
left interface{}
right interface{}
}
func first(x pair) interface{} {
return x.left
}
func second(x pair) interface{} {
return x.right
}
func testPair() {
var s string
var f float32
s = "Erdinger"
f = 0.9
product := pair{s, f}
// string <= interface{}
// float32 <= interface{}
var price float32
price = second(product).(float32) // Type assertions!
// price = first(product).(float32)
// Type checks but fails at run-time.
fmt.Printf("%f", price)
}
// Generics
type pairG[T any, S any] struct {
left T
right S
}
func firstG[T any, S any](x pairG[T, S]) T {
return x.left
}
func secondG[T any, S any](x pairG[T, S]) S {
return x.right
}
func testPairG() {
var s string
var f float32
s = "Erdinger"
f = 0.9
product := pairG[string, float32]{s, f}
var price float32
price = secondG(product)
// price = firstG(product)
// Fails to type check!
fmt.Printf("%f", price)
}
func main() {
testPair()
testPairG()
}
The generic swap function.
T
is a type parameter.
In Go, type parameters always come with a bound (aka constraint).
We refer to this as bounded type parameters (or bounded types for short). Constrained type parameters is another appropriate name.
In the above example, T
is constrained by
any
where any
is a short-hand for
interface{}
(the empty interface).
Any type satisfies the empty interface. Hence, we can swap values of any type.
We can use the swap function to swap Integer values, Boolean values, …
To do so, we need to explicitely provide the type instance.
In the above, we wish to swap int
values.
The program text swap[int]
states that the type
parameter T
in the definition of swap will be instantiated
by int
.
Interfaces are another means to write generic functions in Go.
type Show interface {
show() string
}
type boolean struct {
val bool
}
func (this boolean) show() string {
if this.val {
return "true"
} else {
return "false"
}
}
type number struct {
val int
}
func (this number) show() string {
return strconv.Itoa(this.val)
}
The receiver types boolean
and number
implement the Show
interface.
Interfaces are types.
Hence, we can argue that for example a value of type
boolean
can be used in a context where we expect a value of
type Show
.
This is referred to as structural subtyping.
Consider the following generic showTwice1
function.
func showTwice1(x Show, y Show) {
fmt.Printf("\n %s", x.show()+y.show())
}
func testShow() {
n := number{1}
b := boolean{true}
// We write number <= Show to denote that number is a structural subtype of Show.
// Similarly, we write boolean <= Show.
showTwice1(n, b)
showTwice1(n, n)
showTwice1(b, b)
}
Function showTwice1
is generic because we can apply this
function on numbers and booleans.
We take a look at the combination of interfaces and type parameters.
Consider the following generic showTwice2
function.
We find type parameter T
where any type instance must
satisfy the Show
interface.
Consider
We know that number
and boolean
both
satisfy Show
.
Hence, we can call showTwice2 on numbers and booleans.
Go supports simple forms of type inference that extends to type instances.
For example, in the call showTwice2(n, n)
. Type instance
[int]
is automatically inferred.
The call showTwice2(n,b)
is not accepted. Why?
Type parameter T
must be either instantiated to
number
or boolean
. If we pick
number
, the second argument b
leads to a type
error. If we pick boolean
, the first argument
n
leads to a type error.
Consider
The above describes the union of types int
and
float32
. Hence, we call this a type set.
Type sets can be used in type bounds.
However, there’s no value of type intOrfloat
and
therefore the following variant is rejected
Points to note:
We can use user-defined types such as structs in type sets.
We cannot use interface types in type sets. The following is rejected
package main
import (
"fmt"
"strconv"
)
// Generic swap function.
// Points to note.
//
// 1. T is a type parameter.
// 2. Type parameter come with a bound (aka constraint).
// 3. Bounds are types.
// 4. "any" is a short-hand for "interface{}".
// 5. Any type T satisfies the empty interface.
// 6. Hence, we can swap values of any type.
func swap[T any](x *T, y *T) {
tmp := *x
*x = *y
*y = tmp
}
func testSwap() {
x := 1
y := 3
fmt.Printf("\n x = %d, y = %d", x, y)
swap[int](&x, &y)
fmt.Printf("\n x = %d, y = %d", x, y)
}
// Showing values by turning them into strings.
type Show interface {
show() string
}
type boolean struct {
val bool
}
func (this boolean) show() string {
if this.val {
return "true"
} else {
return "false"
}
}
type number struct {
val int
}
func (this number) show() string {
return strconv.Itoa(this.val)
}
// Interfaces are types.
// Valid arguments are values of some type that implement the Show interface.
func showTwice1(x Show, y Show) {
fmt.Printf("\n %s", x.show()+y.show())
}
// Type parameter T is constrained by Show.
func showTwice2[T Show](x T, y T) {
fmt.Printf("\n %s", x.show()+y.show())
}
func testShow() {
n := number{1}
b := boolean{true}
showTwice1(n, b)
showTwice1(n, n)
showTwice1(b, b)
// Not accepted, can you guess why?
// showTwice2(n,b)
showTwice2(n, n)
showTwice2[boolean](b, b)
}
// Type sets.
type intOrfloat interface {
int | float32
}
func myAdd[T intOrfloat](x T, y T) T {
return x + y
}
/*
// There's no value of type intOrfloat.
func myAdd2[T intOrfloat](x T, y T) intOrfloat {
return x + y
}
*/
func testAdd() {
fmt.Printf("\n %d", myAdd[int](1, 2))
fmt.Printf("\n %f", myAdd[float32](1, 2.0))
/*
// x is of type int, there's no automatic conversion to float32 in Go.
x := 1
fmt.Printf("\n %f", myAdd[float32](x,2.0))
*/
}
func main() {
testSwap()
testShow()
testAdd()
}
The current implementation (applies to Go 1.18 and 1.19) does not quite follow the design that has been described in Featherweight Go (appeared in OOPSLA’21).
For example, if we want to make pairs showable, the “Show” constraint needs to be included in the definition of pairs.
type pair[T Show, S Show] struct {
left T
right S
}
func (this pair[T,S]) show() string {
return "(" + this.left.show() + "," + this.right.show() + ")"
}
This implies that we can only build pairs whose components are showable.
The design proposed in Featherweight Go allows for the following (more flexible) program code.
type ppair[T any, S any] struct {
left T
right S
}
func (this ppair[T Show,S Show]) show() string {
return "(" + this.left.show() + "," + this.right.show() + ")"
}
However, the above method definition is rejected by Go 1.18 and 1.19.
Once you have generics, you might not want to use type assertions at all! However, there may be reasons use them (reflection, …).
The following code is valid.
The following code is invalid.
The Go compiler reports:
invalid operation: cannot use type assertion on type parameter value x (variable of type T constrained by any)
But there is no real reason why both shouldn’t be accepted.
package main
import "fmt"
// Generics in Go as implemented have some limitations.
///////////////////////////////////////////////////////
// Showing values by turning them into strings.
type Show interface {
show() string
}
type boolean struct {
val bool
}
func (this boolean) show() string {
if this.val {
return "true"
} else {
return "false"
}
}
type pair[T Show, S Show] struct {
left T
right S
}
func (this pair[T, S]) show() string {
return "(" + this.left.show() + "," + this.right.show() + ")"
}
func testShow() {
p := pair[boolean, boolean]{boolean{true}, boolean{false}}
fmt.Printf("\n %s", p.show())
}
// Point to note.
// Pairs are only showable (by defining the show method),
// if we constraint the left and right component to implement the Show interface.
type ppair[T any, S any] struct {
left T
right S
}
/*
The following is not accepted.
In a method definition, we cannot "specialize" type bounds.
func (this ppair[T Show,S Show]) show() string {
return "(" + this.left.show() + "," + this.right.show() + ")"
}
*/
// On the other hand, this is not a problem for function definitions.
// Hence, the current Go design feels uneven.
func showFunc[T Show, S Show](this ppair[T, S]) string {
return "(" + this.left.show() + "," + this.right.show() + ")"
}
func testShowFunc() {
p := ppair[boolean, boolean]{boolean{true}, boolean{false}}
fmt.Printf("\n %s", showFunc[boolean, boolean](p))
}
// The use of type sets is strictly limited to built-in primitives.
func plus3[T int | float32 | float64](x T, y T, z T) T {
return x + y + z
}
type onOff struct {
val bool
}
func (this onOff) show() string {
if this.val {
return "On"
} else {
return "Off"
}
}
// The following is rejected.
// func showOnlyOnOffandPair[T boolean | onOff](x T) string { return x.show() }
// Type assertions
///////////////////////////////
func assert1(x any) {
y := x.(boolean)
fmt.Printf("\n%s", y.show())
}
func assert1b(x Show) {
y := x.(pair[boolean, boolean])
fmt.Printf("\n%s", y.show())
}
/*
// "invalid operation: cannot use type assertion on type parameter value x (variable of type T constrained by any)"
// But there's no real reason why this shouldn't be accepted.
func assert2[T any](x T) {
y := x.(boolean)
fmt.Printf("\n%s",y.show())
}
// Invalid again.
// Variant of assert2.
func assert2b[T Show](x T) {
y := x.(pair[boolean,boolean])
fmt.Printf("\n%s",y.show())
}
*/
/*
// Invalid again.
func assert3[T Show](x pair[T,T]) {
y := x.(pair[boolean,boolean])
fmt.Printf("\n%s",y.show())
}
*/
func testAssert() {
assert1(boolean{true})
// assert1(1)
// fails at run-time
assert1b(pair[boolean, boolean]{boolean{false}, boolean{true}})
// assert1b(boolean{false})
// fails at run-time
}
func main() {
testShow()
testShowFunc()
testAssert()
}
In an efficient implementation, Go’s features such as method overloading and interfaces are implemented by translation to a simple(r) target language. Go has structural subtyping. This is an important aspect that also needs to be considered.
The translation of Go without generics is covered here
For formal details check out A Dictionary-Passing Translation of Featherweight Go also appeared in APLAS’21.
The translation of Go with generics is covered by the following works:
A Type-Directed, Dictionary-Passing Translation of Featherweight Generic Go
The last work proposes a more general translation scheme that that does not come with any of the limitations mentioned earlier.