跳转到主要内容

泛型。

在 Go 语言中添加泛型必须是 Go 社区最具争议的话题之一。

从一开始,我就喜欢 Go 的明确性和为我作为开发人员提供的简单性。我知道,查看函数签名,我将期望在该函数的主体中使用什么类型,并且我通常会知道要注意什么。

添加泛型后,我们的代码库变得更加复杂。我们不再有那种简单明了的东西,我们必须做一些推理和挖掘才能真正知道传递给我们的新函数的是什么。

概述


现在,本文的目的不是争论该语言最新添加的细节,而是尝试为您提供所需的一切,以便在您自己的 Go 应用程序中启动和运行泛型.

入门


在我们开始之前,您需要在本地机器上安装 go1.18beta1。如果你已经安装了 go,你可以通过运行:

$ go install golang.org/dl/go1.18beta1@latest
$ go1.18beta1 download


成功运行这两个命令后,您应该能够在终端中运行 go1.18beta1:

$ go1.18beta1 
go version go1.18beta1 darwin/amd64


完美,您现在可以编译和运行通用 Go 代码!

编写泛型函数


让我们从看看如何在 Go 中构建自己的泛型函数开始。传统上,您将从一个函数签名开始,该签名将明确说明此函数期望作为参数的类型:

func oldNonGenericFunc(myAge int64) {
    fmt.Println(myAge)
}


在新世界中,如果我们想创建一个可以接受 int64 或 float64 类型的函数,我们可以像这样修改我们的函数签名:

main.go

package main

import "fmt"

func newGenericFunc[age int64 | float64](myAge age) {
	fmt.Println(myAge)
}

func main() {
	fmt.Println("Go Generics Tutorial")
	var testAge int64 = 23
	var testAge2 float64 = 24.5

	newGenericFunc(testAge)
	newGenericFunc(testAge2)
}

现在让我们尝试运行它,看看会发生什么:

$ go1.18beta1 run main.go
Go Generics Tutorial
23
24.5


所以,让我们分解一下我们在这里所做的事情。我们已经有效地创建了一个名为 newGenericFunc 的通用函数。

在我们的函数名称之后,我们打开方括号 [] 并指定我们可以合理地期望我们的函数被调用的类型:

[age int64 | float64]


当我们在括号中定义函数的参数时,我们说变量 myAge 可以是 age 类型,随后可以是 int64 或 float64 类型。

注意 - 如果我们想添加更多类型,我们可以使用 | 列出更多类型。不同类型之间的分隔符。

使用任何类型


在上面的示例中,我们指定了一个可以采用 int64 或 float64 类型的泛型函数,但是,如果我们想要定义一个可以接受任何类型的函数怎么办?

好吧,为了实现这一点,我们可以使用新的内置任何类型,如下所示:

any.go

package main

import "fmt"

func newGenericFunc[age any](myAge age) {
	fmt.Println(myAge)
}

func main() {
	fmt.Println("Go Generics Tutorial")
	var testAge int64 = 23
	var testAge2 float64 = 24.5

    var testString string = "Elliot"

	newGenericFunc(testAge)
	newGenericFunc(testAge2)
	newGenericFunc(testString)
}

有了这些更改,让我们现在尝试运行它:

$ go1.18beta1 run any.go 
Go Generics Tutorial
23
24.5
Elliot


如您所见,go 编译器成功编译并运行了我们的新代码。我们已经能够传入我们喜欢的任何类型,而不会出现任何编译器错误。

现在,您可能会注意到此代码存在问题。在上面的示例中,我们创建了一个新的通用函数,它将接收一个 Age 值并将其打印出来。然后我们有点厚颜无耻,将一个字符串值传递给这个通用函数,幸运的是,这一次,我们的函数已经处理了这个输入,没有任何问题。

让我们看一个可能引发问题的案例。让我们更新我们的代码以对 myAge 参数进行一些额外的计算。我们将尝试将其强制转换为 int,然后将值加 1:

generic_issue.go

package main

import "fmt"

func newGenericFunc[age any](myAge age) {
	val := int(myAge) + 1
	fmt.Println(val)
}

func main() {
	fmt.Println("Go Generics Tutorial")
	var testAge int64 = 23
	var testAge2 float64 = 24.5

    var testString string = "Elliot"

	newGenericFunc(testAge)
	newGenericFunc(testAge2)
    newGenericFunc(testString)
}

现在,当我们尝试构建或运行这段代码时,我们应该看到它编译失败:

$ go1.18beta1 run generic_issue.go
# command-line-arguments
./generic_issue.go:6:13: cannot convert myAge (variable of type age constrained by any) to type int



在这种情况下,我们无法尝试将 any 类型强制转换为 int。解决这个问题的唯一方法是更明确地传递传入的类型,如下所示:

package main

import "fmt"

func newGenericFunc[age int64 | float64](myAge age) {
	val := int(myAge) + 1
	fmt.Println(val)
}

func main() {
	fmt.Println("Go Generics Tutorial")
	var testAge int64 = 23
	var testAge2 float64 = 24.5

	newGenericFunc(testAge)
	newGenericFunc(testAge2)
}

在大多数情况下,在我们可以合理使用的类型中更加明确是有利的,因为它可以让您对如何处理每个单独的类型进行最深思熟虑和深思熟虑。

$ go1.18beta1 run generic_issue.go
Go Generics Tutorial
24
25


显式传递类型参数


在大多数情况下,Go 将能够推断您传递给泛型函数的参数的类型。但是,在某些情况下,您可能希望更加慎重并指定要传递给这些通用函数的参数的类型。

为了更明确,我们可以使用相同的 [] 括号语法来说明正在传递的参数的类型:

