After several years of work, the generic feature is finally going to follow the 1.18 release. This is a milestone. Considering that the original design document of Go generic is rather difficult and has a messy structure, I’ll compile my understanding into a document and share it with you today. Since there is a lot of content and my own understanding of the English design document is limited (especially the type derivation part), mistakes are inevitable. I welcome your readers to criticize and correct me by leaving comments.

Type parameters

Generic is called type parameters in Go. As we know, Go is a statically strongly typed language. We need to specify explicit types for variables when we write code. Generic programming, on the other hand, is exactly writing code that can be adapted to different types, so we need a way to describe different types.

The following is an example from the Go language generic design scheme.

1
2
3
4
5
6
7
// Print 打印切片 s 的所有元素。
// 此方法可以打印任意类型的切片成员。
func Print(s []T) {
	for _, v := range s {
		fmt.Println(v)
	}
}

The T table is the type of the slice member, but the real internal type of T is not determined at the time of defining Print() and needs to be specified when the function is called. That is, we need to pass in an additional special parameter to specify the specific type of T when we call the Print() function. This special parameter is called a type parameter.

Since the function Print() needs to receive type parameters, it has to declare the type parameters it needs. Thus, the syntax for declaring type parameters is

1
2
3
4
5
// Print 打印切片 s 的所有元素。
// 该函数定义了一个类型参数 T,并且使用 T 作为入参切片 s 的元素类型
func Print[T any](s []T) {
	// 同上
}

Go inserts a set of square brackets between the original function name and the list of function arguments to indicate type arguments. As with function arguments, we need to specify a “type” for each type argument, which the Go language calls a constraint. We’ll analyze this in more detail below. For now, all you need to know is that any is a special constraint that indicates that the corresponding type parameter can accept any type, i.e., there is no constraint.

All type arguments need to be specified at the time of the function call, so we need another syntax, the example of which is as follows.

1
2
3
4
5
6
7
// 调用 Print 打印 []int{1,2,3}
// 因为切片 s 的成员类型为 int,所以需要指定 T 的值为 int
Print[int]([]int{1,2,3})
// 输出
// 1
// 2
// 3

When calling the function Print(), the Go language requires that square brackets be inserted before the function name and the argument list, and that the actual type of the type argument be specified in the square brackets. In the above example, since the actual type of the entry s is []int, the type int needs to be passed to the type parameter T. If you want to print a floating-point slice, you can.

1
2
3
4
5
Print[float64]([]float64{0.1,0.2,0.3})
// 输出
// 0.1
// 0.2
// 0.3

The type parameter can be used not only to declare the type of the function entry, but also to declare the type of the entry, e.g.

1
2
3
4
// Pointer 返回任意参数的指针。
Pointer[T any](t T) *T {
	return &t
}

Use as follows.

1
2
Pointer[int](1) // 返回 *int 类型,指向的值为 1
Pointer[float64](0.1) // 返回 *float64 类型,指向的值为 0.1

A generic function can declare multiple type parameters, e.g.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Must2 接受 t1, t2 和 err 三个参数,如果 err 不为空,则 panic
// 否则返回 t1 和 t2。
// 多类型参数的约束语法跟普通函数的参数类型相同。
func Must2[T1, T2 any](t1 T1, t2 T2, err error) (T1, T2) {
	if err != nil {
		panic(err)
	}
	return t1, t2
}
// 假设我们需要调用 foo 函数
func foo() (int, float64, error)
// 如果 foo 函数返回 err 则会 panic。
i, f := Must2[int, float64](foo())

In addition to generic functions, the Go language supports declaring type parameters in type definitions. For example.

1
2
3
// Vector 是一个切片,其元素的类型由类型参数 T 确定。
// T 的实际类型需要在声明 Vector 对象的时候指定。
type Vector[T any] []T

If we need to save the int element, we can define it like this.

1
var v Vector[int] // v 的类型为 []int

Type definitions that declare type parameters are called Generic Types. For generic types, we can also define generic methods, e.g.

1
2
// Push 向容器尾部追加新元素
func (v *Vector[T]) Push(x T) { *v = append(*v, x) }

