248

sequence

On March 15, 2022, the controversial but highly anticipated generics was finally released with Go 1.18.

However, because the time span of Go's support for generics is too long, there are many articles with "generics" as the keyword that introduce old generics proposals or designs before Go1.18, and many designs ended up in Go1. 18 was obsolete or changed. And many articles (including official ones) that introduce generics in Go1.18 are too simple, they do not give a complete introduction to generics in Go, and do not let everyone realize how much complexity the introduction of generics in Go has added to the language. (Of course, it may also be that I just didn't find a better article)

For these reasons, I decided to refer to The Go Programming Language Specification to write a more complete and systematic introduction to generics in Go 1.18. This article may be one of the more comprehensive articles on Go Generics.

💡 This article strives to make Go's generics better understood by people who have not been exposed to generic programming, so the text may be a little verbose. But trust me, after reading this article you will get a very comprehensive understanding of Go Generics

1. Everything starts with the formal parameters and actual parameters of the function

Suppose we have a function that computes the sum of two numbers

 func Add(a int, b int) int {
    return a + b
}

This function is very simple, but it has a problem - it cannot calculate sums other than int. What if we want to compute the sum of floats or strings? One solution is to define different functions for different types like below

 func AddFloat32(a float32, b float32) float32 {
    return a + b
}

func AddString(a string, b string) string {
    return a + b
}

But is there any better way? The answer is yes, we can review the basic concept of formal parameters and arguments of functions:

 func Add(a int, b int) int {  
    // 变量a,b是函数的形参   "a int, b int" 这一串被称为形参列表
    return a + b
}

Add(100,200) // 调用函数时,传入的100和200是实参

We know that the formal parameters of a function are just placeholders and have no specific values. Only after we call the function and pass in the arguments , we have a specific value.

Then, if we generalize the concept of formal parameters and introduce the concept of similar formal parameters to the type of variables, the problem will be solved: here we call them type parameters and Type arguments , as follows:

 // 假设 T 是类型形参,在定义函数时它的类型是不确定的,类似占位符
func Add(a T, b T) T {  
    return a + b
}

In the above pseudocode, T is called a type parameter , it is not a concrete type, and the type is not determined when the function is defined. Because the type of T is not certain, we need to pass in the specific type when calling the function, just like the formal parameters of the function. So can't we support multiple different types in one function at the same time? The concrete type passed in here is called a type argument :

The following pseudocode shows how to pass type arguments when calling a function:

 // [T=int]中的 int 是类型实参,代表着函数Add()定义中的类型形参 T 全都被 int 替换
Add[T=int](100, 200)  
// 传入类型实参int后,Add()函数的定义可近似看成下面这样:
func Add( a int, b int) int {
    return a + b
}

// 另一个例子:当我们想要计算两个字符串之和的时候,就传入string类型实参
Add[T=string]("Hello", "World") 
// 类型实参string传入后,Add()函数的定义可近似视为如下
func Add( a string, b string) string {
    return a + b
}

By introducing the concepts of type parameters and type arguments , we give a function the ability to handle many different types of data, a programming style known as generic programming .

You may be wondering, can't I achieve such dynamic data processing through Go's interface + reflection? Yes, the functions that generics can achieve can basically be achieved through interface + reflection. But anyone who has used reflection knows that there are many problems with the reflection mechanism:

  1. troublesome to use
  2. Lost compile-time type checking, it is easy to make mistakes if not written carefully
  3. less than ideal performance

And when generics are applicable, it can solve the above problems. But this does not mean that generics are a panacea. Generics have their own applicable scenarios. When you are wondering whether to use generics, please remember the following experience:

If you often have to write the exact same logic for different types, then using generics will be the most appropriate choice

2. Go Generics

Through the above pseudo-code, we actually have the most preliminary and important understanding of Go's generic programming - type parameters and type arguments. Go1.18 also implements generics in this way, but simple formal parameters are far from being able to achieve generic programming, so Go also introduces many new concepts:

  • Type parameter
  • Type argument
  • Type parameter list
  • Type constraint
  • Instantiations
  • Generic type
  • Generic receiver
  • Generic function

Etc., etc.

Ah, it's too many concepts to get dizzy? It's okay, please follow me slowly, let's start with generic types

3. Type parameters, type arguments, type constraints, and generic types

Observe this simple example below:

 type IntSlice []int

var a IntSlice = []int{1, 2, 3} // 正确
var b IntSlice = []float32{1.0, 2.0, 3.0} // ✗ 错误,因为IntSlice的底层类型是[]int,浮点类型的切片无法赋值

A new type is defined here IntSlice , and its underlying type is []int . Of course, only slices of type int can be assigned to variables of type IntSlice .

Next, what if we want to define a slice that can hold float32 or string and other types of slices? Simple, define a new type for each type:

 type StringSlice []string
type Float32Slie []float32
type Float64Slice []float64

But the problem with doing this is obvious, they are all the same in structure, but the member types are different, so many new types need to be redefined. So is there a way to define only one type to represent all the above types? The answer is yes, then you need to use generics:

 type Slice[T int|float32|float64 ] []T

Different from the general type definition, the type name here Slice is followed by square brackets, and an explanation of each part is:

  • T is the Type parameter introduced above. When defining the Slice type, the specific type represented by T is uncertain, similar to a placeholder
  • int|float32|float64 This part is called Type constraint , the middle | means to tell the compiler that the type parameter T can only receive int or float32 or float64 type arguments
  • The whole string in square brackets T int|float32|float64 defines all the type parameters (in this example there is only one type parameter T), so we call it type parameter list (type parameter list)
  • The newly defined type name here is called Slice[T]