newGenericFunc[int64](testAge)


这将明确声明 testAge 变量在传递到此 newGenericFunc 时将是 int64 类型。

类型约束


让我们看看如何在 Go 中修改代码并声明类型约束。

在这个例子中,我们将把泛型函数可以接受的类型移动到我们标记为 Age 的接口中。然后我们在 newGenericFunc 中使用了这个新的类型约束,如下所示:

package main

import "fmt"

type Age interface {
	int64 | int32 | float32 | float64 
}

func newGenericFunc[age Age](myAge age) {
	val := int(myAge) + 1
	fmt.Println(val)
}

func main() {
	fmt.Println("Go Generics Tutorial")
	var testAge int64 = 23
	var testAge2 float64 = 24.5

	newGenericFunc(testAge)
	newGenericFunc(testAge2)
}

当我们运行它时,我们应该看到:

$ go1.18beta1 run type_constraints.go
Go Generics Tutorial
24
25


现在,这种方法的美妙之处在于,我们可以在整个代码中重用这些相同的类型约束,就像我们在 Go 中使用任何其他类型一样。

更复杂的类型约束


让我们看一个稍微复杂一点的用例。例如,假设我们想要创建一个 getSalary 函数,该函数将接收满足给定类型约束的任何内容。我们可以通过定义一个接口然后将其用作泛型函数的类型约束来实现这一点:

package main

import "fmt"

type Employee interface {
	PrintSalary() 
}

func getSalary[E Employee](e E) {
	e.PrintSalary()
}

type Engineer struct {
	Salary int32
}

func (e Engineer) PrintSalary() {
	fmt.Println(e.Salary)
}

type Manager struct {
	Salary int64
}

func (m Manager) PrintSalary() {
	fmt.Println(m.Salary)
}


func main() {
	fmt.Println("Go Generics Tutorial")
	engineer := Engineer{Salary: 10}
	manager := Manager{Salary: 100}

	getSalary(engineer)
	getSalary(manager)
}

在本例中,我们随后指定我们的 getSalary 函数具有类型约束 E,它必须实现我们的 Employee 接口。在我们的 main 函数中,我们定义了一个工程师和一个经理,并将这两个不同的结构传递给 getSalary 函数,即使它们都是不同的类型。

$ go1.18beta1 run complex_type_constraints.go
Go Generics Tutorial
10
100


现在这是一个有趣的例子,它展示了我们如何对泛型函数进行类型约束以仅接受实现此 PrintSalary 接口的类型,然而,同样可以通过直接在函数签名中使用该接口来实现,如下所示:

func getSalary(e Employee) {
	e.PrintSalary()
}


这是相似的,因为在 Go 中使用接口是一种泛型编程,理解方法之间的差异和一种方法的好处可能在官方 go.dev 题为“为什么泛型”的帖子中得到更好的解释.

泛型的好处


到目前为止,我们刚刚介绍了在 Go 中编写通用代码时会遇到的基本语法。让我们将这些新发现的知识更进一步,看看这段代码在我们自己的 Go 应用程序中的哪些方面是有益的。

让我们看一下标准的 BubbleSort 实现:

func BubbleSort(input []int) []int {
    n := len(input)
    swapped := true
    for swapped {
        // set swapped to false
        swapped = false
        // iterate through all of the elements in our list
        for i := 0; i < n-1; i++ {
            // if the current element is greater than the next
            // element, swap them
            if input[i] > input[i+1] {
                // log that we are swapping values for posterity
                fmt.Println("Swapping")
                // swap values using Go's tuple assignment
                input[i], input[i+1] = input[i+1], input[i]
                // set swapped to true - this is important
                // if the loop ends and swapped is still equal
                // to false, our algorithm will assume the list is
                // fully sorted.
                swapped = true
            }
        }
    }
}

现在,在上面的实现中,我们已经定义了这个 BubbleSort 函数必须接受一个 int 类型的切片。例如,如果我们尝试使用 int32 类型的 slice 运行它,我们会得到一个编译器错误:

cannot use list (variable of type []int32) as type []int in argument to BubbleSort


让我们看看我们如何使用泛型编写它并打开输入以接受所有 int 类型和 float 类型:

package main

import "fmt"

type Number interface {
	int16 | int32 | int64 | float32 | float64 
}

func BubbleSort[N Number](input []N) []N {
	n := len(input)
	swapped := true
	for swapped {
		swapped = false
		for i := 0; i < n-1; i++ {
		  	if input[i] > input[i+1] {
				input[i], input[i+1] = input[i+1], input[i]
				swapped = true
		  	}
		}
	}
	return input
}

func main() {
	fmt.Println("Go Generics Tutorial")
	list := []int32{4,3,1,5,}
	list2 := []float64{4.3, 5.2, 10.5, 1.2, 3.2,}
	sorted := BubbleSortGeneric(list)
	fmt.Println(sorted)

	sortedFloats := BubbleSortGeneric(list2)
	fmt.Println(sortedFloats)
}

通过对我们的 BubbleSort 函数进行这些更改并接受类型约束的数字,如果我们想要支持每个 int 和 float 类型,我们就可以有效地减少我们必须编写的代码量!

现在让我们尝试运行它:

$ go1.18beta1 run bubblesort.go
Go Generics Tutorial
[1 3 4 5]
[1.2 3.2 4.3 5.2 10.5]


现在这个例子应该可以证明这个新概念在你的 Go 应用程序中是多么强大。

结论


太棒了,所以在本教程中,我们介绍了 Go 中泛型的基础知识!我们已经了解了如何定义泛型函数并做一些很酷的事情,例如使用类型约束来确保我们的泛型函数不会变得过于疯狂。

文章链接