Since the type parameter is already specified when declaring v, the call to the function Push() eliminates the need to pass in the type parameter.

1
2
3
var v Vector[int]
v.Push(1) // 不需要指定类型参数
fmt.Println(v) // 输出 [1]

Both generic functions and generic types require specific type parameters to be passed in at the time of use. This is natural, yet cumbersome. For example.

1
2
Print[int]([]int{1,2,3})
Print[float64]([]float64{0.1,0.2,0.3})

For the convenience of developers who use generic functions or types, the Go language supports derivation (inference)** of the actual types of arguments by the actual types of the arguments passed in!

So, the above function call can be abbreviated as follows

1
2
3
Print([]int{1,2,3}) // 推导出 T 的实际类型为 int
Println([]float64{0.1,0.2,0.3}) // 推导出 T 的实际类型为 float64
Pointer(0.1) // 推导出 T 的实际类型为 float64

The generic type can also be derived.

1
v := Vector{1} // 推导出 T 的实际类型为 float64

With generic derivation, developers can use generic functions and generic types just like normal functions or types, simply and clearly. This is remarkable design. But generic derivation is very complex, and we’ll cover it in detail later in this article.

By now, we’ve covered the most significant generic syntax of the Go language. A brief summary is as follows.

  • Functions can define type arguments: func F[T any](p T) { ... } .
  • Types can be specified using type parameters in the argument declaration and in the function body.
  • Type parameters can also be specified in type definitions: type M[T any] []T .
  • Type parameters must specify type constraints: func F[T Constraint](p T) { ... } .
  • Using a generic function or type requires passing a type parameter.
  • The use of generics can be simplified by reducing the number of specified type arguments through generic derivation.

We discuss the constraints and generic derivation of generics in detail below.

Generic Constraints

We covered in the previous section that type constraints need to be specified for all type parameters. We also introduced that any represents a special constraint that accepts all types.

So why do type parameters need constraints? Look at the following example.

1
2
3
4
5
6
7
// Stringify 将任意类型的切片转化成对应的字符串切片
func Stringify[T any](s []T) (ret []string) {
	for _, v := range s {
		ret = append(ret, v.String()) // 错误
	}
	return ret
}

Here the type parameter T is bound to any, so the elements of the slice s can be of any type. This means that the elements of s can have no String() method. For example, let’s try to execute.

1
Stringify[int]([]int{1,2,3})

At this point, the actual type of T is int, so the type of s is []int, and thus the type of v is int. Obviously, int has no String() method and is bound to report an error!

So, for the Stringify function, we need to restrict the scope of the type parameter T. Specifically, we can only pass T types that have a String() method.

As another example.

1
2
3
4
5
6
7
// Max 返回两者中比较大的值
func Max[T any](a, b T) T {
	if a > b { // 错误
		return a
	}
	return b
}

If we want to determine the maximum of two integers, we can

1
m := Max(1, 2) // 返回 2

But what if we want to compare two plurals?

1
m := Max(1+2i, 3+4i)

Such a call will report an error. Why? Because complex numbers cannot be compared in size, so in Go complex(64|128) does not support the comparison operator!

So, for the Max function, we also need to restrict the range of T values. Specifically, we can only pass T types that support the > operation.

This is the reason why we need to specify constraints on generic types. We need generic constraints to qualify the functions and operators supported by the type parameter.

One thing to be clear, however, is that adding a generic constraint does not mean that a compilation error will no longer be reported. If you pass the wrong type when calling a generic function, you will still get an error, but the compiler will explicitly report the error at the very beginning of the call, not when you get to the corresponding function body. If there is no constraint, then the compilation error reporting hierarchy can be very deep and extremely unfriendly for developers to troubleshoot errors (see C++’s template error reporting).

If a type constraint does not restrict the functions implemented by the type and also does not restrict the operators supported by the type, then it means that the corresponding type argument can accept any type. This particular constraint is any.

The Go language can already use interface to restrict the methods that need to be implemented. If you want to support all objects, you can use the infamous interface{}. Because interface{} is so stinky and long, Go has officially introduced the any keyword to replace it. And we can now use any to replace interface{} in non-generic code, so we can look forward to that.

