likes
comments
collection
share

学习 Go 泛型

作者站长头像
站长
· 阅读数 29

泛型在 Go 1.18 版本加入,您需要开始使用 1.18 版本,您可以在此处下载。

什么是泛型?为什么 Go 需要它?

泛型,是一种让函数接受多种数据类型作为同一个输入参数的方法。假如您有一个加法函数,它接受两个输入参数 ab,在函数中,输入参数 ab会相加并返回值。

您需要决定输入参数的数据类型,intint64float。这将迫使任何使用该函数的开发人员,在使用之前将其值类型转换为正确的数据类型。

另一种解决方案是为 int 设置一个加法函数,为 float 设置另一个加法函数,让您有多个函数执行相同的工作。您可以点击此处获取可运行的代码。

学习 Go 泛型

在上面的示例中,我们从 float 结果中得到了错误的结果,因为我们在将其转换为 int 时删除了 0.5。

因此,唯一合适的解决方案是编写两个有重复功能的函数,如下所示。

  • Add(a,b int) int
  • AddFloat(a,b float32) float32

我说这是唯一是的解决方案,您可能会告诉我,你可以使用 interface{} 作为输入和返回的类型。我并不喜欢这种魔改的风格。您会丢失编译时错误检查,因为编译器不知道您在使用该 interface 做什么。这很快会变得无法维护,并且添加了许多额外的代码。

这就是泛型解决的问题,也是为什么这么多开发人员非常渴望看到它发布的原因。

泛型在 Go 1.18 发布后,上述问题的解决方案变得很简单。您可以点击此处获取可运行的代码。

学习 Go 泛型

泛型的工作原理

让我们深入了解泛型的基本用法。

我们将从常规 Add 函数开始并向其添加泛型的特征,同时解释我们添加的是什么,以及为什么添加。

我们通过声明 func FunctionName 来定义函数。

函数参数是在内部定义的(),可以任意多。函数参数是通过声明一个名称,后跟数据类型来定义的。例如 (a int) 定义函数局部范围将有一个名为 a 的整数。

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

Add 函数的函数参数是 ab,数据类型都为 int

解释函数参数看起来是愚蠢的,但是了解泛型之前,先了解他它是至关重要的。

接下来,我们定义类型参数。类型参数在 [] 里面定义,应该在函数参数之前定义,[](a,b int)。 您可以像定义函数参数一样定义类型参数,名称后跟数据类型。

类型参数的参数名通常大写,使它们更容易被发现。

举一个例子,我们声明参数 V 是一个整数,[V int]

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

如果将 V 定义为声明参数,在该函数的作用域中,V 不允许用作变量,V 仅代表类型参数。

我们现在可以用 ab 替换数据类型 V,也可以将函数输出替换为 V

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

现在,您可能认为我们什么也没做,只是用更复杂的写法替代 int。你是对的,我们还没有完成,上面的代码此时仍然只能接收 int 类型。

如果您尝试传入 floatAdd 函数,应该会看到一个错误 float32 does not implement int。原因是 Add 函数约束了声明参数类型为 int,我们可以使用 | 字符,向函数添加添加第二种数据类型,竖线字符用来表示 or,这意味着我们可以为 V 参数添加许多不同的数据类型选项。

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

为了让 Add 函数看起来更清楚,我们可以单独声明类型参数,并且声明参数可以被重用了。我们只需要声明一个 Add 包含的数据类型接口。我还添加了更多数据类型。您可以点击此处获取可运行的代码。

学习 Go 泛型

在您成为泛型高手之前的最后一件事。您可以在调用的泛型函数前设置要返回的参数类型。如下所示。

result := Add[int](a, b)