This type of type definition takes type parameters, which is obviously very different from ordinary type definitions, so we will use this

A type with a type parameter in a type definition is called a generic type**

Generic types cannot be used directly, they can only be used after passing in the type argument (Type argument) to determine it as a specific type. The operation of passing in type arguments to determine a specific type is called Instantiations :

 // 这里传入了类型实参int,泛型类型Slice[T]被实例化为具体的类型 Slice[int]
var a Slice[int] = []int{1, 2, 3}  
fmt.Printf("Type Name: %T",a)  //输出:Type Name: Slice[int]

// 传入类型实参float32, 将泛型类型Slice[T]实例化为具体的类型 Slice[string]
var b Slice[float32] = []float32{1.0, 2.0, 3.0} 
fmt.Printf("Type Name: %T",b)  //输出:Type Name: Slice[float32]

// ✗ 错误。因为变量a的类型为Slice[int],b的类型为Slice[float32],两者类型不同
a = b  

// ✗ 错误。string不在类型约束 int|float32|float64 中,不能用来实例化泛型类型
var c Slice[string] = []string{"Hello", "World"} 

// ✗ 错误。Slice[T]是泛型类型,不可直接使用必须实例化为具体的类型
var x Slice[T] = []int{1, 2, 3}

For the above example, we first give the generic type Slice[T] the type argument int , so that the generic type is instantiated as a concrete type Slice[int] , the type definition after being instantiated can be approximated as follows:

 type Slice[int] []int     // 定义了一个普通的类型 Slice[int] ,它的底层类型是 []int

We use the instantiated type Slice[int] to define a new variable a , which can store a slice of type int. Then we instantiate another type Slice[float32] in the same way, and create a variable b .

Because the variables a and b are specific different types (a Slice[int], a Slice[float32]), so a = b variable assignments between different types are not allowed.

At the same time, because the type constraint of Slice[T] restricts that it can only use int or float32 or float64 to instantiate itself, so Slice[string] it is wrong to use the string type to instantiate.

The above is just the simplest example. In fact, the number of type parameters can be far more than one, as follows:

 // MyMap类型定义了两个类型形参 KEY 和 VALUE。分别为两个形参指定了不同的类型约束
// 这个泛型类型的名字叫: MyMap[KEY, VALUE]
type MyMap[KEY int | string, VALUE float32 | float64] map[KEY]VALUE  

// 用类型实参 string 和 flaot64 替换了类型形参 KEY 、 VALUE,泛型类型被实例化为具体的类型:MyMap[string, float64]
var a MyMap[string, float64] = map[string]float64{
    "jack_score": 9.6,
    "bob_score":  8.4,
}

To revisit the various concepts using the example above:

  • KEY and VALUE are type parameters
  • int|string is the type constraint of KEY, float32|float64 is the type constraint of VALUE
  • KEY int|string, VALUE float32|float64 The entire string of text is called a type parameter list because it defines all parameters
  • Map[KEY, VALUE] is a generic type , the name of the type is called Map[KEY, VALUE]
  • The string and float64 in var a MyMap[string, float64] = xx are type arguments , which are used to replace KEY and VALUE respectively, and instantiate the specific type MyMap[string, float64]

Still a little dizzy? It's okay, there are indeed too many concepts at once, here is a simple picture to make it clear:

Go Generics Concepts at a Glance

3.1 Other generic types

All type definitions can use type parameters, so the following structure and interface definitions can also use type parameters:

 // 一个泛型类型的结构体。可用 int 或 sring 类型实例化
type MyStruct[T int | string] struct {  
    Name string
    Data T
}

// 一个泛型接口(关于泛型接口在后半部分会详细讲解)
type IPrintData[T int | float32 | string] interface {
    Print(data T)
}

// 一个泛型通道,可用类型实参 int 或 string 实例化
type MyChan[T int | string] chan T

3.2 Interoperability of type parameters

Type parameters can be applied to each other, as follows

 type WowStruct[T int | float32, S []T] struct {
    Data     S
    MaxValue T
    MinValue T
}

This example may seem complicated and difficult to understand, but in fact just remember one thing: any generic type must be instantiated with a type argument before it can be used. So let's try to pass in the type arguments and see:

 var ws WowStruct[int, []int]
// 泛型类型 WowStuct[T, S] 被实例化后的类型名称就叫 WowStruct[int, []int]

In the above code, we passed in the actual parameter int for T, and then because the definition of S is []T , the actual parameter of S is naturally []int . After instantiation, the definition of WowStruct[T,S] looks like this:

 // 一个存储int类型切片,以及切片中最大、最小值的结构体
type WowStruct[int, []int] struct {
    Data     []int
    MaxValue int
    MinValue int
}

Because the definition of S is []T, if T must be determined, the actual parameters of S cannot be passed randomly. The following code is wrong:

 // 错误。S的定义是[]T,这里T传入了实参int, 所以S的实参应当为 []int 而不能是 []float32
ws := WowStruct[int, []float32]{
        Data:     []float32{1.0, 2.0, 3.0},
        MaxValue: 3,
        MinValue: 1,
    }

