Featured image of post Go泛型学习笔记

Go泛型学习笔记

泛型尝鲜

封面PID=98135423

Go自从1.18开始正式支持了泛型,官方称其为Go开源发布之后最大的一次变更。
在出现泛型之前,Go传递不定参数的通用方法是传递接口,这次支持泛型无疑是对Go编码影响巨大的升级。

本文针对学习过程中的一些要点做初步记录。

泛型

Go支持了泛型,为其带来了三个新的特性:

  • Type parameters for function and types.
  • Defining interface types as sets of types, including types that don’t have methods.
  • Type inference, which permits omitting type arguments in many cases when calling a function.

简单来说,就是支持了函数、type关键字的类型参数,使用无方法接口实现类型集,函数调用泛型推理省略类型参数。

以下是参照Go的官方例子做的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 无方法接口实现类型集
type Number interface {
	int | float64
}
// 函数类型参数
func GMin[T Number](x, y T) T {
    if x < y {
        return x
    }
    return y
}

Hit:

  • 类型参数用[]来进行定义,泛型标识符T
  • Go的编译器会在替换泛型标识符后检查实际类型是否满足函数的类型约束条件。
1
2
3
4
// 这一步编译器会进行函数实例化,如果类型不满足函数内部约束条件(本例里是运算符 '<' ),则会报错
fmin := GMin[float64]   // float64在这就是实际指定的类型
m := fmin(2.71, 3.14)   // 实例化成功后可正常调用 
fmt.Println(m)          // 2.71

其中Go在进行编译时会进行类型推算(type inference),因此你也可以将你的函数写成:

1
2
m := GMin(2.71, 3.14)   // 不需要手动指定T的真实类型,Go自己推算
fmt.Println(m)          // 2.71

以上是一个简单的Go泛型函数调用,其中我们可以看到Go使用了接口关键字interface关键字来定义泛型的约束类型。 接下来我们就说说为啥Go是用interface来定义泛型的约束类型集。

泛型约束集

接口定义了一系列方法,实现了这些方法的type可以表现成type->接口的一种映射关系;从另一个角度上来说,接口同样定义了一个实现了这些方法的type的集合,这个集合中的type都实现了接口的方法。因此我们反过来可以获得接口->type的映射关系

官方博客里的这张图片里展示了一个接口在被多个type实现时所具有的集合关系,其中每一个圈都是一个接口的方法集。可以看到,typeP、Q、R在实现了interface后,会在中心区域有一个“方法交集”。这个交集反过来定义了下图的类型集。

一般的接口是方法的接口,这里泛型的接口可以理解为类型的接口。

Go的泛型还支持指定某个Underlying Type,例如:

1
2
3
type Number interface {
	~int | float64
}

上述代码中,~int表示所有以int为Underlying Type的类型。

泛型的Core Type

需要注意的是Go的在使用内置函数的时候需要确定类型的Core Type,比如使用makerange等操作。如果你的泛型interface没有Core Type,你会看到类似如下报错

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type NumberSlice interface {
	[]float64 | []int | []string
}

func test[T NumberSlice](num T) {
    // cannot range over num (variable of type T constrained by NumberSlice) (T has no core type)
	for _, v := range num {
        // do something
	}
}

这是因为Go定义类型接口的Core Type只在两种情况存在:

  1. 接口类型集存在统一的Underlying Type
  2. 接口类型集存在统一的管道,管道的类型、方向需一致

像上面的代码,NumberSlice由于不存在相同的Underlying Type,因此不具有Core Type,不能使用range关键字进行操作。不过我们可以通过调整泛型来实现相同的功能:

1
2
3
4
5
6
7
8
9
type Number interface {
	float64 | int
}

func test[T Number](num []T) {
	for _, v := range num {
		 // do something
	}
}

这样就可以正常的实现遍历泛型slice了。

相关文档