类型参数和(~

我们现在可以使用泛型函数了,但是还剩下一些细节。想象一下,如果您想控制在泛型函数中返回的数据类型应该怎么办?

Add[int](1,2) // 希望返回 int 数据类型
Add[int64](1,2) // 希望返回 int64 数据类型

我们现在可以告诉函数返回什么数据类型,太棒了!

现在,这里还有一个问题。如果您的传入函数 Add 的参数数据类型是类型别名 alias,它不是任何类型怎么办?

您将无法告诉函数返回什么类型的数据。

// 创建一个自定义类型 OwnInteger
type OwnInteger int

var myInt OwnInteger
myInt = 10
Add(myInt, 20) // 这将引发 painc,因为 myInt 不属于任何类型

为了解决这个问题,Go 团队还添加了 ~ 告诉类型参数,允许任何自定义类型。您可以点击此处获取可运行的代码。

学习 Go 泛型

通过泛型完成类型约束

我们已经学会了 generic functions 泛型函数,现在该了解 generic types 泛型类型了。

要创建泛型类型,您必须定义一个新类型,并给它一个类型参数。举一个例子,我们将创建一个 Results 类型,它重用了类型参数 Addable

type Results[T Addable] []T

在示例中,我们创建了一个 Slice 切片,其中存放的数据类型为 Addable,从而告诉编译器在初始化 Results 切片时,必须定义 Addable 接口中包含的类型。您可以点击此处获取可运行的代码。

学习 Go 泛型

您可能会在想,为什么不直接使用 Addable 用作变量声明类型呢?

var resultStorage Results[Addable]

这样并不可以,因为 Addable 是一个参数声明,又叫做类型约束,这样写会引发编译错误 interface contains type constraints

我们可以使用新引入的 any 来允许 Results 保存任何数据类型。anyinterface{} 的类型别名。

学习 Go 泛型

通过泛型完成接口约束

目前为止,我们只为单个类型进行约束,但是泛型也可以用作接口的约束。

举一个例子,我们创建一个名为 Move 的泛型函数。

type Moveable interface {
   Move(int)
}

// Move 是一个泛型函数,它接收 Moveable 并对其进行移动
func Move[V Moveable](v V, meters int) {
   v.Move(meters)
}

我们再创建一个 PersonCar 试一下。您可以点击此处获取可运行的代码。

学习 Go 泛型

到目前为止,我们只使用了一个通用参数来保持简单。但请记住,您可以添加多个,并输出多个。

接下来,让我们将 MoveableSubtractable 约束结合到 Move 函数中,允许用户添加一个 Distance 值,我们用它来计算距离目标还有多远。

func Move[V Moveable, S Subtractable](v V, distance S, meters S) S

但是,这样的更改会导致编译器 panic,因为 Move 的入参为 int 类型,那我们将它修改为 Subtractable。您可以点击此处获取代码。

学习 Go 泛型

遗憾的是,修改后它仍然无法工作,这只是我们想要完成的伪代码。编译器对我们"大吼大叫"的原因是,Subtractable 只允许在类型约束中使用,不允许作为参数类型。

不过,有一种方法可以得到我们想要的,那就是泛型结构体。

我们对接口 Moveable 添加类型约束。

// 实现这个接口,需要一个具有 Subtractable 约束的泛型类型
type Moveable[S Subtractable] interface {
    Move(S)
}

现在我们将结构体修改为泛型结构体。这意味着在创建对象时,您还必须定义用于该对象的数据类型,通过 []即可,请记住,它们被称为类型参数。

type Car[S Subtractable] struct {
   Name string
}

type Person[S Subtractable] struct {
   Name string
}

func main(){
   p := Person[float64]{Name: "Guowei"}
   c := Car[int]{Name: "POLO"}
}

我们结构体实现接口的方法也需要修改。

func (p Person[S]) Move(meters S) {
   fmt.Printf("%s moved %d meters\n", p.Name, meters)
}

func (c Car[S]) Move(meters S) {
   fmt.Printf("%s moved %d meters\n", c.Name, meters)
}

Move 函数的函数参数 Moveable 必须增加类型约束。

func Move[V Moveable[S], S Subtractable](v V, distance S, meters S) S {
   v.Move(meters)
   return Subtract(distance, meters)
}

我们已经准备好了,接下来只要在 main 函数中调用就可以了,第一个 Car 的调用很容易理解,因为我们使用 int 类型约束,编译器也会默认使用它。 但是第二个 Person 的调用是复杂的,因为我们要使用 float64 数据类型的约束。因此,我们需要告诉 Move 初始化一个 float64 的值,否则 Move 将默认 int

func main(){
   p := Person[float64]{Name: "GuoWei"}
   c := Car[int]{Name: "POLO"}
   
   milesToDestination := 100
   distanceLeft := Move(c, milesToDestination, 95)
   fmt.Println(distanceLeft)
   fmt.Println("DistanceType: ", reflect.TypeOf(distanceLeft))

   newDistanceLeft := Move[Person[float64], float64](p, float64(distanceLeft), 5)
   fmt.Println(newDistanceLeft)
   fmt.Println("DistanceType: ", reflect.TypeOf(newDistanceLeft))
}

您可以点击此处获取可运行的代码。

我们可以通过重新排序函数 Move 的类型参数来避免上述 Move[Person[float64], float64] 这种情况。这要归功于编译器和运行时推断数据类型。修改后,语法上看起来是更好的。

func Move[S Subtractable, V Moveable[S]](v V, distance S, meters S) S {
   v.Move(meters)
   return Subtract(distance, meters)
}
newDistanceLeft := Move[float64](p, float64(distanceLeft), 5)

总结

恭喜!现在您已经掌握了 Go 的泛型领域!

泛型很有用。当初,许多开发者都在期待 Go1.18 这个版本。泛型很方便,但有时很容易让事情变得有点过于复杂。仅在有实际用例时,我们再尝试使用它吧。

其他

封面来自互联网。