你想知道的 Go 泛型都在这里

泛型现在进展如何?这个友好而实用的教程将解释泛型函数和类型是什么,为什么我们需要它们,它们在 Go 中如何工作,以及我们可以在哪里使用它们。这是非常简单有趣的,让我们开始吧!

John Arundel 是一位 Go 语言的老师兼顾问,也是《For the Love of Go》一书的作者。这是一套关于现代软件工程在 Go 语言中实践的电子书,完全面向初学者。 

084b56a0b7e76d19d58cc826c50e05e0.png

 《For the Love of Go》是一系列有趣并且容易理解的电子书,专门介绍软件工程在 Go 语言中的实践。

什么是泛型

大家都知道, Go 是一种 强类型 语言,这意味着程序中的每个变量和值都有特定的类型,如 intstring 。当我们编写函数时,我们需要在所谓的 函数签名 中指定它们的形参类型,像这样:

func PrintString(s string) {

这里,形参 s 的类型是 string 。我们可以想象编写这个函数接受 intfloat64 、任意结构类型等形参的版本。但是当需要处理的不仅仅是这些明确类型时,多少是不太方便的,尽管我们有时可以使用 接口 来解决这个问题(例如 map[string]interface 教程 中所描述),但这种方法也有很多局限性。

Go 泛型函数

相反,现在我们可以声明一个 泛型函数 PrintAnything,它接受一个表示任意类型的 any 参数(我们称它为T ),并使用它做一些事情。

这是它看起来的样子:

func PrintAnything[T any](thing T) {

很简单对吧?这里的 any 表示T 可以是任何类型。

我们怎么样调用这个函数?这也同样很简单:

PrintAnything("Hello!")

注意:我在这里描述的对 Go 泛型的支持还没有发布,但它 正在实现中 ,很快就会发布。现在你可以在 支持泛型的 Go Playground 中使用它,或者在你的项目中使用实验性的 go2go 工具 来尝试获得 Go 泛型支持。

约束

要实现 PrintAnything 函数其实非常容易,因为 fmt 库就可以打印任何东西。假设我们想实现我们自己版本的 strings.Join 函数,它接受一个 T 类型的切片,并返回一个将它们连接在一起的字符串。让我们来试一试:

// 我有一种不好的预感 func Join[T any](things []T) (result string) { for _, v := range things { result += v.String() } return result }我们已经创建了一个泛型函数 Join() ,它接受一个任意类型 T 的切片参数。很好,但是现在我们遇到了一个问题:

output := Join([]string{"a", "b", "c"})
// v.String 没有被定义(绑定的类型 T 没有 String 方法)

也就是说在 Join() 函数中,我们想对每个切片元素 v 调用 .String()方法 ,将其转换为 string 。但是 Go 需要能够提前检查 T 类型是否有 String()方法,然而它并不知道 T 是什么,所以它不能直接调用!

我们需要做的是稍微地约束下 T 类型。实际上我们只对具有 String() 方法的类型感兴趣,而不是直接接受任何类型的 T 。任何具有这种方法的类型才能作为 Join() 函数的输入,那么我们如何用 Go 表达这个约束呢?我们可以使用一个 接口 :

type Stringer interface {
    String() string
}

当给定类型实现了 String() 方法,现在我们就可以把这个约束应用到泛型函数的类型上:

func Join[T Stringer] ...

因为Stringer保证了任何类型T的值都有 String() 方法,Go 现在很乐意让我们在函数内部调用它。但是,如果你尝试使用某个未实现 Stringer 类型的切片(例如 int )来调用 Join() 方法时 ,Go 将会抱怨:

result := Join([]int{1, 2, 3})
// int 未实现 Stringer 接口(未找到 String 方法)

可比较的约束

基于方法集的约束(如 Stringer)是有用的,但如果我们想对我们的泛型输入做一些不涉及方法调用的事情呢?

例如,假设我们想编写一个 Equal 函数,它接受两个 T类型的形参,如果它们相等则返回 true ,否则返回 false 。让我们试一试:

// 这将不会有效
func Equal[T any](a, b T) bool {
    return a == b
}

fmt.Println(Equal(1, 1))
// 不能比较 a == b (类型 T 没有定义操作符 == )

这与在 Join() 中使用 String() 方法遇到的问题相同,但由于我们现在没有直接调用方法,所以不能使用基于方法集的约束。相反,我们需要将T 约束为可使用 ==!= 操作符,这被称为 可比较 类型。幸运的是,有一种直接的方式来指定这种类型:使用内置的 comparable 约束,而不是 any

func Equal[T comparable] ...

constraints 包

增加点难度,假设我们想用 T的值做一些事情,既不比较它们也不调用它们的方法。例如,假设我们想为泛型 T 类型编写一个 Max() 函数,它接受 T 的一个切片,并返回切片元素中的最大值。我们可以尝试这样做:

// Nope.
func Max[T any](input []T) (max T) {
    for _, v := range input {
        if v > max {
            max = v
        }
    }
    return max
}

我对此不太乐观,但让我们看看会发生什么:

fmt.Println(Max([]int{1, 2, 3}))
// 不能比较 v > max ( T 类型没有定义操作符 > )

同样,Go 不能提前验证 T类型可以使用 > 操作符(也就是说,T 是 有序的 )。我们如何解决这个问题?我们可以简单地在约束中列出所有可能允许的类型,像这样(称为 列表类型 ):

type Ordered interface {
    type int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        string
}

幸运的是,在标准库的 constraints 包中已经为我们定义了一些实用的约束条件,所以我们只需要动动键盘就可以导入并像这样来使用:

func Max[T constraints.Ordered] ...问题解决了!

泛型类型

到目前为止,一切都很酷。我们知道如何编写可以接受任何类型参数的函数。但是如果我们想要创建一个可以包含任何类型的类型呢?例如,一个 “任意类型的切片” 。这其实也很简单:

type Bunch[T any] []T

这里指对于任何给定的T类型 , Bunch[T]T类型的切片。例如, Bunch[int]int 的切片。我们可以用常规的方法来创建该类型的值:

x := Bunch[int]{1, 2, 3}

正如你所期望的,我们可以编写接受泛型类型的泛型函数:

func PrintBunch[T any](b Bunch[T]) {

方法也同样可以:

func (b Bunch[T]) Print() {

我们也可以对泛型类型施加约束:

type StringableBunch[T Stringer] []T

视频:Code Club: Generics

泛型 Golang playground Go 团队提供了一个支持泛型的 Go Playground 版本,你可以在上面使用当前泛型提案的实现(例如尝试本教程中的代码示例)。

泛型 Golang Playground

它的工作方式与我们所了解和喜爱的普通 Go Playground 完全相同,只是它支持本文描述的泛型语法。由于在 Playground 中不可能运行所有的 Go 代码(例如网络调用或者访问文件系统的代码),你可以尝试使用 go2go 工具,它可以将使用泛型的代码翻译成当前 Go 版本能编译的代码。

Q&A

Go 泛型提案是什么

你可以在这里阅读完整的设计文档草稿:

类型参数 - 设计草稿

Golang 会支持泛型吗

是的。正如本教程的概述,在 Go 中目前对于支持泛型的提案已经在 2020 年 6 月一篇博客文章:泛型的下一阶段 中宣布了。并且这篇 Github issue (关于新增上文所描述形式的泛型)也已经被接受了。

Go 博客 表示,在 Go 1.18 的测试版本可能会包含对泛型的支持,该测试版本将于 2021 年 12 月发布。

在此之前,你可以使用 泛型 Playground 来试验它,并尝试运行此文的示例。

泛型 vs 接口:这是泛型的另一种选择吗

正如我在 map[string]interface 教程 中提到的,我们可以通过 接口 来编写 Go 代码处理任何类型的值,而不需要使用泛型函数或类型。但是,如果你想编写实现任意类型的集合之类的库,那么使用泛型类型要比使用接口简单得多,也方便得多。

any 因何而来

当定义泛型函数或类型时,输入类型必须有一个约束。类型约束可以是接口(如 Stringer )、列表类型(如 constraints.ordered)或关键字 comparable。但如果你真的不想要约束,也就是说,像字面意义上的 任何 T 类型 ?

符合逻辑的方法是使用 interface{} (接口对类型的方法集没有任何限制)来表达。由于这是一个常见的约束,所以预先声明关键字 any 被提供来作为 interface{} 的别名。但是你只能在类型约束中使用这个关键字,所以 any 并不是等价于 interface{}

我可以使用代码生成器代替泛型吗

在 Go 的泛型出现之前,“代码生成器” 方法是处理此类问题的另一种传统方法。本质上,针对每种你的库中需要处理的特定类型,它都需要使用 go 生成器工具 产生新的 Go 代码。

这虽然可行,但使用起来很笨拙,它的灵活性受到限制,并且需要额外的构建步骤。虽然代码生成器在某些情况下仍然有用,但我们不再需要使用它来模拟 Go 中的泛型函数和类型。

什么是合约

早期的 设计草案 中泛型使用了与我们今天相似的语法,但是它使用了一个新的关键字 contract 来实现类型约束,而非现有的 interface 。由于种种原因,它不太受欢迎,现在已经被废弃了。

Further reading 延伸阅读

  • 一个增加泛型的提案(https://go.dev/blog/generics-proposal)

  • 泛型的下一阶段(https://go.dev/blog/generics-next-step)

  • 为什么使用泛型?(https://go.dev/blog/why-generics)

  • Go 泛型:将设计草案应用到真实的用例中(https://secrethub.io/blog/go-generics/)

  • 在 Go 中尝试泛型(https://medium.com/swlh/experimenting-with-generics-in-go-39ffa155d6a1)

原文地址:https://bitfieldconsulting.com/golang/generics

原文作者:John Arundel

本文永久链接:https://github.com/gocn/translator/blob/master/2021/w13_Generics_in_Go.md

译者:haoheipi

校对:

想要了解更多资讯,还可以入群和大家一起畅聊哦~

146b13c31feb923c7d3f763aec8f96c7.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在Golang中,泛型接口是一种接口类型,可以用于处理不同类型的数据。泛型接口在编程中非常有用,因为它允许我们编写可重用、灵活的代码,而无需针对特定类型进行硬编码。 在Golang中,泛型接口可以通过使用空接口(`interface{}`)来实现。空接口是一个没有任何方法的接口,可以接受任何类型的值。通过使用空接口,可以实现泛型的功能,使得接口可以接收任何类型的数据。 使用泛型接口,我们可以在不改变接口定义的情况下,接受不同类型的参数。例如,我们可以定义一个泛型接口`Container`,用于表示一个可以容纳不同类型元素的容器。这个接口可以定义添加元素、删除元素以及获取元素等方法。 使用泛型接口的好处是可以编写灵活的代码,尽可能减少重复代码的编写。由于泛型接口可以处理多种类型的数据,我们可以将相同的逻辑应用于不同的数据类型,实现代码的重用。 然而,目前Golang没有原生支持泛型接口的功能,因此在实现泛型接口时可能需要一些额外的代码处理。一种常见的做法是使用类型断言来判断接口的实际类型,然后进行相应的操作。 总而言之,虽然Golang没有内置的泛型功能,但通过使用空接口和类型断言,我们可以实现泛型接口从而处理不同类型的数据,提高代码的重用性和灵活性。 ### 回答2: Go语言是一种静态类型的编程语言,其最近的版本Go 1.18中引入了泛型接口的概念。泛型指的是在编写代码时不指定具体类型,而是允许使用者在使用时根据自己的需求来指定具体的类型。 在传统的面向对象编程中,常用的接口表示方式是通过接口类型断言来判断对象是否实现了某个接口。但是这种方式在处理不同类型的数据时需要进行类型转换,不够灵活且有一定的性能损耗。 而泛型接口则可以在接口定义时使用类型参数,通过类型参数来指定接口的具体类型。这样一来,在使用时就可以直接将对应类型的数据传入接口中,无需进行类型转换。 泛型接口的引入为Go语言提供了更加灵活和高效的编程方式。通过泛型接口,我们可以编写更加通用和复用的代码。它还能帮助我们更好地约束函数和数据类型之间的关系,提高代码的健壮性和可读性。 不过需要注意的是,泛型接口的引入也会带来一定的复杂性。在使用泛型接口时,我们需要仔细考虑类型参数的合理性和边界条件,并且需要充分测试以确保代码的正确性。 总之,引入泛型接口是Go语言进一步发展的一大步。它提供了更多的编程方式,并且可以在一定程度上简化代码和提高效率。希望未来随着泛型接口的进一步成熟和普及,我们可以看到更多高质量、灵活和通用的Go代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值