学习 Go 泛型
泛型在 Go 1.18 版本加入,您需要开始使用 1.18 版本,您可以在此处下载。
什么是泛型?为什么 Go 需要它?
泛型,是一种让函数接受多种数据类型作为同一个输入参数的方法。假如您有一个加法函数,它接受两个输入参数 a
、b
,在函数中,输入参数 a
、b
会相加并返回值。
您需要决定输入参数的数据类型,int
、int64
或 float
。这将迫使任何使用该函数的开发人员,在使用之前将其值类型转换为正确的数据类型。
另一种解决方案是为 int
设置一个加法函数,为 float
设置另一个加法函数,让您有多个函数执行相同的工作。您可以点击此处获取可运行的代码。
在上面的示例中,我们从 float
结果中得到了错误的结果,因为我们在将其转换为 int
时删除了 0.5。
因此,唯一合适的解决方案是编写两个有重复功能的函数,如下所示。
Add(a,b int) int
AddFloat(a,b float32) float32
我说这是唯一是的解决方案,您可能会告诉我,你可以使用 interface{}
作为输入和返回的类型。我并不喜欢这种魔改的风格。您会丢失编译时错误检查,因为编译器不知道您在使用该 interface
做什么。这很快会变得无法维护,并且添加了许多额外的代码。
这就是泛型解决的问题,也是为什么这么多开发人员非常渴望看到它发布的原因。
泛型在 Go 1.18 发布后,上述问题的解决方案变得很简单。您可以点击此处获取可运行的代码。
泛型的工作原理
让我们深入了解泛型的基本用法。
我们将从常规 Add
函数开始并向其添加泛型的特征,同时解释我们添加的是什么,以及为什么添加。
我们通过声明 func FunctionName
来定义函数。
函数参数是在内部定义的(),可以任意多。函数参数是通过声明一个名称,后跟数据类型来定义的。例如 (a int)
定义函数局部范围将有一个名为 a
的整数。
func Add(a, b int) int {
return a + b
}
该 Add
函数的函数参数是 a
和 b
,数据类型都为 int
。
解释函数参数看起来是愚蠢的,但是了解泛型之前,先了解他它是至关重要的。
接下来,我们定义类型参数。类型参数在 []
里面定义,应该在函数参数之前定义,[](a,b int)
。 您可以像定义函数参数一样定义类型参数,名称后跟数据类型。
类型参数的参数名通常大写,使它们更容易被发现。
举一个例子,我们声明参数 V
是一个整数,[V int]
。
func Add[V int](a, b int) int {
return a + b
}
如果将 V
定义为声明参数,在该函数的作用域中,V
不允许用作变量,V
仅代表类型参数。
我们现在可以用 a
和 b
替换数据类型 V
,也可以将函数输出替换为 V
。
func Add[V int](a, b V) V {
return a + b
}
现在,您可能认为我们什么也没做,只是用更复杂的写法替代 int
。你是对的,我们还没有完成,上面的代码此时仍然只能接收 int
类型。
如果您尝试传入 float
到 Add
函数,应该会看到一个错误 float32 does not implement int
。原因是 Add
函数约束了声明参数类型为 int
,我们可以使用 |
字符,向函数添加添加第二种数据类型,竖线字符用来表示 or
,这意味着我们可以为 V
参数添加许多不同的数据类型选项。
func Add[V int | float32](a, b V) V {
return a + b
}
为了让 Add
函数看起来更清楚,我们可以单独声明类型参数,并且声明参数可以被重用了。我们只需要声明一个 Add
包含的数据类型接口。我还添加了更多数据类型。您可以点击此处获取可运行的代码。
在您成为泛型高手之前的最后一件事。您可以在调用的泛型函数前设置要返回的参数类型。如下所示。
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 团队还添加了 ~
告诉类型参数,允许任何自定义类型。您可以点击此处获取可运行的代码。
通过泛型完成类型约束
我们已经学会了 generic functions
泛型函数,现在该了解 generic types
泛型类型了。
要创建泛型类型,您必须定义一个新类型,并给它一个类型参数。举一个例子,我们将创建一个 Results
类型,它重用了类型参数 Addable
。
type Results[T Addable] []T
在示例中,我们创建了一个 Slice
切片,其中存放的数据类型为 Addable
,从而告诉编译器在初始化 Results
切片时,必须定义 Addable
接口中包含的类型。您可以点击此处获取可运行的代码。
您可能会在想,为什么不直接使用 Addable
用作变量声明类型呢?
var resultStorage Results[Addable]
这样并不可以,因为 Addable
是一个参数声明,又叫做类型约束,这样写会引发编译错误 interface contains type constraints
。
我们可以使用新引入的 any
来允许 Results
保存任何数据类型。any
是 interface{}
的类型别名。
通过泛型完成接口约束
目前为止,我们只为单个类型进行约束,但是泛型也可以用作接口的约束。
举一个例子,我们创建一个名为 Move
的泛型函数。
type Moveable interface {
Move(int)
}
// Move 是一个泛型函数,它接收 Moveable 并对其进行移动
func Move[V Moveable](v V, meters int) {
v.Move(meters)
}
我们再创建一个 Person
和 Car
试一下。您可以点击此处获取可运行的代码。
到目前为止,我们只使用了一个通用参数来保持简单。但请记住,您可以添加多个,并输出多个。
接下来,让我们将 Moveable
和 Subtractable
约束结合到 Move
函数中,允许用户添加一个 Distance
值,我们用它来计算距离目标还有多远。
func Move[V Moveable, S Subtractable](v V, distance S, meters S) S
但是,这样的更改会导致编译器 panic
,因为 Move
的入参为 int
类型,那我们将它修改为 Subtractable
。您可以点击此处获取代码。
遗憾的是,修改后它仍然无法工作,这只是我们想要完成的伪代码。编译器对我们"大吼大叫"的原因是,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
这个版本。泛型很方便,但有时很容易让事情变得有点过于复杂。仅在有实际用例时,我们再尝试使用它吧。
其他
封面来自互联网。
转载自:https://juejin.cn/post/7132768402745065479