首页>>后端>>Golang->Go1.18泛型浅谈

Go1.18泛型浅谈

时间:2023-12-01 本站 点击:0

引言

刷力扣经常用到min和max函数,但是众嗦粥汁,Go没有函数重载,所以每次需要提前写好一堆minInt, minFloat, maxInt的函数,真是离离原上谱!

那有没有什么方法可以实现一个方法,多种类型使用呢?别的语言有泛型来保证,Go的话,在18版本之后引入了泛型,可以实现这一需求。

使用

直接看代码:

func min[T int | float64](a, b T) T {    if a < b {        return a    } else {        return b    }}func main() {    fmt.Println(min(1, 2))    fmt.Println(min(2.7, 3.1))}

如果觉得类型约束写在方法里比较丑,还有这种方法:

type Comparable interface {    int | float64}func min[T Comparable](a, b T) T {    if a < b {        return a    } else {        return b    }}func main() {    fmt.Println(min(1, 2))    fmt.Println(min(2.7, 3.1))}

如果懒得写常见的类型封装,可以使用官方的扩展库:

import (    "fmt"    "golang.org/x/exp/constraints")func min[T constraints.Ordered](a, b T) T {    if a < b {        return a    } else {        return b    }}func main() {    fmt.Println(min(1, 2))    fmt.Println(min(2.7, 3.1))}

其中Ordered实现如下:

type Ordered interface {    Integer | Float | ~string}// Integertype Integer interface {    Signed | Unsigned}// Floattype Float interface {    ~float32 | ~float64}// Signedtype Signed interface {    ~int | ~int8 | ~int16 | ~int32 | ~int64}// Unsignedtype Unsigned interface {    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr}

上面我们对于泛型方法的调用,并没有指明调用参数的类型,但是却编译通过了,如果你想显式指出,可以这样:

func main() {    fmt.Println(min[int](1, 2))    fmt.Println(min[float64](2.7, 3.1))}

如果不指名泛型的实际类型,编译期会在编译期间触发类型推断,然后特例化泛型函数,并根据推导出来的类型去调用对应的特例化方法,这点类似C++,或者直接看下面的代码:

func main() {    iMin := min[int]    fMin := min[float64]    fmt.Println(iMin(1, 2))    fmt.Println(fMin(2.7, 3.1))}

可以看到对于int和float,编译期分别把min展开成了两个类型对应的方法,这是编译期间完成的,所以泛型方法调用看起来就是一个普通的函数调用,而没有运行时开销。

Providing the type argument to min, in this case int, is called instantiation. Instantiation happens in two steps. First, the compiler substitutes all type arguments for their respective type parameters throughout the generic function or type. Second, the compiler verifies that each type argument satisfies the respective constraint. We’ll get to what that means shortly, but if that second step fails, instantiation fails and the program is invalid.

对应的引用在这里。

此外,泛型亦可作用于类型,同时可以对类型特例化:

type add[T constraints.Ordered] struct {    v T}func (a *add[T]) run(val T) T {    return a.v + val}func (a *add[T]) print() {    fmt.Println(a.v)}// 特例化addtype sAdd add[string]type iAdd add[int]func main() {    s := &add[string]{v: "Hello"}    i := &add[int]{v: 1}    s.print()    fmt.Println(i.run(2))}

解释

现在我们来解释一些用法。

Go的泛型使用[]的原因在于避免<>对于大于和小于产生的歧义。此外,Go的泛型无法直接标注,需要指出泛型类型的约束。最大级别的约束即空接口interface{},如果你曾经学习过Java,就知道Java的所有对象都继承自Object,二者可以比较理解。

Go对于类型约束的写法有两种,一种是直接写在[]中,另一种则是定义一个约束接口,然后通过接口指定约束集合。

比如我们期待一个类型只能是无符号数,则可以这样写:

func f[T uint8 | uint16 | uint32 | uint64 | uintptr | uint](v T) {}

但是这样很难实现复用,所以可以把这个约束提到一个接口中,然后通过接口来约束:

type unsigned interface {    uint8 | uint16 | uint32 | uint64 | uintptr | uint}func f[T unsigned](v T) {}func ff[T unsigned](v1, v2 T) {}

这也是扩展包中Unsigned的实现方式。但是扩展包中对于每个类型,其前面多了一个~符号。假如有T ~int,则表示T的约束不仅仅是int,所有底层类型为int的自定义类型亦可满足T的约束:

type int0 intfunc add[T ~int](v1, v2 T) T {    return v1 + v2}func main() {    var v1, v2 int0    v1 = 1    v2 = 2    fmt.Println(add(v1, v2))}

接口:旧瓶装新酒?

这里来解释为什么接口可以做到类型约束。

首先,接口是怎么定义的?接口的定义为方法的集合。任何实现了接口所有方法的类型,都称为实现了该接口: 如上图所示,类型P,Q,R... ...都实现了接口。但是从另一种层面来说,接口也定义了一个类型集合,集合中的元素都实现了这一接口。 此时接口的语义转变成了类型的集合。这是我们如何理解约束条件通过接口组织的关键。

既然接口可以理解成类型的集合,那我们为何不直接在接口里放类型呢? 这样一来就完成通过接口对类型的约束了。当然,类型集合的接口也可以存在方法。此外,如果接口约束很简单的话,也可以使用单行写法:

type Comparable interface {    int | float64}func min[T Comparable](a, b T) T {    if a < b {        return a    } else {        return b    }}func main() {    fmt.Println(min(1, 2))    fmt.Println(min(2.7, 3.1))}0

约束推断

现在我们来看一个场景:

type Comparable interface {    int | float64}func min[T Comparable](a, b T) T {    if a < b {        return a    } else {        return b    }}func main() {    fmt.Println(min(1, 2))    fmt.Println(min(2.7, 3.1))}1

这是一段简单的对切片元素扩大N倍的代码。现在我们有一个自定义类型:

type Comparable interface {    int | float64}func min[T Comparable](a, b T) T {    if a < b {        return a    } else {        return b    }}func main() {    fmt.Println(min(1, 2))    fmt.Println(min(2.7, 3.1))}2

可是这是为什么呢?接着看:

type Comparable interface {    int | float64}func min[T Comparable](a, b T) T {    if a < b {        return a    } else {        return b    }}func main() {    fmt.Println(min(1, 2))    fmt.Println(min(2.7, 3.1))}3

在这里,我们仅仅更改了泛型函数的返回值类型为切片的类型,而非切片元素类型的切片([]E -> T),所以保留了T的完整信息,包括它的方法,此时方法调用成功。

当然,我们强转一下也是可以的:

type Comparable interface {    int | float64}func min[T Comparable](a, b T) T {    if a < b {        return a    } else {        return b    }}func main() {    fmt.Println(min(1, 2))    fmt.Println(min(2.7, 3.1))}4

现在来思考一个问题,为什么类型推断成功了?明明我们没有指定factor的类型,而Integer有int, int32, int64等多种类型,单靠输入的数字'2'是无法确定的不是吗?

答案是factor和slice的元素是同一类型,而我们可以通过推断slice的类型确定元素类型,进而确定factor的类型,这就是约束推断。

上述只是一个引言。Go1.18的约束推断比较复杂,这里留坑,并放入链接。

引用

An Introduction To Generics

Type Parameters Proposal

原文:https://juejin.cn/post/7096373901810204679


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/Golang/5875.html