Go 泛型的这 3 个核心设计,你都知道吗?

小鸠儿
• 阅读 4076

大家好,我是煎鱼。

Go1.18 的泛型是闹得沸沸扬扬,虽然之前写过很多篇针对泛型的一些设计和思考。但因为泛型的提案之前一直还没定型,所以就没有写完整介绍。

如今已经基本成型,就由煎鱼带大家一起摸透 Go 泛型。本文内容主要涉及泛型的 3 大概念,非常值得大家深入了解。

如下:

  • 类型参数。
  • 类型约束。
  • 类型推导。

类型参数

类型参数,这个名词。不熟悉的小伙伴咋一看就懵逼了。

泛型代码是使用抽象的数据类型编写的,我们将其称之为类型参数。当程序运行通用代码时,类型参数就会被类型参数所取代。也就是类型参数是泛型的抽象数据类型

简单的泛型例子:


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

代码有一个 Print 函数,它打印出一个片断的每个元素,其中片断的元素类型,这里称为 T,是未知的。

这里引出了一个要做泛型语法设计的点,那就是:T 的泛型类型参数,应该如何定义

在现有的设计中,分为两个部分:

  • 类型参数列表:类型参数列表将会出现在常规参数的前面。为了区分类型参数列表和常规参数列表,类型参数列表使用方括号而不是小括号。
  • 类型参数约束:如同常规参数有类型一样,类型参数也有元类型,被称为约束(后面会进一步介绍)。

结合完整的例子如下:

// Print 可以打印任何片断的元素。
// Print 有一个类型参数 T,并有一个单一的(非类型)的 s,它是该类型参数的一个片断。
func Print[T any](s []T) {
    // do something...
}

在上述代码中,我们声明了一个函数 Print,其有一个类型参数 T,类型约束为 any,表示为任意的类型,作用与 interface{} 一样。他的入参变量 s 是类型 T 的切片。

函数声明完了,在函数调用时,我们需要指定类型参数的类型。如下:

    Print[int]([]int{1, 2, 3})

在上述代码中,我们指定了传入的类型参数为 int,并传入了 []int{1, 2, 3} 作为参数。

其他类型,例如 float64:

    Print[float64]([]float64{0.1, 0.2, 0.3})

也是类似的声明方式,照着套就好了。

类型约束

说完类型参数,我们再说说 “约束”。在所有的类型参数中都要指定类型约束,才能叫做完整的泛型。

以下分为两个部分来具体展开讲解:

  • 定义函数约束。
  • 定义运算符约束。

为什么要有类型约束

为了确保调用方能够满足接受方的程序诉求,保证程序中所应用的函数、运算符等特性能够正常运行。

泛型的类型参数,类型约束,相辅相成。

定义函数约束

问题点

我们看看 Go 官方所提供的例子:

func Stringify[T any](s []T) (ret []string) {
    for _, v := range s {
        ret = append(ret, v.String()) // INVALID
    }
    return ret
}

该方法的实现目的是:任何类型的切片都能转换成对应的字符串切片。但程序逻辑里有一个问题,那就是他的入参 T 是 any 类型,是任意类型都可以传入。

其内部又调用了 String 方法,自然也就会报错,因为只像是 int、float64 等类型,就可能没有实现该方法。

你说要定义有效的类型约束,那像是上面的例子,在泛型中如何实现呢?

要求传入方要有内置方法,就得定义一个 interface 来约束他。

单个类型

例子如下:

type Stringer interface {
    String() string
}

在泛型方法中应用:

func Stringify[T Stringer](s []T) (ret []string) {
    for _, v := range s {
        ret = append(ret, v.String())
    }
    return ret
}

再将 Stringer 类型放到原有的 any 类型处,就可以实现程序所需的诉求了。

多个类型

如果是多个类型约束。例子如下:

type Stringer interface {
    String() string
}

type Plusser interface {
    Plus(string) string
}

func ConcatTo[S Stringer, P Plusser](s []S, p []P) []string {
    r := make([]string, len(s))
    for i, v := range s {
        r[i] = p[i].Plus(v.String())
    }
    return r
}