The Go team took a number of factors into account and decided to extend the existing interface to support restricted operators when used as a generic constraint. The syntax is discussed in the following sections.

Before we dive into the discussion of generic constraints, we need to talk about the any constraint.

any constraint

Because there are no restrictions on types, we can only write code with a syntax that is supported by all types: the

  • declare or define variables
  • Assign values to each other before variables of the same type
  • Use as a parameter or return value of a function
  • Get the address of the corresponding variable
  • Convert the corresponding variable to interface{} or assign it to a variable of type interface{}.
  • Convert variables of type interface{} to variables of the corresponding type: t, ok := v.(T)
  • Use the corresponding type in the switch type enumeration: switch v.(type) { case T: /* ... */ }
  • Construct composite types, such as []T
  • pass to some built-in function, e.g. p := new(T)

So any is not as free as you want it to be, there is no absolute freedom.

Functions Constraints

Back to the Stringify generic function above

1
2
3
4
// Stringify 将任意类型的切片转化成对应的字符串切片
func Stringify[T any](s []T) (ret []string) {
	// 同上
}

We want all values of the type parameter T to implement the String() string function, so we can.

1
2
3
4
5
6
// Stringer 是泛型类型约束,要求所有类型都需要实现 String 方法。
// 所以在泛型代码中可以调用该类型变量的 String 方法。
// String 方法返回变量的字符串表示。
type Stringer interface {
	String() string
}

From form to content there is no difference from a normal interface. We can then modify the Stringify function to

1
2
3
4
5
6
7
// Stringify 将任意类型的切片转化成对应的字符串切片
func Stringify[T Stringer](s []T) (ret []string) {
	for _, v := range s {
		ret = append(ret, v.String()) // 正确
	}
	return ret
}

At this point, the following code will report a compile error.

1
Stringify([]int{1,2,3})

Because the int type does not implement the String() string method, it cannot be passed to the type parameter T.

Instead, the following method will work properly.

1
2
3
4
type MyInt int
func (i MyInt) String() string { return strconv.Itoa() }

Stringify(MyInt{1,2,3}) // 返回 []string{"1","2","3"}

Instead of declaring the Stringer interface separately, we can also write it as follows

1
2
3
func Stringify[T inference{ String() string }](s []T) (ret []string) {
	// 同上
}

The effect is the same. If we don’t want to put any restrictions on the scope of T, we can write it as follows

1
func Stringify[T interface{}](s []T) { ... }

Isn’t that a lot harder to read than func Stringify[T any](s []T) { ... } is a lot harder to read?

Of course, we can also use someone else’s well-defined interface to restrict the type parameters, e.g.

1
2
3
func Stringify[T fmt.Stringer](s []T) (ret []string) {
	// 同上
}

The above is the main content of the function constraint, the following we discuss the operator constraints.

Operators Constraints

Returning to the previous example.

1
2
3
4
5
6
7
// Max 返回两者中比较大的值
func Max[T any](a, b T) T {
	if a > b { // 错误
		return a
	}
	return b
}

The function Max requires that all Ts need to support the > operator. How can we express this constraint? One way is to convert all operator operations into function calls, so that we can use the interface to constrain operator operations. But this approach is very complicated to implement. In the end, the Go language officially chose a less elegant but very easy to implement approach: type collections.

The Go language does not allow overloading operators. That is, only Go’s built-in objects can support operator operations. We can’t declare a struct and then try to compare the size of the corresponding variables using >. This makes it easier to restrict the operators to objects. Since the built-in types are limited, we can enumerate all the supported types.

If we want to restrict the range of type arguments to all Go’s built-in signed integers, we can.

1
2
3
4
// PredeclaredSignedInteger 只能匹配内置的有符号整数类型
type PredeclaredSignedInteger interface {
	int | int8 | int16 | int32 | int64
}

So PredeclaredSignedInteger only allows passing in the five built-in signed integer types, passing in any other type will report a compilation error.

We know that the Go language allows redefining its own types, e.g.

1
type MyInt8 int8

