Golang(1.18 )泛型尝鲜

从一个实际例子开始

有这样一个需求:

实现两个输入求和的能力,输入可能是 int32、int64、float32、float64

在 Go 1.18 之前,你可能会这样写:

func AddInt32(a, b int32) int32 {
    return a + b
}

func AddInt64(a, b int64) int64 {
    return a + b
}

func AddFloat32(a, b float32) float32 {
    return a + b
}

func Addfloat64(a, b float64) float64 {
    return a + b
}

或者借助 reflect:

func AddByReflect(a, b interface{}) (interface{}, error) {
    aValue := reflect.ValueOf(a)
    bValue := reflect.ValueOf(b)

    if aValue.Type() != bValue.Type() {
        return nil, errors.New("invalid error")
    }

    switch aValue.Kind() {
    case reflect.Int32, reflect.Int64:
        return aValue.Int() + bValue.Int(), nil
    case reflect.Float32, reflect.Float64:
        return aValue.Float() + bValue.Float(), nil
    default:
        return nil, errors.New("invalid error")
    }
}

从上面的例子可以看出,为了达到同样的功能适配不同的参数类型的目的,我们或者会重复的制造类似的函数,或者基于 interface + reflect 在运行时识别具体类型在做处理:

  • 前者导致代码实现臃肿,且不优雅,大量的重复实现还会影响编译速度
  • 而后者引入的运行时开销,则对性能有一定的冲击。

而泛型编程则是为了解决这一问题,比较成熟的语言,如 C++(template) 、Java (generic)早已给开发者提供了相应的能力,golang 社区也一直在致力于解决这个问题,终于在 golang 1.18 支持:

Go 1.18 includes an implementation of generic features as described by the Type Parameters Proposal.

注释: Type Parameters Proposal 有泛型的完整阐释,详细的描述的golang 泛型设计过程中的一些思考与抉择,非常推荐阅读

上述例子在 Golang 1.18,则可以这么写:

 func AddByGeneric[T int32| int64 | float32 | float64](a , b T)  T{
     return a + b
 }

泛型函数

上面的例子中,我们使用的方式是泛型函数(generic function)AddByGeneric 即是一个泛型函数,我们先来看一些新的概念。

泛型函数示例.png

  • T 是 type parameter,实质上是个占位符
  • int32| int64 | float32 | float64 是 type constraint,约束了 T 的类型范围,使用时,传入的具体类型被称为 type argument
  • 泛型函数要实例化(instantiations)后才可以使用
  • int32 | int64 是一种新的语法结构,叫做 union element
【TIP】type constraint 为什么选择使用 []?
1. () 函数入参和返回等都是圆括号,容易搞混
2.  <> 容易和 <, > 容易搞混,实现时还要考虑兼容,成本也较高
3. 《》非 ASCII 码不考虑

泛型类型

除了泛型函数外,go 的泛型还支持泛型类型(generic type),再来看一个例子:

// Vector is a name for a slice of any element type.
type Vector[T any] []T

上面的例子中,Vector 即一个泛型类型,同泛型函数一样,基于 type parameter,使用时需要传入具体的 type argument 实例化,泛型类型也可以拥有方法(method):

type Vector[T any] []T

func (v *Vector[T]) Push(x T) {
    *v = append(*v, x)
}

func (v *Vector[T]) PushList(x []T) {
    *v = append(*v, x...)
}

func useVector(){
    var v Vector[int64]
    fmt.Println("before push:", v)

    v.Push(1)
    v.PushList([]int64{2,3,4})
    fmt.Println("after push:", v)
}

但是,非泛型类型的的方法中不能使用 type parameter,eg:


Constraint

再看一个例子,以下使用方式在 golang 是不合法的:


在上面的代码中,T 有可能没有 String 方法,所以会存在问题,这是所有实现泛型的语言都要面对的一个问题,C++ 中「可以这么写的」,但是会在编译时报错,而且为了找到这个错误的根因要打印非常长的调用栈,也不怎么优雅。

Golang 没有采用类似的机制,原因是:

  1. One reason is the style of the language.
  2. Another reason is that Go is designed to support programming at scale.

这里提现了 golang 在设计泛型时的原则:

This is an important rule that we believe should apply to any attempt to define generic programming in Go: generic code can only use operations that its type arguments are known to implement.

Any

上面的例子中出现了一个新的关键字 any,其实际上是空接口的别名,也就是说在在 go1.18 以后,所有使用空接口的地方都可以使用 any 替换(后面会更详细的展开 interface 的讨论)。

// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{} 

Interface: Method Set -> Type Set

回顾上面的例子(使用 any 的例子,而 any 本质是个空 interface ),我们会使用 interface 做作为 constraint,而 1.18 前 interface 本质是a set of methods,即一组方法的集合,这也就限定了我们使用任意 type 只能用来实现方法调用,但是方法调用并不能满足我们全部的变成场景,我们还会使用 operator,来看一个使用 operator 的例子:

为了解决以上问题,golang 引入了新概念 type set,即一组类型的集合,而 interface 的定义也悦然一新:

An interface type defines a type set (一个接口类型定义了一个类型集)

