If you have been paying attention to the design and implementation of Go generic type, you must know that Go generic code implementation is implemented by type parameter (type parameter), which is replaced by type argument (type argument) when running generic code. (unfortunately both parameter and argument are translated into Chinese parameters)

The type parameter also has a type, which is the metadata that describes the behavior of the parameter type, and is called a constraint. The most general constraint is the built-in any type, which represents an arbitrary type.

1
2
3
4
5
func Print[T any](s []T) {
	for _, v := range s {
		fmt.Println(v)
	}
}

In Go generic design, constraints are implemented through interface types ( interface ). Because the interface type is similar to the function black of the constraint, that is, the constraint that qualifies the type argument must implement the type parameter (method set). Of course, in order to achieve the function of generalization, in addition to the method set, Go also used as a constraint to do the extension of the interface, the definition of the concept of type set ( type set ), for example, the following is a constraint on behalf of a type argument can be int, int8, int16, int32 or int64 type, is and ( union ) of the relationship so use the | notation.

1
2
3
type Signed interface {
	int | int8 | int16 | int32 | int64
}

Further, Go also defines the ~ notation, which means that as long as the underlying types are all of a particular type, it’s okay, so the above example can be written in a more generic way

1
2
3
type Signed interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}

So that instances of type MyInt defined by type MyInt int also satisfy this constraint.

constraints package

Go’s current implementation adds a new package, called constraints, to define built-in constraints, such as the common:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type Signed interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}
type Unsigned interface {
	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
type Integer interface {
	Signed | Unsigned
}
type Float interface {
	~float32 | ~float64
}
type Complex interface {
	~complex64 | ~complex128
}
type Ordered interface {
	Integer | Float | ~string
}

Even Russ Cox, Ian Lance Taylor, and their proposed and discussed adding the necessary constraints for slice, map, and chan, since they are so commonly used and available in the standard library. (#47203, #47319, 47330#).

1
2
3
4
5
6
7
8
9
type Slice[Elem any] interface {
	~[]Elem
}
type Map[Key comparable, Val any] interface {
	~map[Key]Val
}
type Chan[Elem any] interface {
	~chan Elem
}

Rob Pike recently submitted a new issue, suggesting not to add generic support to the standard library in Go 1.18 #48918. This is a very pertinent suggestion from the guru, suggesting that related cry changes be added to the extensions library first (x/exp), and then added to the standard library when mature, which was endorsed by many Gophers. This is another topic.

The package constraints defines common constraints that can be good for our development, but do you feel a bit unusual?

Omit interface

Yes, according to the Go generic specification, we have to define a constraint before we can use it in generic types and generic methods. Compared to the generic definitions in other languages, do you feel that this has the flavor of being redundant with your pants down?

Look at the definition of Slice, Map, and Chan above, isn’t it redundant? Why can’t we just use ~[]Elem, ~map[Key]Val, ~chan Elem directly in the definition of generic types and methods?

So fzipp proposes that for a non-interface type, the default equivalence is a constraint #48424, which is well described by the following formula.

1
[T nonInterfaceType]  [T interface{~nonInterfaceType}]

In the definition of a generic type, the non-interface type nonInterfaceType is equivalent to the constraint interface{~nonInterfaceType} , for example ~int is equivalent to interface{~int} . This allows us to omit the constraints package. This offer was accepted by North and the relevant functionality was added to the go master branch.

mattn’s Go generic example of converting a shaped array to a chan example (which I slightly changed to a more authentic Go writing):

 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
26
package main
import (
	"constraints"
	"context"
	"fmt"
)
func makeChan[T constraints.Chan[E], E any](ctx context.Context, arr []E) T {
	ch := make(T)
	go func() {
		defer close(ch)
		for _, v := range arr {
			select {
			case <-ctx.Done():
				return
			case ch <- v:
			}
			
		}
	}()
	return ch
}
func main() {
	for v := range makeChan(context.Background(), []int{1, 2, 3}) {
		fmt.Println(v)
	}
}

Here constraints.Chan[E] is used to represent a generic channel, but now there is an easier way:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main
import (
	"context"
)
func makeChan[T chan E, E any](ctx context.Context, arr []E) T {
	ch := make(T)
	go func() {
		defer close(ch)
		for _, v := range arr {
			select {
			case <-ctx.Done():
				return
			case ch <- v:
			}
			
		}
	}()
	return ch
}

Is it convenient to use chan E directly?

chan E implicitly stands for interface {chan E} , which is simpler to use and doesn’t require additional interface (constraint) definitions.

Although Go 1.18 is approaching, I feel that the development work of Go generic is getting heavier and heavier, and there are even some unclear places, so bless it and hope that it will be launched smoothly.