3.3 Several syntax errors

  1. When defining a generic type, the underlying type cannot have only type parameters , as follows:

     // 错误,类型形参不能单独使用
    type CommonType[T int|string|float32] T
  2. An error will be reported when some types of type constraints are misinterpreted as expressions by the compiler. as follows:

     //✗ 错误。T *int会被编译器误认为是表达式 T乘以int,而不是int指针
    type NewType[T *int] []T
    // 上面代码再编译器眼中:它认为你要定义一个存放切片的数组,数组长度由 T 乘以 int 计算得到
    type NewType [T * int][]T 
    
    //✗ 错误。和上面一样,这里不光*被会认为是乘号,| 还会被认为是按位或操作
    type NewType2[T *int|*float64] []T 
    
    //✗ 错误
    type NewType2 [T (int)] []T

    In order to avoid this misunderstanding, the solution is to add the type constraint package interface{} or add a comma to disambiguate (the specific usage of the interface will be mentioned in the second half)

     type NewType[T interface{*int}] []T
    type NewType2[T interface{*int|*float64}] []T 
    
    // 如果类型约束中只有一个类型,可以添加个逗号消除歧义
    type NewType3[T *int,] []T
    
    //✗ 错误。如果类型约束不止一个类型,加逗号是不行的
    type NewType4[T *int|*float32,] []T

    Because the usage of the comma above is relatively limited, it is recommended to use interface{} to solve the problem uniformly.

3.4 Special generic types

A special kind of generic type is discussed here, as follows:

 type Wow[T int | string] int

var a Wow[int] = 123     // 编译正确
var b Wow[string] = 123  // 编译正确
var c Wow[string] = "hello" // 编译错误,因为"hello"不能赋值给底层类型int

Although the type parameter is used here, because the type definition is type Wow[T int|string] int , no matter what type parameter is passed in, the underlying type of the instantiated new type is int. So the number 123 of type int can be assigned to variables a and b, but the string "hello" of type string cannot be assigned to c

This example doesn't make much sense, but it allows us to understand the mechanics of the instantiation of generic types

3.5 Nesting dolls of generic types

Generics, like ordinary types, can be nested within each other to define more complex new types, as follows:

 // 先定义个泛型类型 Slice[T]
type Slice[T int|string|float32|float64] []T

// ✗ 错误。泛型类型Slice[T]的类型约束中不包含uint, uint8
type UintSlice[T uint|uint8] Slice[T]  

// ✓ 正确。基于泛型类型Slice[T]定义了新的泛型类型 FloatSlice[T] 。FloatSlice[T]只接受float32和float64两种类型
type FloatSlice[T float32|float64] Slice[T] 

// ✓ 正确。基于泛型类型Slice[T]定义的新泛型类型 IntAndStringSlice[T]
type IntAndStringSlice[T int|string] Slice[T]  
// ✓ 正确 基于IntAndStringSlice[T]套娃定义出的新泛型类型
type IntSlice[T int] IntAndStringSlice[T] 

// 在map中套一个泛型类型Slice[T]
type WowMap[T int|string] map[string]Slice[T]
// 在map中套Slice[T]的另一种写法
type WowMap2[T Slice[int] | Slice[string]] map[string]T

3.6 Two options for type constraints

Observe how the following two types of constraints are written

 type WowStruct[T int|string] struct {
    Name string
    Data []T
}

type WowStruct2[T []int|[]string] struct {
    Name string
    Data T
}

Limited to this example, these two ways of writing and implementing are actually similar, and the structure is the same after instantiation. But in the following cases, we use the former way of writing it will be better:

 type WowStruct3[T int | string] struct {
    Data     []T
    MaxValue T
    MinValue T
}

3.7 Anonymous structs do not support generics

We sometimes often use anonymous structs and initialize them directly after defining the anonymous struct:

 testCase := struct {
        caseName string
        got      int
        want     int
    }{
        caseName: "test OK",
        got:      100,
        want:     100,
    }

So can anonymous structs use generics? The answer is no, the following usage is wrong:

 testCase := struct[T int|string] {
        caseName string
        got      T
        want     T
    }[int]{
        caseName: "test OK",
        got:      100,
        want:     100,
    }

So when using generics, we can only give up the use of anonymous structures, which will cause trouble in many scenarios (the main trouble is when unit testing is concentrated, and it will be very troublesome to do unit testing for generics, which I will later will be explained in detail in the article)

4. Generic receiver

After reading the above example, you will definitely say that it introduces so many complex concepts, but it seems that generic types are useless at all?

Yes, mere generic types are actually not very useful for development. But if you combine the generic type with the generic receiver to be introduced next, the generic type will be very useful.

We know that after defining a new normal type, we can add methods to the type. So is it possible to add methods to generic types? The answer is naturally yes, as follows:

 type MySlice[T int | float32] []T

func (s MySlice[T]) Sum() T {
    var sum T
    for _, value := range s {
        sum += value
    }
    return sum
}

This example adds a method to calculate the sum of the members ---b783fcabcd6f1c9448046cb8fc5a96fa--- for the generic type MySlice[T] Sum() . Notice the definition of this method:

  • First look at the receiver (s MySlice[T]) , so we directly write the type name MySlice[T] into the receiver
  • Then we use the type parameter T * * for the return parameter of the method (actually, the receiving parameter of the method can also be a practical type parameter if necessary)
  • In the method definition, we can also use the type parameter T (in this example, we define a new variable ---4a4fa919e406797e93aa5116e59b5ecb--- by var sum T sum )

For this generic type MySlice[T] how do we use it? Remember what I said many times before, generic types need to be instantiated with type arguments anyway, so the usage is as follows:

 var s MySlice[int] = []int{1, 2, 3, 4}
fmt.Println(s.Sum()) // 输出:10

var s2 MySlice[float32] = []float32{1.0, 2.0, 3.0, 4.0}
fmt.Println(s2.Sum()) // 输出:10.0

How to understand the instantiation above? First we instantiate the generic type MySlice[T] with the type argument int, so all Ts in the generic type definition are replaced with int, and finally we can think of the code like this:

 type MySlice[int] []int // 实例化后的类型名叫 MyIntSlice[int]