与常规的入参、出参类型声明一样的规则。

定义运算符约束

完成了函数约束的定义后,剩下一个要啃的大骨头就是 “运算符” 的约束了。

问题点

我们看看 Go 官方的例子:

func Smallest[T any](s []T) T {
    r := s[0] // panic if slice is empty
    for _, v := range s[1:] {
        if v < r { // INVALID
            r = v
        }
    }
    return r
}

经过上面的函数例子,我们很快能意识到这个程序根本无法运行成功。

其入参是 any 类型,程序内部是按 slice 类型来获取值,且在内部又进行运算符比较,那如果真是 slice,内部就可能每个值类型都不一样。

如果一个是 slice,一个是 int 类型,又如何进行运算符的值对比?

近似元素

可能有的同学想到了重载运算符,但...想太多了,Go 语言没有支持的计划。为此做了一个新的设计,那就是允许限制类型参数的类型范围。

语法如下:

InterfaceType  = "interface" "{" {(MethodSpec | InterfaceTypeName | ConstraintElem) ";" } "}" .
ConstraintElem = ConstraintTerm { "|" ConstraintTerm } .
ConstraintTerm = ["~"] Type .

例子如下:

type AnyInt interface{ ~int }

上述声明的类型集是 ~int,也就是所有类型为 int 的类型(如:int、int8、int16、int32、int64)都能够满足这个类型约束的条件。

包括底层类型是 int8 类型的,例如:

type AnyInt8 int8

也就是在该匹配范围内的。

联合元素

如果希望进一步缩小限定类型,可以结合分隔符来使用,用法为:

type AnyInt interface{
 ~int8 | ~int64
}

就可以将类型集限定在 int8 和 int64 之中。

实现运算符约束

基于新的语法,结合新的概念联合和近似元素,可以把程序改造一下,实现在泛型中的运算符的匹配。

类型约束的声明,如下:

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

应用的程序如下:

func Smallest[T Ordered](s []T) T {
    r := s[0] // panics if slice is empty
    for _, v := range s[1:] {
        if v < r {
            r = v
        }
    }
    return r
}

确保了值均为基础数据类型后,程序就可以正常运行了。

类型推导

程序员写代码,一定程度的偷懒是必然的。

在一定的场景下,可以通过类型推导来避免明确地写出一些或所有的类型参数,编译器会进行自动识别。

建议复杂函数和参数能明确是最好的,否则读代码的同学会比较麻烦,可读性和可维护性的保证也是工作中重要的一点。

参数推导

函数例子。如下:

func Map[F, T any](s []F, f func(F) T) []T { ... }

公共代码片段。如下:

var s []int
f := func(i int) int64 { return int64(i) }
var r []int64

明确指定两个类型参数。如下:

r = Map[int, int64](s, f)

只指定第一个类型参数,变量 f 被推断出来。如下:

r = Map[int](s, f)

不指定任何类型参数,让两者都被推断出来。如下:

r = Map(s, f)

约束推导

神奇的在于,类型推导不仅限与此,连约束都可以推导。

函数例子,如下:

func Double[E constraints.Number](s []E) []E {
    r := make([]E, len(s))
    for i, v := range s {
        r[i] = v + v
    }
    return r
}

基于此的推导案例,如下:

type MySlice []int

var V1 = Double(MySlice{1})

MySlice 是一个 int 的切片类型别名。变量 V1 的类型编译器推导后 []int 类型,并不是 MySlice。

原因在于编译器在比较两者的类型时,会将 MySlice 类型识别为 []int,也就是 int 类型。

要实现 “正确” 的推导,需要如下定义:

type SC[E any] interface {
    []E 
}

func DoubleDefined[S SC[E], E constraints.Number](s S) S {
    r := make(S, len(s))
    for i, v := range s {
        r[i] = v + v
    }
    return r
}

基于此的推导案例。如下:

var V2 = DoubleDefined[MySlice, int](MySlice{1})

只要定义显式类型参数,就可以获得正确的类型,变量 V2 的类型会是 MySlice。

那如果不声明约束呢?如下:

var V3 = DoubleDefined(MySlice{1})

