• Go 1.18泛型的局限性初探


    前言#

    Go 1.18 版本之后正式引入泛型,它被称作类型参数(type parameters),本文初步介绍 Go 中泛型的使用。长期以来 go 都没有泛型的概念,只有接口 interface 偶尔类似的充当泛型的作用,然而接口终究无法满足一些基本的泛型需求,比如这篇文章里,我们会尝试用 Go 的泛型循序渐进地实现一些常见的函数式特性,从而探索 Go 泛型的优势和不足。

    Go 1.18#

    在 Go1.18 可以通过如下命令安装体验:

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

    例1: 泛型版本的求和函数#

    Copy
    import ( "golang.org/x/exp/constraints" ) func Sum[T constraints.Integer](values ...T) T { var sum T for _, v := range values { sum += v } return sum }

    constraints 原本是放在标准库的包,但是近期被移除了,改到了 x/exp 中,参见 #50792

    这个版本实现了对任意多个同类型的整数求和。Sum 后面的中括号 [] 内就是定义类型参数的地方,其中 T 为类型参数名,constraints.Integer 是对该类型参数的约束,即 T 应该满足的条件,在这里我们要求 T 是一个整数。剩下的代码就和普通没有泛型的代码一致了,只不过后面 T 可以当作一个类型来使用。

    泛型语法#

    • 函数名后可以附带一个方括号,包含了该函数涉及的类型参数(Type Paramters)的列表:func F[T any](p T) { ... }

    • 这些类型参数可以在函数参数和函数体中(作为类型)被使用

    • 自定义类型也可以有类型参数列表:type M[T any] []T

    • 每个类型参数对应一个类型约束,上述的 any 就是预定义的匹配任意类型的约束

    • 类型约束在语法上以 interface 的形式存在,在 interface 中嵌入类型 T 可以表示这个类型必须是 T:

    Copy
    type Integer1 interface { int }
    • 嵌入单个类型意义不大,我们可以用 | 来描述类型的 union:
    Copy
    type Integer2 interface { intint8 | int16 | int32 | int64 }
    • ~T 语法可以表示该类型的「基础类型」是 T,比如说我们的自定义类型 type MyInt int 不满足上述的 Integer1 约束,但满足以下的约束:
    Copy
    type Integer3 interface { ~int }

    高阶函数实例#

    filter 操作是高阶函数的经典应用,它接受一个函数 f(func (T) bool)和一个线性表 l([] T),对 l 中的每个元素应用函数 f,如结果为 true,则将该元素加入新的线性表里,否则丢弃该元素,最后返回新的线性表。

    Copy
    func Filter[T any](f func(T) bool, src []T) []T { var dst []T for _, v := range src { if f(v) { dst = append(dst, v) } } return dst } func main() { src := []int{-2, -1, -0, 1, 2} dst := Filter(func(v int) bool { return v >= 0 }, src) fmt.Println(dst) } // Output: // [0 1 2]

    让人开心的改变 : )#

    实现一个三元操作#

    众所周知Go语言不支持三元运算符操作,现在有了泛型,让我们来模拟一个:

    Copy
    // IFF if yes return a else b func IFF[T any](yes bool, a, b T) T { if yes { return a } return b } // IFN if yes return func, a() else b(). func IFN[T any](yes bool, a, b func() T) T { if yes { return a() } return b() } func main() { a := -1 assert.Equal(t, utils.IFF(a > 0, a, 0), 0) assert.Equal(t, utils.IFN(a > 0, func() int { return a }, func() int { return 0 }), 0) }

    令人沮丧 😦#

    泛型类型系统的不足#

    众多函数式特性的实现依赖于一个强大类型系统,Go 的类型系统显然不足以胜任, 在 Go 语言中引入泛型之后,类型系统有哪些水土不服的地方。

    编译期类型判断#

    当我们在写一段泛型代码里的时候,有时候会需要根据 T 实际上的类型决定接下来的流程,可 Go 的完全没有提供在编译期操作类型的能力。运行期的 workaround 当然有,怎么做呢:将 T 转化为 interface{},然后做一次 type assertion, 比如我想实现一个通用的字符串类型到数字类型的转换函数:

    Copy
    import "strconv" type Number interface { int | int32 | int64 | uint32 | uint64 | float64 } func Str2Number[N Number](strNumber string) (N, error) { var num N switch (interface{})(num).(type) { case int: cn, err := strconv.Atoi(strNumber) return N(cn), err case int32: cn, err := strconv.ParseInt(strNumber, 10, 32) return N(cn), err case int64: cn, err := strconv.ParseInt(strNumber, 10, 64) return N(cn), err case uint32: cn, err := strconv.ParseUint(strNumber, 10, 32) return N(cn), err case uint64: cn, err := strconv.ParseUint(strNumber, 10, 64) return N(cn), err case float64: cn, err := strconv.ParseFloat(strNumber, 64) return N(cn), err } return 0, nil }

    无法辨认「基础类型」#

    在类型约束中可以用 ~T 的语法约束所有 基础类型为 T 的类型,这是 Go 在语法层面上首次暴露出「基础类型」的概念,在之前我们只能通过 reflect.(Value).Kind 获取。而在 type assertion 和 type switch 里并没有对应的语法处理「基础类型」:

    Copy
    type Int interface { ~int | ~uint } func IsSigned[T Int](n T) { switch (interface{})(n).(type) { case int: fmt.Println("signed") default: fmt.Println("unsigned") } } func main() { type MyInt int IsSigned(1) IsSigned(MyInt(1)) } // Output: // signed // unsigned

    乍一看很合理,MyInt 确实不是 int。那我们要如何在函数不了解 MyInt 的情况下把它当 int 处理呢, 比较抱歉的是目前在1.18中没办法对这个进行处理。

    类型约束不可用于 type assertion#

    一个直观的想法是单独定义一个 Signed 约束,然后判断 T 是否满足 Signed:

    Copy
    type Signed interface { ~int } func IsSigned[T Int](n T) { if _, ok := (interface{})(n).(Signed); ok { fmt.Println("signed") } else { fmt.Println("unsigned") } }

    但很可惜,类型约束不能用于 type assertion/switch,编译器报错如下:

    interface contains type constraints

    尽管让类型约束用于 type assertion 可能会引入额外的问题,但牺牲这个支持让 Go 的类型表达能力大大地打了折扣。

    总结#

    • 确实可以实现部分函数式特性能以更通用的方式。

    • 灵活度比代码生成更高 ,用法更自然,但细节上的小问题很多。

    • 1.18 的泛型在引入 type paramters 语法之外并没有其他大刀阔斧的改变,导致泛型和这个语言的其他部分显得有些格格不入,也使得泛型的能力受限。 至少在 1.18 里,我们要忍受泛型中存在的种种不一致。

    • 受制于 Go 类型系统的表达能力,我们无法表示复杂的类型约束,自然也无法实现完备的函数式特性。

    推广#

    推广下个人项目,目前也正在使用Go 1.18的特性也踩了很多坑:

    YoyoGo is a simple, light and fast , dependency injection based micro-service framework written in Go. Support Nacos ,Consoul ,Etcd ,Eureka ,kubernetes.

    https://github.com/yoyofx/yoyogo

  • 相关阅读:
    双十一购物狂欢节是如何轰动全球的
    7/27 训练日志(位运算+后缀数组)
    监控易机房运维大屏:打造高效机房管理的新标杆
    程序化广告还有未来么?(4/5)——程序化领域变化的底层逻辑和反思
    大学生网页作业成品——基于HTML网上书城项目的设计与实现
    Unreal Engine(虚幻引擎)渲染 – 正确使用方法
    IDC:阿里云获2021中国数据治理平台市场份额第一
    全面指南:如何使用Python编写代码,利用热像仪自动测量人员体温进行发热症状的早期筛查
    初识rust
    在vue3中通过vue-i18n实现国际化多语言切换
  • 原文地址:https://www.cnblogs.com/maxzhang1985/p/16113583.html