// 方法中所有类型形参 T 都被替换为类型实参 int
func (s MySlice[int]) Sum() int {
    var sum int 
    for _, value := range s {
        sum += value
    }
    return sum
}

Instantiation with float32 is the same as instantiation with int, so I won't repeat it here.

With the generic receiver, the utility of generics has been greatly expanded all at once. Before generics, if we wanted to implement common data structures, such as heaps, stacks, queues, linked lists, etc., we had only two choices:

  • Write an implementation for each type
  • Using interface + reflection

With generics, we can create generic data structures very simply. Next, use a more practical example - queue to explain

4.1 Generic-based queues

Queue is a first-in, first-out data structure. It is the same as queuing in reality. Data can only be put in from the tail of the queue and taken out from the head of the queue. The data put in first is taken out first.

 // 这里类型约束使用了空接口,代表的意思是所有类型都可以用来实例化泛型类型 Queue[T] (关于接口在后半部分会详细介绍)
type Queue[T interface{}] struct {
    elements []T
}

// 将数据放入队列尾部
func (q *Queue[T]) Put(value T) {
    q.elements = append(q.elements, value)
}

// 从队列头部取出并从头部删除对应数据
func (q *Queue[T]) Pop() (T, bool) {
    var value T
    if len(q.elements) == 0 {
        return value, true
    }

    value = q.elements[0]
    q.elements = q.elements[1:]
    return value, len(q.elements) == 0
}

// 队列大小
func (q Queue[T]) Size() int {
    return len(q.elements)
}
💡 For the convenience of explanation, the above is a very simple implementation method of the queue, without considering many issues such as thread safety

Queue[T] Because it is a generic type, it must be instantiated if it is to be used. The instantiation and usage methods are as follows:

 var q1 Queue[int]  // 可存放int类型数据的队列
q1.Put(1)
q1.Put(2)
q1.Put(3)
q1.Pop() // 1
q1.Pop() // 2
q1.Pop() // 3

var q2 Queue[string]  // 可存放string类型数据的队列
q2.Put("A")
q2.Put("B")
q2.Put("C")
q2.Pop() // "A"
q2.Pop() // "B"
q2.Pop() // "C"

var q3 Queue[struct{Name string}] 
var q4 Queue[[]int] // 可存放[]int切片的队列
var q5 Queue[chan int] // 可存放int通道的队列
var q6 Queue[io.Reader] // 可存放接口的队列
// ......

4.2 Dynamically determine the type of variables

When using an interface, type assertion or type swith is often used to determine the specific type of the interface, and then different types are handled differently, such as:

 var i interface{} = 123
i.(int) // 类型断言

// type switch
switch i.(type) {
    case int:
        // do something
    case string:
        // do something
    default:
        // do something
    }
}

Then you must think, for valut T such variables defined by type parameters, can we judge the specific type and then treat different types differently? The answer is not allowed, as follows:

 func (q *Queue[T]) Put(value T) {
    value.(int) // 错误。泛型类型定义的变量不能使用类型断言

    // 错误。不允许使用type switch 来判断 value 的具体类型
    switch value.(type) {
    case int:
        // do something
    case string:
        // do something
    default:
        // do something
    }
    
    // ...
}

Although type switches and type assertions cannot be used, we can achieve this through reflection:

 func (receiver Queue[T]) Put(value T) {
    // Printf() 可输出变量value的类型(底层就是通过反射实现的)
    fmt.Printf("%T", value) 

    // 通过反射可以动态获得变量value的类型从而分情况处理
    v := reflect.ValueOf(value)

    switch v.Kind() {
    case reflect.Int:
        // do something
    case reflect.String:
        // do something
    }

    // ...
}

This seems to achieve our purpose, but there is a problem when you write code like the above:

You chose generics to avoid reflection, and ended up using reflection in generics for some functionality

When this happens, you may need to reconsider whether your needs really need to use generics (after all, the generic mechanism itself is very complicated, plus the complexity of reflection, the added complexity is not must be worth it)

Of course, all these options are in your own hands

5. Generic functions

After introducing generic types and generic receivers, let's introduce the last place where generics can be used - generic functions. With the above knowledge, writing generic functions is also very simple. Suppose we want to write a function that computes the sum of two numbers:

 func Add(a int, b int) int {
    return a + b
}

Of course, this function can only calculate the sum of ints, and floating-point calculations are not supported. At this point we can define a generic function like this:

 func Add[T int | float32 | float64](a T, b T) T {
    return a + b
}

The above is the definition of a generic function.

Such functions with type parameters are called generic functions

The difference between it and ordinary functions is that the function name is followed by a type parameter. The meaning, writing and usage of the type parameter here are exactly the same as the generic type, so I won't go into details.

Like generic types, generic functions cannot be called directly. If you want to use generic functions, you must pass in type arguments before you can call them.

 Add[int](1,2) // 传入类型实参int,计算结果为 3
Add[float32](1.0, 2.0) // 传入类型实参float32, 计算结果为 3.0

Add[string]("hello", "world") // 错误。因为泛型函数Add的类型约束中并不包含string

You may find it inconvenient to manually specify the type arguments every time. So Go also supports automatic deduction of type arguments:

 Add(1, 2)  // 1,2是int类型,编译请自动推导出类型实参T是int
Add(1.0, 2.0) // 1.0, 2.0 是浮点,编译请自动推导出类型实参T是float32

The way of writing automatic deduction is like eliminating the step of passing in the actual parameter, but please remember that this is just the compiler that deduces the type argument for us, and the step of passing in the actual parameter actually happens.

