go 泛型的使用

go 1.18 release note 中对于泛型提到了语法上的 6 条改变1

  1. The syntax for function and type declarations now accepts type parameters.
  2. Parameterized functions and types can be instantiated by following them with a list of type arguments in square brackets.
  3. The new token ~ has been added to the set of operators and punctuation.
  4. The syntax for Interface types now permits the embedding of arbitrary types (not just type names of interfaces) as well as union and ~T type elements. Such interfaces may only be used as type constraints. An interface now defines a set of types as well as a set of methods.
  5. The new predeclared identifierany is an alias for the empty interface. It may be used instead of interface{}.
  6. The new predeclared identifiercomparable is an interface that denotes the set of all types which can be compared using == or !=. It may only be used as (or embedded in) a type constraint.

我们或许可以把他们总结为 3 大类:

  1. 在 function 中可以使用泛型
  2. 在 type 中可以使用泛型
  3. 在 interface 中可以使用泛型

我们逐一讨论。

Go 文档的一个泛型语法教程2里展示了在定义 function 时使用泛型的案例。比如:

1
2
3
4
5
6
7
8
9
// SumIntsOrFloats sums the values of map m. It supports both int64 and float64
// as types for map values.
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}

如此,SumIntsOrFloats 就可以处理多种类型的参数。但如果没有泛型的语法,是否可以用一个 function 处理多种类型呢。go 中的 interface 或许支持这种想法,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func SumIntsOrFloatsWithInterface(m interface{}) interface{} {
switch nums := m.(type) {
case map[string]int64:
var s int64
for _, v := range nums {
s += v
}
return s
case map[string]float64:
var s float64
for _, v := range nums {
s += v
}
return s
default:
panic("Unknown type")
}
}

又或者把不同类型的 map 抽象出一个带有 Sum() 方法的接口,比如:

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
27
28
29
30
31
32
33
34
35
36
type NumMap interface {
Sum() interface{}
}

type NumMapInt64 map[string]int64

func (m NumMapInt64) Sum() interface{} {
var s int64
for _, v := range m {
s += v
}
return s
}

type NumMapFloat64 map[string]float64

func (m NumMapFloat64) Sum() interface{} {
var s float64
for _, v := range m {
s += v
}
return s
}

func SumIntsOrFloatsWithPredifinedInterface(m interface{}) interface{} {
switch nums := m.(type) {
case NumMapInt64:
return nums.Sum()
case NumMapFloat64:
return nums.Sum()
default:
panic("Unknown type")
}
}

// fmt.Printf("Predifined Interface Sums: %v and %v\n", SumIntsOrFloatsWithPredifinedInterface(NumMapInt64(ints)), SumIntsOrFloatsWithPredifinedInterface(NumMapFloat64(floats)))

但是不使用泛型的写法

  1. 看起来有点啰嗦
  2. 返回值的类型可能还需要再 type assertion 或者 switch 一下
  3. 类型错误可能会在 runtime 中发生,而泛型的参数类型如果不被支持,在 compile time 就会报错2

综上,虽然我不知道什么时候使用泛型函数合适,但是所有需要 type switch 的地方,或许都可以考虑一下泛型的写法。

声明 type 时也可以使用类型参数,从而构成泛型。比如 go 文档中的一个例子 3

1
2
3
4
type List[T any] struct {
next *List[T]
value T
}

这样的泛型 type 可以实例化、用于指定函数的参数类型、甚至作为 receiver 去声明 methods:

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
27
28
29
30
31
// 1. 泛型 type 的实例化
var l *List[int]
l = &List[int]{value: 1, next: &List[int]{value: 2, next: &List[int]{value: 3, next: nil}}}
fmt.Println(l)

// 2. 指定函数的参数类型
func TraverseList[T int | float64](l *List[T]) {
for l != nil {
fmt.Println(l.value)
l = l.next
}
}
// or
func TraverseList(l *List[int]) {
for l != nil {
fmt.Println(l.value)
l = l.next
}
}
// call with
// TraverseList(l)

// 3. 作为 receiver 去声明 methods
func (l *List[T]) Traverse() {
for l != nil {
fmt.Println(l.value)
l = l.next
}
}
// call with
// l.Traverse()

如此使用泛型也可以避免写极其相似的代码来构建属性、方法都接近的 types。

最后,在 interface 中可以插入非 interface 的类型,形成一个 type constraint,但这样的 interface 也只能作为 type constraint 了。在目前的 go 语法规则下,它不能实例化,不能作为函数的参数类型或者 composite 结构的 field 类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
type Interface interface {
struct{} | int | []any
}

/* Err: MisplacedConstraintIface
https://pkg.go.dev/golang.org/x/tools/internal/typesinternal#MisplacedConstraintIface

var i Interface

func _print(i Interface) {
fmt.Println(i)
}
*/

但是这样的 type constraint 依然可以插入接口需要的 method 或者其他 interface。比如下面的语句是可以的:

1
2
3
4
5
type Interface interface {
struct{} | int | []any
NumMap // NumMap 是上面定义一个 interface
String()
}

有博客4指出,go 文档将 interface 的定义从 a method set 改为了 a type set。在 type constraint 中混合 interface 原先的用法,达成的效果是进一步限制 type constraint 的范围5,实现一个不会比之前的 type constraint 更大的 type set。

而以前 method set 形式的 interface 现在是一个没有 type union(type constraint 中类似 int | float64 的语句)的 type set,除了作他以前的用途(比如,声明后实例化为一个 nil 的变量,指定函数的参数类型)也可以用作 type constraint,比如:

1
2
3
func test[A NumMap](a A) {
fmt.Println(a)
}

综上,我们对比一下含有和不含有 type union 的 interface 的区别:

含有 type union 的 interface 不含有
实例化 不可以 可以声明出一个 nil 的变量
指定参数类型或 filed 类型 不可以 可以
嵌入其他 interface 的效果 限制 type set 继承接口的 methods
必须作为 type constraint 使用 不是
1. “Go 1.18 Release Notes - The Go Programming Language.” 1 Aug. 2022, go.dev/doc/go1.18#generics.
2. “Tutorial: Getting started with generics - The Go Programming Language.” 1 Aug. 2022, go.dev/doc/tutorial/generics.
3. “The Go Programming Language Specification - The Go Programming Language.” 1 Aug. 2022, go.dev/ref/spec#Type_parameter_declarations.
4. “Go generic programming: interface is no longer the interface.” 9 Jan. 2022, www.sobyte.net/post/2022-01/the-interface-is-not-that-interface-in-go-1-18.
5. “Three new concepts related to interfaces since Go 1.18.” 18 Jan. 2022, www.sobyte.net/post/2022-01/three-new-concepts-of-go-interface-since-1-18.