Although the underlying type of MyInt8 is still int8, MyInt8 does not match the PredeclaredSignedInteger constraint. But MyInt8 supports exactly the same operator operations as int8, so we need a syntax to indicate that we can match both int8 and MyInt8. So the Go language introduced the concept of Approximation Constraint, with the following syntax.

1
2
3
4
// Int8 匹配所有底层类型为 int8 的类型
type Int8 interface {
	~int8
}

Note that ~ is added here before int8 to indicate an approximate match. This is fine as long as the underlying type is int8. So MyInt8 can match the Int8 constraint.

With the approximation constraint, our expressiveness is instantly taken to the next level. All types that support comparison operators can be written as the following constraint.

1
2
3
4
5
6
7
8
// Ordered 限制所有支持比较运算符的类型。
// 也就是说符合条件的类型都支持 <, <=, >, 和 >= 运算符。
type Ordered interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
		~float32 | ~float64 |
		~string
}

We can rewrite the Max function above as follows

1
2
3
4
5
6
7
// Max 返回两者中比较大的值
func Max[T Ordered](a, b T) T {
	if a > b { // 正确
		return a
	}
	return b
}

This time we execute Max(1+2i, 3+4i) again and it will trigger a compilation error.

Composite Constraints

In a generic constraint, we can enumerate not only possible basic or approximate types by |, but also other constraints. I personally call this a composite constraint.

Concatenation Constraints

For example, the constraint that matches all signed integers is

1
2
3
4
// SignedInteger 匹配所有有符号整数类型
type SignedInteger interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}

The constraint to match all unsigned integers is

1
2
3
4
// UnsignedInteger 匹配所有无符号整数类型
type UnsignedInteger interface {
	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

So the constraint to match all integers can be abbreviated as

1
2
3
4
// Integer 匹配所有整数类型
type Integer interface {
	SignedInteger | UnsignedInteger
}

The essence here is to use | to denote the relation of a concatenation, the result of which is that Integer can match the concatenation of all results matched by SignedInteger and UnsignedInteger.

Intersection Constraints

We can represent the intersection of two or more constraints, e.g.

1
2
3
4
5
// StringableInteger 匹配所有实现了 String() 方法的整数类型
type StringableInteger interface {
	Integer
	Stringer
}

Here we embed two constraints Integer and Stringer in the StringableInteger constraint, representing the intersection of the two matching results. The types that conform to this constraint are not only integers in the underlying type, but also implement the String() string method.

We can also just list the corresponding types and the list of functions that need to be implemented, e.g.

1
2
3
4
5
// StringableSignedInteger 匹配所有实现 String 方法的有符号整数类型
type StringableSignedInteger interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
	String() string
}

For some simple usage scenarios, we can even omit the interface keyword. For example.

1
type Max[T interface{int|uint}](a, b T) { ... }

It can be directly simplified to.

1
type Max[T int|uint](a, b T) { ... }

Generic Constraints

The Go language also supports declaring type parameters in constraints! For example.

1
2
3
4
// SliceConstraint 匹配所有类型为 T 的切片,但 T 的类型需要是使用的时候指定!
type SliceConstraint[T any] interface {
	[]T
}

We need to specify specific type parameters for the constraints when we use them, e.g.

1
2
3
4
// Map 接受一个切片对象和一个转换函数。
// Map 声明了两个类型参数 S 和 E,其中 S 的约束为 SliceConstraint。
// SliceConstraint 声明了类型参数 T,Map 将 T 转成 E,最终 S 的实际约束为 []E。
func Map[S SliceConstraint[E], E any](s S, f func(E) E) S { ... }

This example seems very complicated and redundant. It could have been written like this.

1
func Map[S []E, E any](s S, f func(E) E) S { ... }

In fact, it is not. This involves the problem of generic derivation, which we will explain in detail later.

In generic constraints, we can also declare self-referential constraints. For example, the following example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Equaler 限制类型必须实现 Equal 方法,但参数 T 需要在使用的时候指定。
type Equaler[T any] interface {
        Equal(v T) bool
}