5.1 Anonymous functions do not support generics

In Go we often use anonymous functions like:

 fn := func(a, b int) int {
    return a + b 
}  // 定义了一个匿名函数并赋值给 fn 

fmt.Println(fn(1, 2)) // 输出: 3

So does Go support anonymous generic functions? The answer is no - anonymous functions cannot define their own type parameters:

 // 错误,匿名函数不能自己定义类型实参
fnGeneric := func[T int | float32](a, b T) T {
        return a + b
} 

fmt.Println(fnGeneric(1, 2))

But anonymous functions can use type arguments defined elsewhere, such as:

 func MyFunc[T int | float32 | float64](a, b T) {

    // 匿名函数可使用已经定义好的类型形参
    fn2 := func(i T, j T) T {
        return i*2 - j*2
    }

    fn2(a, b)
}

5.2 Since generic functions are supported, what about generic methods?

Since all functions support generics, you should naturally think, does method support support generics? Unfortunately, the current Go methods do not support generics, as follows:

 type A struct {
}

// 不支持泛型方法
func (receiver A) Add[T int | float32 | float64](a T, b T) T {
    return a + b
}

But because the receiver supports generics, if you want to use generics in the method, the only way at present is to save the country through the curve, and use the type parameter through the receiver in a roundabout way:

 type A[T int | float32 | float64] struct {
}

// 方法可以使用类型定义中的形参 T 
func (receiver A[T]) Add(a T, b T) T {
    return a + b
}

// 用法:
var a A[int]
a.Add(1, 2)

var aa A[float32]
aa.Add(1.0, 2.0)

first half summary

After talking about generic types, generic receivers, and generic functions, Go's generics are more than half of the introduction. Here we make a summary of the concept:

  1. Go's generics (or or type parameters) are currently available in 3 places

    1. generic type - a type with type parameters in a type definition
    2. generic receiver - the receiver of the generic type
    3. generic function - a function with type parameters
  2. To implement generics, Go introduces some new concepts:

    1. type parameter
    2. Type parameter list
    3. type arguments
    4. type constraints
    5. Instantiation - Generic types cannot be used directly. If you want to use them, you must pass in type arguments for instantiation

What, this article is already very long and complicated, and it's only halfway through? Yes, the introduction of generics in Go 1.18 this time has added a lot of complexity to the language. At present, it is only an introduction to new concepts. The second half of the following paragraph will introduce the major adjustments made to the interface after the introduction of generics to Go. So get ready, let's go.

6. Interfaces that get complicated

Sometimes when using generic programming, we will write long type constraints, as follows:

 // 一个可以容纳所有int,uint以及浮点类型的泛型切片
type Slice[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64] []T

Of course, this way of writing is unbearable and difficult to maintain, and Go supports defining type constraints separately into interfaces, making the code easier to maintain:

 type IntUintFloat interface {
    int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}

type Slice[T IntUintFloat] []T

This code takes out the type constraint separately and writes it into the interface type IntUintFloat . When you need to specify type constraints, you can directly use the interface IntUintFloat .

However, such code is still not easy to maintain, and interfaces and interfaces, interfaces and common types can also be combined by | :

 type Int interface {
    int | int8 | int16 | int32 | int64
}

type Uint interface {
    uint | uint8 | uint16 | uint32
}

type Float interface {
    float32 | float64
}

type Slice[T Int | Uint | Float] []T  // 使用 '|' 将多个接口类型组合

In the above code, we define three interface types Int, Uint, Float respectively, and finally combine them by using | in the type constraint of Slice[T].

At the same time, other interfaces can also be directly combined in the interface, so it can also be as follows:

 type SliceElement interface {
    Int | Uint | Float | string // 组合了三个接口类型并额外增加了一个 string 类型
}

type Slice[T SliceElement] []T

6.1 ~ : specify the underlying type

Although Slie[T] defined above can achieve its purpose, it has a disadvantage:

 var s1 Slice[int] // 正确 

type MyInt int
var s2 Slice[MyInt] // ✗ 错误。MyInt类型底层类型是int但并不是int类型,不符合 Slice[T] 的类型约束

The reason for the error here is that the generic type Slice[T] allows int as a type argument, not MyInt (though MyInt's underlying type is int, it's still not an int).

In order to fundamentally solve this problem, Go has added a new symbol ~ , in the type constraint, use something like ~int This way of writing means not only int, but also int Types that are underlying types are also available for instantiation.

The code is rewritten using ~ as follows:

 type Int interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

type Uint interface {
    ~uint | ~uint8 | ~uint16 | ~uint32
}
type Float interface {
    ~float32 | ~float64
}

type Slice[T Int | Uint | Float] []T 

var s Slice[int] // 正确

type MyInt int
var s2 Slice[MyInt]  // MyInt底层类型是int,所以可以用于实例化

type MyMyInt MyInt
var s3 Slice[MyMyInt]  // 正确。MyMyInt 虽然基于 MyInt ,但底层类型也是int,所以也能用于实例化

type MyFloat32 float32  // 正确
var s4 Slice[MyFloat32]

Restrictions : There are certain restrictions when using ~ :

  1. The type after ~ cannot be an interface
  2. The type after ~ must be a primitive type
 type MyInt int

type _ interface {
    ~[]byte  // 正确
    ~MyInt   // 错误,~后的类型必须为基本类型
    ~error   // 错误,~后的类型不能为接口
}

6.2 From Method set to Type set

In the above example, we learned a new way of writing interfaces that didn't exist before Go 1.18. If you are more keen, you will be vaguely aware of this change of writing, which must also mean that the concept in the Go language 接口(interface) has undergone a very big change.

Yes, before Go1.18, Go's official definition of 接口(interface) is: an interface is a method set (method set)

An interface type specifies a method set called its interface

Just like the following code, ReadWriter interface defines an interface (method set), which contains two methods Read() and Write() . All types that define both methods are considered to implement this interface.

 type ReadWriter interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}

However, if we rethink the above interface from another angle, we will find that the definition of the interface can actually be understood like this:

We can regard the ReaderWriter interface as representing a collection of types , all the types that implement Read() Writer() these two methods are in the type collection represented by the interface among

By looking at the interface from another angle, the definition of the interface in our eyes has changed from 方法集(method set) to 类型集(type set) . Since Go1.18, the definition of the interface has been officially changed to Type set based on this point.

An interface type defines a type set

You might think, doesn't this mean that changing the conceptual definition is actually useless? Yes, if the interface functionality does not change. But remember the following way of using interfaces to simplify type constraints:

 type Float interface {
    ~float32 | ~float64
}

type Slice[T Float] []T

This shows why you need to change the definition of the interface. Re-understanding the above code with the concept of type set is:

The interface type Float represents a type set , and all types with float32 or float64 as the underlying type are in this type set

In type Slice[T Float] []T , the real meaning of type constraint is:

Type constraints specify the set of types acceptable for type parameters, and only types that belong to this set can replace the parameters for instantiation

Such as:

 var s Slice[int]      // int 属于类型集 Float ,所以int可以作为类型实参
var s Slice[chan int] // chan int 类型不在类型集 Float 中,所以错误

6.2.1 Changes in the definition of interface implementation

Now that the interface definition has changed, the definition of 接口实现(implement) has naturally changed since Go1.18:

We can say that type T implements interface I when the following conditions are met:

  • When T is not an interface: Type T is a member of the type set represented by interface I (T is an element of the type set of I)
  • When T is an interface: The type set represented by the T interface is a subset of the type set represented by I (Type set of T is a subset of the type set of I)

6.2.2 Union of types

We are already familiar with the union. The symbol we have been using before is | is the union of types ( union )

 type Uint interface {  // 类型集 Uint 是 ~uint 和 ~uint8 等类型的并集
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

6.2.3 Intersection of Types

An interface can be written in more than one line. If an interface has multiple lines of type definitions, take the intersection between them.

 type AllInt interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint32
}

type Uint interface {
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

type A interface { // 接口A代表的类型集是 AllInt 和 Uint 的交集
    AllInt
    Uint
}

type B interface { // 接口B代表的类型集是 AllInt 和 ~int 的交集
    AllInt
    ~int
}

In the above example

  • Interface A represents the intersection of AllInt and Uint, namely ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
  • Interface B represents the intersection of AllInt and ~int, ie ~int

In addition to the above intersection, the following is also an intersection:

 type C interface {
    ~int
    int
}

Obviously, the intersection of ~int and int has only one type of int, so there is only one type of int in the type set represented by interface C

6.2.4 Empty set

When the intersection of multiple types is as follows Bad this is empty, Bad The type set represented by this interface is an empty set :

 type Bad interface {
    int
    float32 
} // 类型 int 和 float32 没有相交的类型,所以接口 Bad 代表的类型集为空

None of the types belong to the empty set . Although Bad is compilable, it doesn't actually make any sense.

6.2.5 Empty interface and any

The empty set is mentioned above, and then a special type set -- 空接口 interface{} . Because the definition of the interface has changed since Go1.18, the definition of interface{} has also undergone some changes:

An empty interface represents a collection of all types

Therefore, the empty interface after Go1.18 should be understood as follows:

  1. Although no type is written in the empty interface, it represents a collection of all types, not an empty set
  2. Specifying an empty interface in a type constraint means specifying a type set that contains all types, not a type constraint that restricts the use of an empty interface as a type parameter

     // 空接口代表所有类型的集合。写入类型约束意味着所有类型都可拿来做类型实参
    type Slice[T interface{}] []T
    
    var s1 Slice[int]    // 正确
    var s2 Slice[map[string]string]  // 正确
    var s3 Slice[chan int]  // 正确
    var s4 Slice[interface{}]  // 正确

Because the empty interface is a type set that includes all types, we often use it. So, Go1.18 began to provide a new keyword equivalent to the empty interface interface{} any to make the code simpler:

 type Slice[T any] []T // 代码等价于 type Slice[T interface{}] []T

In fact, the definition of any is located in the builtin.go file of the Go language (see below), any is actually the alias of interaface{} alias), the two are completely equivalent

 // any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}

So starting from Go 1.18, all places where empty interfaces can be used can actually be replaced with any directly, such as:

 var s []any // 等价于 var s []interface{}
var m map[string]any // 等价于 var m map[string]interface{}

func MyPrint(value any){
    fmt.Println(value)
}

If you are happy, after the project is migrated to Go1.18, you can use the following command to directly replace all empty interfaces in the entire project with any. Of course, because it is not mandatory, whether to use interface{} or any depends on your preferences

 gofmt -w -r 'interface{} -> any' ./...
💡 In the Go language project, someone once proposed to replace all interface{ } in the Go language with any issue , and then it was rejected because the scope of influence was too large and the influencing factors were uncertain.

6.2.6 Comparable and ordered

For some data types, we need to restrict the type constraints to only accept types that can be compared with != and == , such as map:

 // 错误。因为 map 中键的类型必须是可进行 != 和 == 比较的类型
type MyMap[KEY any, VALUE any] map[KEY]VALUE

So Go directly built an interface called comparable , which represents all available != and == types of comparison:

 type MyMap[KEY comparable, VALUE any] map[KEY]VALUE // 正确

comparable The more misleading point is that many people confuse it with sortable. Comparable refers to types that can perform != == operations. There is no guarantee that this type can perform size comparisons ( >,<,<=,>= ). as follows:

 type OhMyStruct struct {
    a int
}

var a, b OhMyStruct

a == b // 正确。结构体可使用 == 进行比较
a != b // 正确

a > b // 错误。结构体不可比大小

The size-comparable type is called Orderd . At present, the Go language does not have directly built-in corresponding keywords like comparable , so if you want, you need to define the relevant interfaces yourself. For example, we can refer to the official Go package golang.org/x/exp/constraints How to define:

 // Ordered 代表所有可比大小排序的类型
type Ordered interface {
    Integer | Float | ~string
}

type Integer interface {
    Signed | Unsigned
}

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

type Unsigned interface {
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

type Float interface {
    ~float32 | ~float64
}
💡 Although the official package golang.org/x/exp/constraints can be used directly here, because this package is an experimental x package, it may change greatly in the future, so it is not recommended to use it directly

6.3 Two types of interfaces

Let's look at another example, which is the best example of how an interface is a set of types:

 type ReadWriter interface {
    ~string | ~[]rune

    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}

When you first see this example, you must be a little confused and don't understand what it means, but it doesn't matter, we can easily understand the meaning of this interface by using the concept of type set:

The interface type ReadWriter represents a set of types, all of which take string or []rune as the underlying type and implement Read() Write() The types of these two methods are in the type set represented by ReadWriter

As in the following code, StringReadWriter exists in the type set represented by the interface ReadWriter, and BytesReadWriter does not belong to the type set represented by ReadWriter because the underlying type is []byte (neither string nor []rune)

 // 类型 StringReadWriter 实现了接口 Readwriter
type StringReadWriter string 

func (s StringReadWriter) Read(p []byte) (n int, err error) {
    // ...
}

func (s StringReadWriter) Write(p []byte) (n int, err error) {
 // ...
}

//  类型BytesReadWriter 没有实现接口 Readwriter
type BytesReadWriter []byte 

func (s BytesReadWriter) Read(p []byte) (n int, err error) {
 ...
}

func (s BytesReadWriter) Write(p []byte) (n int, err error) {
 ...
}

You will definitely say, ah, wait, this interface has become too complicated, then I define an interface variable of type ReadWriter , and then when assigning an interface variable, not only must consider the implementation of the method, but also Must take into account the specific underlying type? The mental burden is too great. Yes, in order to solve this problem and maintain the compatibility of the Go language, Go1.18 began to divide the interface into two types

  • Basic interface
  • General interface

6.3.1 Basic interface

If there are only methods in the interface definition, then this interface is called the basic interface (Basic interface) . This interface is the interface before Go1.18, and the usage is basically the same as before Go1.18. The basic interface can be roughly used in the following places:

  • The most commonly used, define interface variables and assign values

     type MyError interface { // 接口中只有方法,所以是基本接口
        Error() string
    }
    
    // 用法和 Go1.18之前保持一致
    var err MyError = fmt.Errorf("hello world")
  • Because the base interface also represents a type set, it can also be used in type constraints

     // io.Reader 和 io.Writer 都是基本接口,也可以用在类型约束中
    type MySlice[T io.Reader | io.Writer]  []Slice

6.3.2 General interface

If there are not only methods but also types in an interface, this interface is called a general interface . The following examples are general interfaces:

 type Uint interface { // 接口 Uint 中有类型,所以是一般接口
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

type ReadWriter interface {  // ReadWriter 接口既有方法也有类型,所以是一般接口
    ~string | ~[]rune

    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}

General interface types cannot be used to define variables and can only be used in generic type constraints . So the following usage is wrong:

 type Uint interface {
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

var uintInf Uint // 错误。Uint是一般接口,只能用于类型约束,不得用于变量定义

This restriction ensures that the use of general interfaces is limited to generics, will not affect the code before Go1.18, and also greatly reduces the mental burden when writing code

6.4 Generic Interfaces

Type parameters can be used in the definition of all types, so interface definitions can also use type parameters. Observe the following two examples:

 type DataProcessor[T any] interface {
    Process(oriData T) (newData T)
    Save(data T) error
}

type DataProcessor2[T any] interface {
    int | ~struct{ Data interface{} }

    Process(data T) (newData T)
    Save(data T) error
}

Because of the introduction of type parameters, these two interfaces are generic types. To use a generic type, it only makes sense to pass in the type argument to instantiate it . So let's try to instantiate these two interfaces. Because the type constraint of T is any, you can choose any type as an actual parameter (such as string):

 DataProcessor[string]

// 实例化之后的接口定义相当于如下所示:
type DataProcessor[string] interface {
    Process(oriData string) (newData string)
    Save(data string) error
}

After instantiation, it is easy to understand, DataProcessor[string] Because there are only methods, it is actually a Basic interface . This interface contains two methods that can handle string types. Implementing these two methods that can handle the string type as follows implements this interface:

 type CSVProcessor struct {
}

// 注意,方法中 oriData 等的类型是 string
func (c CSVProcessor) Process(oriData string) (newData string) {
    ....
}

func (c CSVProcessor) Save(oriData string) error {
    ...
}

// CSVProcessor实现了接口 DataProcessor[string] ,所以可赋值
var processor DataProcessor[string] = CSVProcessor{}  
processor.Process("name,age\nbob,12\njack,30")
processor.Save("name,age\nbob,13\njack,31")

// 错误。CSVProcessor没有实现接口 DataProcessor[int]
var processor2 DataProcessor[int] = CSVProcessor{}

Then use the same method to instantiate DataProcessor2[T] :

 DataProcessor2[string]

// 实例化后的接口定义可视为
type DataProcessor2[T string] interface {
    int | ~struct{ Data interface{} }

    Process(data string) (newData string)
    Save(data string) error
}

DataProcessor2[string] Because it has type union, it is a general interface , so the meaning of this interface after instantiation is:

  1. Only if the two methods Process(string) string and Save(string) error are implemented, and the underlying type is int or struct{ Data interface{} } interface
  2. The general interface cannot be used for variable definitions and can only be used for type constraints, so the interface DataProcessor2[string] just defines a type set for type constraints
 // XMLProcessor 虽然实现了接口 DataProcessor2[string] 的两个方法,但是因为它的底层类型是 []byte,所以依旧是未实现 DataProcessor2[string]
type XMLProcessor []byte

func (c XMLProcessor) Process(oriData string) (newData string) {

}

func (c XMLProcessor) Save(oriData string) error {

}

// JsonProcessor 实现了接口 DataProcessor2[string] 的两个方法,同时底层类型是 struct{ Data interface{} }。所以实现了接口 DataProcessor2[string]
type JsonProcessor struct {
    Data interface{}
}

func (c JsonProcessor) Process(oriData string) (newData string) {

}

func (c JsonProcessor) Save(oriData string) error {

}

// 错误。DataProcessor2[string]是一般接口不能用于创建变量
var processor DataProcessor2[string]

// 正确,实例化之后的 DataProcessor2[string] 可用于泛型的类型约束
type ProcessorList[T DataProcessor2[string]] []T

// 正确,接口可以并入其他接口
type StringProcessor interface {
    DataProcessor2[string]

    PrintString()
}

// 错误,带方法的一般接口不能作为类型并集的成员(参考6.5 接口定义的种种限制规则
type StringProcessor interface {
    DataProcessor2[string] | DataProcessor2[[]byte]

    PrintString()
}

6.5 Various restriction rules defined by interface

Since the beginning of Go1.18, a lot of very trivial restriction rules have been added when defining type sets (interfaces), many of which have been introduced in the previous content, but there are still some remaining rules because they cannot find a good The place is introduced, so here is a unified introduction:

  1. When using | to connect multiple types, there must be no intersecting parts between the types (that is, they must be disjoint):

     type MyInt int
    
    // 错误,MyInt的底层类型是int,和 ~int 有相交的部分
    type _ interface {
        ~int | MyInt
    }

    However, if the intersecting type is an interface, it is not subject to this restriction:

     type MyInt int
    
    type _ interface {
        ~int | interface{ MyInt }  // 正确
    }
    
    type _ interface {
        interface{ ~int } | MyInt // 也正确
    }
    
    type _ interface {
        interface{ ~int } | interface{ MyInt }  // 也正确
    }
  2. A union of types cannot have type parameters

     type MyInf[T ~int | ~string] interface {
        ~float32 | T  // 错误。T是类型形参
    }
    
    type MyInf2[T ~int | ~string] interface {
        T  // 错误
    }
  3. An interface cannot be directly or indirectly incorporated into itself

     type Bad interface {
        Bad // 错误,接口不能直接并入自己
    }
    
    type Bad2 interface {
        Bad1
    }
    type Bad1 interface {
        Bad2 // 错误,接口Bad1通过Bad2间接并入了自己
    }
    
    type Bad3 interface {
        ~int | ~string | Bad3 // 错误,通过类型的并集并入了自己
    }
  4. When the number of union members of an interface is greater than one, it cannot be directly or indirectly merged comparable interface

     type OK interface {
        comparable // 正确。只有一个类型的时候可以使用 comparable
    }
    
    type Bad1 interface {
        []int | comparable // 错误,类型并集不能直接并入 comparable 接口
    }
    
    type CmpInf interface {
        comparable
    }
    type Bad2 interface {
        chan int | CmpInf  // 错误,类型并集通过 CmpInf 间接并入了comparable
    }
    type Bad3 interface {
        chan int | interface{comparable}  // 理所当然,这样也是不行的
    }
  5. Interfaces with methods (whether base or generic), cannot be written into the union of interfaces:

     type _ interface {
        ~int | ~string | error // 错误,error是带方法的接口(一般接口) 不能写入并集中
    }
    
    type DataProcessor[T any] interface {
        ~string | ~[]byte
    
        Process(data T) (newData T)
        Save(data T) error
    }
    
    // 错误,实例化之后的 DataProcessor[string] 是带方法的一般接口,不能写入类型并集
    type _ interface {
        ~int | ~string | DataProcessor[string] 
    }
    
    type Bad[T any] interface {
        ~int | ~string | DataProcessor[T]  // 也不行
    }

7. Summary

So far, we have finally introduced the generics of Go1.18 from scratch. Because the introduction of generics to Go this time brings a lot of complexity, and also adds a lot of scattered and trivial rule restrictions. So it took me almost a week to write this article on and off. Although generics are highly anticipated, the recommended usage scenarios are not so extensive. For the use of generics, we should follow the following rules:

Generics do not replace the dynamic types implemented by interface + reflection before Go1.18. Generics are very suitable for use in the following situations: When you need to write the same logic for different types, it is best to use generics to simplify the code (For example, if you want to write a queue, write a linked list, stack, heap and other data structures)

References


WonderfulSoap
685 声望338 粉丝