编译器通过函数参数进行推导,也可以明确变量 V3 类型是 MySlice。

总结

今天我们在文章中给大家介绍了泛型的三个重要概念,分别是:

  • 类型参数:泛型的抽象数据类型。
  • 类型约束:确保调用方能够满足接受方的程序诉求。
  • 类型推导:避免明确地写出一些或所有的类型参数。

在内容中也涉及到了联合元素、近似元素、函数约束、运算符约束等新概念。本质上都是基于三个大概念延伸出来的新解决方法,一环扣一环。

你学会 Go 泛型了吗,设计的如何,欢迎一起讨论:)

若有任何疑问欢迎评论区反馈和交流,最好的关系是互相成就,各位的点赞就是煎鱼创作的最大动力,感谢支持。

文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blog 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。

参考

点赞
收藏
评论区
推荐文章
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
3年前
java 泛型详解
对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下。本文参考java泛型详解、Java中的泛型方法、java泛型详解1\.概述泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。什么是泛型?为什么要使用泛型?泛型,即“参数化类型”。一提到参数,最熟
浪人 浪人
4年前
死磕Java泛型(一篇就够)
Java泛型,算是一个比较容易产生误解的知识点,因为Java的泛型基于擦除实现,在使用Java泛型时,往往会受到泛型实现机制的限制,如果不能深入全面的掌握泛型知识,就不能较好的驾驭使用泛型,同时在阅读开源项目时也会处处碰壁,这一篇就带大家全面深入的死磕Java泛型。泛型擦除初探相信泛型大家都使用过,所以一些基础的知识点就不废话了,以免显得啰嗦。
浪人 浪人
4年前
java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一
java泛型详解绝对是对泛型方法讲解最详细的,没有之一对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下。本文参考、、1、概述泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。什么是泛型?
Wesley13 Wesley13
3年前
Java泛型详解
引言Java泛型是jdk1.5中引入的一个新特性,泛型提供了编译时的类型检测机制,该机制允许程序员在编译时检测到非法的类型。泛型是Java中一个非常重要的知识点,在Java集合类框架中泛型被广泛应用。本文我们将从零开始来看一下Java泛型的设计,将会涉及到通配符处理,以及让人苦恼的类型擦除。泛型基础
Easter79 Easter79
3年前
Thinking in java Chapter15 泛型
1与C比较2简单泛型泛型类3泛型接口4泛型方法5匿名内部类6构建复杂模型78910“泛型”意思就是:适用于许多许多的类型<h2id"1"1与C比较</h2C
Stella981 Stella981
3年前
20175209 《Java程序设计》第八周学习总结
20175209《Java程序设计》第八周学习总结一、教材知识点总结1.泛型1.泛型类声明:格式classPeople<EPeople是泛型类名称E是泛型列表,可以是任何对象或接口,但不能是基本类型数据
Wesley13 Wesley13
3年前
JAVA泛型的简单思考一
对于熟悉JAVA语言的coder来说,泛型绝对曾让自己伤透脑筋,因为java中的泛型就像是一个糖果,但嚼起来却痛苦不堪(可能有点过分,不过看很多论坛贴吧的抱怨,我觉得也是不可否认的)。每个初涉泛型的人可能都会经历这样的阶段,什么是泛型,为什么会有泛型,怎么样使用泛型,它能给我们带来什么?等等   其实早在JDK1.5之前,java还不存在泛型,但j
Wesley13 Wesley13
3年前
Java泛型一览笔录
1、什么是泛型?泛型(Generics)是把类型参数化,运用于类、接口、方法中,可以通过执行泛型类型调用分配一个类型,将用分配的具体类型替换泛型类型。然后,所分配的类型将用于限制容器内使用的值,这样就无需进行类型转换,还可以在编译时提供更强的类型检查。2、泛型有什么用?泛型主要有两个好处:(1)消除显
可莉 可莉
3年前
20175209 《Java程序设计》第八周学习总结
20175209《Java程序设计》第八周学习总结一、教材知识点总结1.泛型1.泛型类声明:格式classPeople<EPeople是泛型类名称E是泛型列表,可以是任何对象或接口,但不能是基本类型数据