// Index 从切片 s 中查找元素 e 的索引。
// 类型参数 T 的约束为 Equaler[T],需要实现 Equal(v T) bool 方法。
// 这里在声明 T 的约束的时候又用到了 T 本身。
func Index[T Equaler[T]](s []T, e T) int {
        for i, v := range s {
                if e.Equal(v) {
                        return i
                }
        }
        return -1
}

Mutual Constraints

The Go language not only supports defining type parameters in constraints, but also supports cross-referencing of constraints. The purpose is to solve more complex problems in practice, such as graph theory problems.

Taking graph theory as an example, if we want to write a series of graph theory algorithms, then we need both Edge and Node types.

  • The Node type needs to implement the Edges() []Edege method
  • The Edge type needs to implement the Nodes() (Edge, Edge) method

A graph can be represented as a []Node. This is enough to implement the graph theory algorithm. The following code is a bit more cerebral, so please read it carefully against the comments.

 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
// NodeConstraint 是一个简单的约束,要求被约束的类型一定要实现 Edges 方法。
// 但是 Edges 方法返回的 []Edge 类型为 Edge,没有确定,
// 需要在使用 NodeConstraint 的时候指明。
type NodeConstraint[E any] interface {
	Edges() []E
}

// EdgeConstraint 也是一个简单的约束,要求被约束的类型一定要实现 Nodes 方法。
// 同样 Nodes 方法返回的 from t to 类型为 Node,没有确定,
// 需要在使用 EdgeConstraint 的时候指明。
type EdgeConstraint[N any] interface {
	Nodes() (from, to N)
}

// Graph 为泛型类型,声明了两个类型变量为 Node 和 Edge。
// Node 类型必须满足 NodeConstraint 约束,并且指定了 NodeConstraint 中 E 的类型为 Edge。
// 所以 Node 类型必须实现 NodeConstraint 中规定的 Edges 方法,返回 []Edge。
// 同理,Edge 类型必须实现 EdgeConstraint 中规定的 Nodes 方法,返回 (from, to Node)。
type Graph[Node NodeConstraint[Edge], Edge EdgeConstraint[Node]] struct { ... }

// New 方法通过传入一组 []Node 构造 Graph 对象
func New[Node NodeConstraint[Edge], Edge EdgeConstraint[Node]] (nodes []Node) *Graph[Node, Edge] { ... }

// ShortestPath 查询图中两点之间的最短路径
func (g *Graph[Node, Edge]) ShortestPath(from, to Node) []Edge { ... }

The above is only the declaration part, continue to see the calling part below. First, define the specific structure.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Vertex 表示图的顶点。
type Vertex struct { ... }

// Edges 返回连接该顶点的所有边。
func (v *Vertex) Edges() []*FromTo { ... }

// FromTo 表示图的边。
type FromTo struct { ... }

// Nodes 返回边的两个端点
func (ft *FromTo) Nodes() (*Vertex, *Vertex) { ... }

The graph algorithm is then called.

1
2
g := New[*Vertex, *FromTo]([]*Vertex{...})
edges := g.ShortestPath(a, b)

Now let’s analyze the initialization process of the type parameters.

First, the type parameters Node and Edge of Graph are replaced with *Vertex and *FromTo, respectively. Then the compiler starts checking for type constraints. For the Node type, the constraint is NodeConstraint[*FromTo], so the Edges() []*FromTo method needs to be implemented. And *Vertex does implement the Edges method. For the Edge type, its constraint is EdgeConstraint[*Vertex], so it needs to implement the Nodes() []*Vertex method, which is obviously also implemented by *FromTo. At this point, the constraint checking ends.

So, when we call g.ShortestPath(a, b), the type of edges is []*FromTo!

Some people may say that this way of writing is too complicated and brain-burning, it is perfectly possible to simplify NodeConstraint and EdgeConstraint to.

1
2
type NodeInterface interface { Edges() []EdgeInterface }
type EdgeInterface interface { Nodes() (NodeInterface, NodeInterface) }

But this would require modifying the function definitions of Vertex and FromTo.

1
2
func (v *Vertex) Edges() []EdgeInterface { ... }
func (ft *FromTo) Nodes() (NodeInterface, NodeInterface) { ... }

