Technology Sharing

Go Generics Explained

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Introduction

If we want to write a function to compare the size of two integers and floating-point numbers respectively, we have to write two functions. As follows:

func Min(x, y float64) float64 {
    if x < y {
        return x
    }
    return y
}

func MinInt(x, y int) int {
	if x < y {
		return x
	}
	return y
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

The two functions have the same processing logic except for the different data types. Is there a way to use one function to complete the above functions? Yes, that is generics.

func min[T int | float64](x, y T) T {
	if x < y {
		return x
	}
	return y
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Generics

Official website documentation:https://go.dev/blog/intro-generics
Generics add three important new features to the language:

  • Type parameters for functions and types.
  • Defines an interface type as the set of types, including types with no methods.
  • Type inference, which in many cases allows type parameters to be omitted when calling a function.

Type Parameters

Functions and types are now allowed to have type parameters. A type parameter list looks like a normal parameter list, except that it uses square brackets instead of parentheses.
insert image description here

package main

import (
	"fmt"
	"golang.org/x/exp/constraints"
)

func GMin[T constraints.Ordered](x, y T) T {
	if x < y {
		return x
	}
	return y
}

func main() {
	x := GMin[int](2, 3)
	fmt.Println(x) // 输出结果为2
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

Among them, constraints.Ordered is a custom type (the source code is not shown here).
If you don't understand, you can temporarily replace constraints.Ordered with ·int | float64

Providing a type parameter (in this case, int) to GMin is called instantiation. Instantiation occurs in two steps.

  • First, the compiler replaces all type arguments with their respective type parameters throughout the generic function or type.
  • Second, the compiler verifies that each type parameter satisfies its respective constraints.
    We'll see what this means shortly, but if the second step fails, instantiation will fail and the program will be invalid.

After successful instantiation, we have a non-generic function that can be called like any other function. For example, in code like

fmin := GMin[float64]
m := fmin(2.71, 3.14)
  • 1
  • 2

The full code is

package main

import (
	"fmt"
	"golang.org/x/exp/constraints"
)

func GMin[T constraints.Ordered](x, y T) T {
	if x < y {
		return x
	}
	return y
}

func main() {
	fmin := GMin[float64] // 相当于func GMin(x, y float64) float64{...}
	m := fmin(2.71, 3.14)
	fmt.Println(m) // 输出结果为2.71
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

Instantiating GMin[float64] actually generates our original floating point Min function, which we can use in function calls.

Type parameters can also be used with types.

type Tree[T interface{}] struct {
    left, right *Tree[T]
    value       T
}

func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }

var stringTree Tree[string]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Here the generic type Tree stores the value of the type parameter T. Generic types can have methods, like Lookup in this example. To use a generic type,It must be instantiated; Tree[string] is an example of instantiating Tree with type parameter string.

Type sets

Each type parameter in a type parameter list has a type. Since a type parameter is itself a type, the type of a type parameter defines the set of types. This metatype is calledType Constraints
In the generic method GMin, type constraints are imported from the constraints package. The Ordered constraint describes the set of all types that have orderable values, or in other words, are compared with the &lt; operator (or &lt;=, &gt;, etc.). This constraint ensures that only types with orderable values ​​can be passed to GMin. This also means that in the GMin function body, the value of this type parameter can be used for comparison with the &lt; operator.
In Go, type constraints must be interfacesThat is, interface types can be used as value types as well as meta-types. Interfaces define methods, so obviously we can express type constraints that require the presence of certain methods. But constraints.Ordered is also an interface type, and the &lt; operator is not a method.
The dual purpose of interface types is indeed an important concept in Go. Let's take a deeper look and give examples to illustrate the statement that "interface types can be used as value types and metatypes"[1][2][3][4][5].

  1. Interfaces as value types:

When an interface is used as a value type, it defines a set of methods, and any type that implements these methods can be assigned to this interface variable. This is the most common usage of interfaces.

For example:

type Stringer interface {
    String() string
}

type Person struct {
    Name string
}

func (p Person) String() string {
    return p.Name
}

var s Stringer = Person{"Alice"} // Person 实现了 Stringer 接口
fmt.Println(s.String()) // 输出: Alice
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

In this example,Stringer Interfaces are used as value types,Person Type implementsString() method, so it can be assigned toStringer Type of variable.

  1. Interface as meta-type:

When an interface is used as a metatype, it defines a set of type constraints for generic programming. This is a new usage after Go 1.18 introduced generics.

For example:

type Ordered interface {
    int | float64 | string
}

func Min[T Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

fmt.Println(Min(3, 5))       // 输出: 3
fmt.Println(Min(3.14, 2.71)) // 输出: 2.71
fmt.Println(Min("a", "b"))   // 输出: a
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

In this example,Ordered The interface is used as a metatype, which defines a set of types (integers, floating point numbers, and strings) that can be compared.Min The function uses this interface as a type constraint and can accept anyOrdered The type of the constraint is taken as a parameter.

This dual purpose makes Go interfaces very powerful and flexible in generic programming. They can not only define the behavior of objects (as value types), but also define type collections (as metatypes), thus greatly enhancing the expressiveness and reusability of the code while maintaining the simplicity of the language.

Until recently, the Go spec said that an interface defines a set of methods, roughly the set of methods enumerated in the interface. Any type that implements all of those methods implements the interface.
insert image description here
But another way to look at this is to say that an interface defines a set of types, namely types that implement these methods. From this perspective, any type that is an element of the interface type set implements the interface.
insert image description here
Both views lead to the same result: for each set of methods, we can imagine a corresponding set of types that implement those methods, namely the set of types defined by the interface.

For our purposes, though, type set views have one advantage over method set views: we can explicitly add types to the set, allowing us to control type sets in new ways.

We extend the syntax of interface types to do this. For example, interface{ int|string|bool } defines a type set that includes the types int, string, and bool.
insert image description here
Another way to say this is that the interface is only satisfied by int, string, or bool.

Now let's look at the actual definition of constraints.Ordered:

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

This declaration says that the Ordered interface is the set of all integer, floating point, and string types. The vertical bar represents a union of types (or a set of types in this case). Integer and Float are similarly defined interface types in the constraint package. Note that the Ordered interface does not define any methods.

For type constraints we usually don't care about specific types, such as strings; we are interested in all string types. This is ~ The purpose of the token. Expression~string Represents the set of all types whose underlying type is string. This includes type string itself and all types declared using defines, such astype MyString string

Of course we still want to specify methods in interfaces, and we want to be backwards compatible. In Go 1.18, interfaces can contain methods and embed interfaces as before, but it can also embed non-interface types, unions, and underlying typesets.

The interface used as a constraint can be specified by name (such as Ordered) or it can be a literal interface inline in the type parameter list. For example:

[S interface{~[]E}, E interface{}]
  • 1

Here S must be a slice type, and its element type can be any type.

Because this is a common case, the enclosing interface{} can be omitted for interfaces in the constraint position, and we can simply write (syntactic sugar for generics and simplified writing of type constraints in the Go language):

[S ~[]E, E interface{}]
  • 1

Since empty interfaces are common in type parameter lists and ordinary Go code, Go 1.18 introduces a new predeclared identifier any as an alias for empty interface types. This way, we get this idiomatic code:

[S ~[]E, E any]
  • 1

Type inference

With type parameters, we need to pass type parameters, which may lead to verbose code. Back to our general GMin function: