Go语言学习之泛型

130 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第14天,点击查看活动详情

泛型允许我们的函数或数据结构采用以通用形式定义的几种类型。

为了真正理解这意味着什么,让我们看看一个非常简单的案例。

假设你需要做一个函数,您可以编写以下类型的函数:

func Print(s []string) {
	for _, v := range s {
		fmt.Print(v)
	}
}

很简单,对吧?如果我们想让切片成为一个整数呢?您需要为此制定一个新方法:

func Print(s []int) {
	for _, v := range s {
		fmt.Print(v)
	}
}

这些解决方案可能看起来是多余的,因为我们只是在更改参数。但目前,这就是我们在Go中解决这个问题的方式,而无需将其转换为某些接口。

现在,有了泛型,它们将允许我们像这样声明我们的函数:

func Print[T any](s []T) {
	for _, v := range s {
		fmt.Print(v)
	}
}

在上述函数中,我们声明了两件事:

  1. 我们有T,这是any关键字的类型(此关键字被专门定义为泛型的一部分,它表示任何类型)
  2. 还有我们的参数,其中我们有变量s,其类型是T的切片。

我们现在可以这样调用我们的方法:

func main() {
	Print([]string{"Hello, ", "playground\n"})
	Print([]int{1,2,3})
}

这只是泛型最基本的实现之一。但到目前为止,它看起来还不错。

泛型局限性

我们已经看到了泛型能做什么。他们让我们指定一个可以接受任何类型参数的函数。

但我之前给出的例子很简单。泛型能带是有限制的。例如,打印非常简单,因为Golang可以打印出扔进去的任何类型的变量。

如果我们想做更复杂的事情呢?假设我们已经为结构定义了自己的方法,并希望称之为:

package main

import (
	"fmt"
)

type worker string

func (w worker) Work(){
	fmt.Printf("%s is working\n", w)
}


func DoWork[T any](things []T) {
    for _, v := range things {
        v.Work()
    }
}

func main() {
	var a,b,c worker
	a = "A"
	b = "B"
	c = "C"
	DoWork([]worker{a,b,c})	
}

你会得到这个:

type checking failed for main
prog.go2:25:11: v.Work undefined (type bound for T has no method Work)

它无法运行,因为函数内部处理的切片是any类型,并且它没有实现方法Work,这使得它无法运行。

然而,我们实际上可以通过使用界面来让它工作:

package main

import (
	"fmt"
)

type Person interface {
    Work()
}

type worker string

func (w worker) Work(){
	fmt.Printf("%s is working\n", w)
}

func DoWork[T Person](things []T) {
    for _, v := range things {
        v.Work()
    }
}

func main() {
	var a,b,c worker
	a = "A"
	b = "B"
	c = "C"
	DoWork([]worker{a,b,c})
}

它将打印出这个:

A is working
B is working
C is working

好吧,它适用于接口,但只是一个没有泛型的接口也很好:

package main

import (
	"fmt"
)

type Person interface {
    Work()
}

type worker string

func (w worker) Work(){
	fmt.Printf("%s is working\n", w)
}

func DoWorkInterface(things []Person) {
    for _, v := range things {
        v.Work()
    }
}

func main() {
	var d,e,f worker
	d = "D"
	e = "E"
	f = "F"
	DoWorkInterface([]Person{d,e,f})
}

这将给我们以下结果:

D is working
E is working
F is working

使用泛型只会为我们的代码添加额外的逻辑。因此,如果仅使用接口就足够了,我认为没有任何理由向代码中添加泛型。

泛型仍处于早期的开发阶段,它们仍然有进行复杂处理的局限性。

约束

早些时候,我们发现了any类型的通用约束。除了这种类型,我们还可以使用其他几个约束。

其中一个约束是comparable的。让我们看看它是如何工作的:

func Equal[T comparable](a, b T) bool {
    return a == b
}

func main() {
	Equal("a","a")
}

除此之外,我们还可以尝试像这样进行自己的约束:

package main

import(
	"fmt"
)

type Number interface {
    type int, float64
}

func MultiplyTen[T Number](a T) T{
	return a*10
}

func main() {
	fmt.Println(MultiplyTen(10))
	fmt.Println(MultiplyTen(5.55))
}

我认为这很整洁——我们可以为一个简单的数学表达式有一个函数。通常,我们最终会制作两个函数来接受它,或者我们会使用反射,所以我们只写一个函数。

虽然这很酷,但我们仍然需要进行相当多的实验来制定自己的约束。现在知道他们的局限性还为时过早。我们应该小心不要滥用它,只有在我们真正确定需要它时才使用它。

除了将泛型用作函数的一部分外,您还可以将它们声明为以下变量:

type GenericSlice[T any] []T

您可以将其用作函数中的参数,也可以用该类型制作方法:

func (g GenericSlice[T]) Print() {
	for _, v := range g {
		fmt.Println(v)
	}
}

func Print [T any](g GenericSlice[T]) {
	for _, v := range g {
		fmt.Println(v)
	}
}

func main() {
	g := GenericSlice[int]{1,2,3}
	
	g.Print() //1 2 3
	Print(g) //1 2 3
}

使用情况因您的需求而异。我只能说,我们仍然需要更多地尝试泛型,看看哪些用例最有效。

结论

感谢您你的阅读!!!