PS:其实从之前的定义来看(method set),也可以理解成 type set,即实现了这 method 的类型的集合

相应的,我们可以这样定义一个这样的 interface,在泛型编程时用作 constraint:

// SignedInteger is a constraint that matches any signed integer type.
type SignedInteger interface {
        int | int8 | int16 | int32 | int64
}

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

新符号 ~

假设我们定义了一个类型 type MyInt64 Int64,在上述Smallest中是行不通的,因为 constraint 中只有 int64 而没有 MyInt64:

所以,go 1.18 提供了一个新的符号~来描述所有底层都是这一基础类型的所有类型,新的SignedInteger 定义如下,这时 Smallest 方法就是接受 MyInt64 型的 argument了。

type SignedInteger interface {
        ~int | ~int8 | ~int16 | ~int32 | ~int64
}

使用上有一些需要注意的地方:

  • ~ 只支持基本类型(most predeclared types)
  • 不支持 type parameter 或者是 interface type

comparable 和 ordered

Golang 在 1.18 还新增加了一个内置关键字comparable,用来解决使用 「==」 和「!=」 这两种 operator 的场景

  // comparable is an interface that is implemented by all comparable types
  // (booleans, numbers, strings, pointers, channels, arrays of comparable types,
  // structs whose fields are all comparable types).
  // The comparable interface may only be used as a type parameter constraint,
  // not as the type of a variable.
  type comparable interface{ comparable }

comparable只能被用作 type parameter 的 constraint,不能用来声明变量,这里是不是和 interface 之前的用法有些矛盾呢?确实,按1.18 以前的逻辑,这里是冲突的,所以在 1.18 后,为了兼容泛型的实现,golang 在 interface 上还有很多变化,不仅仅是 type set,下个段落我们详细展开 interface 聊聊。

注意,comparable 是不包含 「<」 、「+」这些 operator,对于这类operator,golang 也提供了一个额外的库(见后文)支持:

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

回过头来再看 interface

完整官方说明:https://go.dev/ref/spec#Interface_types

上文我们提到 interface 变成了 type set,此外还有比较多的概念,首先是 interface 有不同的类型定义:

  • Basic interfaces:这个比较好理解,即1.8 版本之前的接口,只包含 method
  • Embedded interfaces:一个 interface T 中嵌入了另一个 interface E,即 interface 中包含其他 interface
type Reader interface {
        Read(p []byte) (n int, err error)
        Close() error
}

type Writer interface {
        Write(p []byte) (n int, err error)
        Close() error
}

// ReadWriter's methods are Read, Write, and Close.
type ReadWriter interface {
        Reader  // includes methods of Reader in ReadWriter's method set
        Writer  // includes methods of Writer in ReadWriter's method set
}

// 注意:嵌入接口时,同名 method 需要有相同的函数签名,否则不合法
type ReadCloser interface {
        Reader   // includes methods of Reader in ReadCloser's method set
        Close()  // illegal: signatures of Reader.Close and Close are different
}
  • General interface:既包含任意的类型 T、或者 ~T、T1|T2|T3|...,同时包含 method,要注意都是,这种接口是不能用来定义变量的,只能在泛型场景使用。

interface 的 Implementing 语义也发生了变化,当满足以下条件时,我们可以说 类型 T 实现了接口 I ( type T implements interface I)

  • T 不是接口时:类型 T 是接口 I 代表的类型集中的一个成员 (T is an element of the type set of I)
  • T 是接口时: T 接口代表的类型集是 I 代表的类型集的子集(Type set of T is a subset of the type set of I)

官方提供的一些泛型库

泛型的实现原理

根据Russ Cox的观察,实现泛型至少要面对下面三条困境之一,那还是在2009年:

  • Leave them out(slow programmers):比如C语言,增加了程序员的负担,需要曲折的实现,但是不对增加语言的复杂性
  • Compile-time specialization or macro expansion(slow compilers): 比如C++编程语言,增加了编译器的负担,可能会产生很多冗余的代码,重复的代码还需要编译器斟酌删除,编译的文件可能非常大(Rust的泛型也属于这一类)。
  • Box everything implicitly(slow execution times):比如Java,将一些装箱成Object,进行类型擦除。虽然代码没啥冗余了,空间节省了,但是需要装箱拆箱操作,代码效率低。java 主要借助 “Type Erasure” 实现

在 type parameter 的提案中有提到,golang不会是 slow programmers,所以会在slow compilersslow execution times 中做选择

In other words, this design permits people to stop choosing slow programmers, and permits the implementation to decide between slow compilers (compile each set of type arguments separately) or slow execution times (use method calls for each operation on a value of a type argument).

找到一篇大佬的分析资料,golang 在1.18 中实际使用的是一种 GC Shape Stenciling 的方案,更多分析参考:https://colobu.com/2021/08/30/how-is-go-generic-implemented/

总结

  • 泛型函数&泛型类型,以及 constraint
  • 新符号 ~ 和 |
  • 新的关键字 any、comparable
  • 焕然一些的 Interface(type set)
  • 官方库
  • 实现原理

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354