But the result is that calling the Edges function returns an abstract []EdgeInterface slice rather than a concrete []*FromTo list.

Built-in constraints

comparable

Earlier we said that the Go language only supports performing operator operations on built-in types. The exceptions are two operators, == and ! =.

These two operators allow comparing user-defined struct objects, so they need to be handled separately.

The inconsistency of Go’s design can be seen here. On the one hand, they don’t want to introduce operator overloading, which would greatly increase the complexity of the Go language; on the other hand, they have to support equality comparisons for struct types. To do so, they had to automatically insert the equality comparison function during compilation and then “overload” the == and ! = operators.

For this reason, the Go language introduces a separate comparable constraint for this two-operator.

That is, if we want the types to support equal comparisons, we can write it as follows.

1
2
3
4
5
6
7
8
9
// Index 查询元素 x 在切片 s 中的位置。
func Index[T comparable](s []T, x T) int {
	for i, v := range s {
		if v == x {
			return i
		}
	}
	return -1
}

constraints

For the convenience of developers, Go has a built-in constraints package that provides commonly used type constraints.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Signed 有符号整数类型
type Signed interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}
// Unsigned 无符号整数类型
type Unsigned interface {
	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
// Integer 整数类型
type Integer interface {
	Signed | Unsigned
}
// Float 浮点数类型
type Float interface {
	~float32 | ~float64
}
// Complex 复数类型
type Complex interface {
	~complex64 | ~complex128
}
// Ordered 支持排序的类型,即支持操作符:< <= >= >
type Ordered interface {
	Integer | Float | ~string
}

The above basically covers most of the contents of generic constraints, and we start discussing generic derivation below.

Generic derivation (Type inference)

We have briefly introduced generic inference earlier, and its purpose is to simplify the use of generic functions/types as much as possible and reduce unnecessary type parameter passing.

This section analyzes the functionality and design of generic derivation in detail. Before we start, let’s look at the effect of generic derivation.

1
2
3
// Map 对入参 s 切片中的每个元素执行函数 f,将结果保存到新的切片并返回。
// 类型参数 F 和 T 需要在调用的时候指定。
func Map[F, T any](s []F, f func(F) T) []T { ... }

The Go language supports type derivation in the following cases.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var s []int
f := func(i int) int64 { return int64(i) }
var r []int64

// 普通情形,指定全部类型参数,前文已经介绍过
r = Map[int, int64](s, f)

// 仅指定第一个类型参数,自动推导后面的类型参数
r = Map[int](s, f)

// 自动推导所有类型参数
r = Map(s, f)

If a generic function/type is used without specifying all type parameters, the compiler will try to derive the missing type. If the derivation fails, a compilation error is reported.

Go uses so-called type unification for type derivation. However, the original text is rather abstract, so this article focuses on concrete examples to illustrate how type derivation works.

Type unification

Assimilation is a comparison of two types to see if they are equivalent. Whether two types can be equivalent depends on.

  • Whether the knot structure is the same, e.g. []int is equivalent to []T (if T matches int), and not equivalent to map[int]T.
  • Whether the untyped variables have the same underlying type, e.g. map[T]int is not equivalent to map[T]string, but []MyInt is equivalent to []int.

For example, T1 and T2 are type parameters, and []map[int]bool can be assimilated to the following types.

  • []map[int]bool
  • T1 ( T1 matches []map[int]bool )
  • []T1 ( T1 matches map[int]bool )
  • []map[T1]T2 ( T1 matches int , T2 matches bool )

But []map[int]bool is not equivalent to the following types.

  • int
  • struct{}
  • []struct{}
  • []map[T1]string

Function argument type inference

The function argument inference is divided into two stages.

The first stage skips all real parameters without type constants matching once. If there are still type arguments that have not been determined, the second stage will begin. At this point, all untyped constants need to be set to their corresponding default types, and then matched again. The same type parameter may be matched more than once, and if the result of multiple matches does not match, a compilation error will be reported.

Returning to the example we mentioned earlier.

1
func Print[T any](s []T) { ...}

can be simplified to Print([]int{1,2,3}) . Since the type of T is not specified, the compiler performs type derivation.

The compiler compares the real reference type []int with the formal reference type []T . By definition of assimilation, the type of T can only be int, which gives the actual type of T.

So the final function call is Print[int]([]int{1,2,3}) .

To analyze a more complex example.

1
2
3
4
5
// Map 对入参 s 切片中的每个元素执行函数 f,将结果保存到新的切片并返回。
// 类型参数 F 和 T 需要在调用的时候指定。
func Map[F, T any](s []F, f func(F) T) []T { ... }

strs := Map([]int{1, 2, 3}, strconv.Itoa)

The derivation process is as follows.

  • Compare []int and []F and infer that F is of type int
  • compare strconv.Itoa(int) string and func(F) T, inferring that F is int and T is string (F is matched twice, but both are int)
  • The final inferred call is Map[int,string]([]int{1,2,3}, strconv.Itoa)

All of the above entries have explicit types, and the cases where the entry has or does not have a type constant are discussed below.

1
2
// NewPair 返回 Pair 对象指针,包含两个相同类型的值。
func NewPair[F any](f1, f2 F) *Pair[F] { ... }

For NewPair(1,2).

The first stage skips all untyped constants, so the type of T is not deduced. The second stage sets the default type to int for 1 and 2. So the type parameter F corresponds to int and the function call is inferred as NewPair[int](1,2).

For NewPair(1,int64(2)).

The first stage of the derivation ignores the untyped constant 1. Since the second argument is of type int64, it is inferred that the argument to F is int64. So the final function call is NewPair[int64](1,2) .

For NewPair(1,2.5).

The first stage of the derivation ignores untyped constants. The second stage first sets 1 and 2.5 to default types int and float64. Then match from left to right. For parameter 1 it is confirmed that F is int and for parameter 2.5 it is determined that F is float64. The two results are not the same, so an error is reported.

After the type derivation is complete, the compiler still performs constraint checks and parameter type checks.

Constraint type inference

We said earlier that type parameter constraints can also use type parameters. For such structured constraints, we can infer their actual constraints from other type parameters or constraints.

The derivation algorithm is also rather verbose, so here are a few examples.

Example of element type constraint

Suppose we have the following functions.

1
2
3
4
5
6
7
8
// Double 返回新切片,每个元素都是 s 对应元素的两倍。
func Double[E constraints.Number](s []E) []E {
	r := make([]E, len(s))
	for i, v := range s {
		r[i] = v + v
	}
	return r
}

With the above definition, if the function is called like the following.

1
2
type MySlice []int
var v1 = Double(MySlice{1})

The derived type of v1 is actually []int, not MySlice as we would like. Because the compiler replaced MySlice with the underlying type []int when comparing MySlice and []E, it deduces that E is int.

In order for Double to return the type MySlice normally, we rewrite the function as follows

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// SC 限定类型必须为元素类型为 E 的切片。
type SC[E any] interface {
	[]E
}

func DoubleDefined[S SC[E], E constraints.Number](s S) S {
	r := make(S, len(s))
	for i, v := range s {
		r[i] = v + v
	}
}

The calling code needs to be changed to look like this.

1
var v2 = DoubleDefined[MySlice, int](MySlice{1})

We can also let the compiler automatically derive the type.

1
var v3 = DoubleDefined(MySlice{1})

First, the compiler performs a function argument type derivation. At this point it is necessary to compare MySlice with S, but S uses structural constraints, so its actual constraint type needs to be derived.

To do this, the compiler constructs a mapping table.

1
{S -> MySlice}

Then, the compiler expands the S constraint, expanding SC[E] into []E. Since we previously documented the mapping of S to MySlice, we can compare []E to MySlice. And since the underlying type of MySlice is []int, it follows that the type of E is int.

1
{S -> MySlice, E -> int}

Then, we replace all the Es in the constraint with ints to see if there are any more indeterminate type parameters. There are none left, so the derivation is over. So the original call is deduced as

1
var v3 = DoubleDefined[MySlice,int](MySlice{1})

The result returned is still MySlice.

Example of pointer method constraints

Suppose we wish to convert a set of strings into a set of other types of data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Setter 限制类型需要实现 Set 方法,通过 string 设置自身的值。
type Setter interface {
	Set(string)
}

// FromStrings 接受字符串切片,返回类型为 T 的切片。
// 返回切片中每个元素的值通过调用其 Set 方法设置。
func FromStrings[T Setter](s []string) []T {
	result := make([]T, len(s))
	for i, v := range s {
		result[i].Set(v)
	}
	return result
}

Let’s look at the calling code (which is faulty and does not compile).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Settable 是可以从字符串设置自身取值的整数类型。
type Settable int

// Set 将字符串解析成整数并赋给 *p。
func (p *Settable) Set(s string) {
	i, _ := strconv.Atoi(s) // 实际代码不能忽略报错
	*p = Settable(i)
}

// 调用函数,无法编译
nums := FromStrings[Settable]([]string{"1", "2"})

The above code does not compile properly. The reason is that we specified the type T as Settable, but the type Settable does not implement the Set(string) method. It is type *Settable that implements that method.

So we change the calling code to.

1
2
// 调用函数,正常编译,但运行报错
nums := FromStrings[*Settable]([]string{"1", "2"})

This time it compiles fine, but running the code gives an error again. This is because in FromStrings, result[i] is of type *Settable and the value is nil, so the assignment *p = *Settable(i) cannot be performed.

So, for the FromStrings defined above, we can neither set T to Settable, which would lead to a compilation error, nor to *Settable, which would lead to a runtime error.

To implement FromStrings, we need to specify both Settable and *Settable types, which requires the structuring constraint.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Setter2 限制类型必须是 B 的指针而且要实现 Set 方法。
type Setter2[B any] interface {
	Set(string)
	*B
}

// FromStrings2 接受字符串切片,返回 T 切片。
//
// 这里定义了两个类型参数,所以才能在返回 T 切片的同时
// 调用 *T 也就是 PT 的方法。
// Setter2 约束可以确保 PT 是 T 的指针。
func FromStrings2[T any, PT Setter2[T]](s []string) []T {
	result := make([]T, len(s))
	for i, v := range s {
		// &result[i] 类型是 *T,也就是 Setter2 的类型。
		// 所以可以将其强转为 PT。
		p := PT(&result[i])
		// PT 实现了 Set 方法
		p.Set(v)
	}
	return result
}

The calling code is as follows.

1
nums := FromStrings2[Settable, *Settable]([]string{"1", "2"})

Repeating Settable twice feels a bit silly and can be simplified to

1
2
// 因为函数入参中没有使用 T,所以无法进一步简化
nums := FromStrings2[Settable]([]string{"1", "2"})

The whole process of compiler derivation is like this. First, the mapping table is constructed from the known types.

1
{T -> Settable}

Then replace T with Settable and expand all structured parameters. So the type Setter2[T] of PT is expanded to *T and added to the mapping table.

1
{T -> Settable, PT -> *T}

Then replace all T with Settable to get.

1
{T -> Settable, PT -> *Settable}

This is the end of the derivation, the actual function call is FromStrings2[Settable,*Settable]([]string{"1", "2"}) .

Constraint checks after derivation
1
2
3
4
5
// Unsettable 也是 int,但没有实现 Set 方法。
type Unsettable int

// 错误调用
nums := FromString2[Unsettable]([]string{"1", "2"})

In this example, the compiler can derive the type of PT as *Unsettable. After the derivation, the compiler continues to check the Setter2 constraint. But *Unsettable does not implement the Set method, so it will report a compilation error.

Summary

The above is the main content of Go generic design, the main points are as follows.

  • Functions can define type parameters: func F[T any](p T) { ... } .
  • Types can be specified in parameter declarations and function bodies using type parameters.
  • Type parameters can also be specified in type definitions: type M[T any] []T .
  • Type parameters must specify type constraints: func F[T Constraint](p T) { ... } .
  • Using generic functions or types requires specifying type parameters.
  • Specifying type parameters can be reduced by generic derivation, simplifying the use of generics.