Golang泛型初体验

对于是否会在以“less is more”为原则的golang语言中增加泛型(generic)特性一直颇有争议,直到官方确定泛型是go2发展的重点才一锤定音。go 1.18中即将正式发布泛型特性,当前go 1.18beta1已经发布,让我们对泛型尝尝鲜吧。

安装go 1.18 beta1

当前可有如下两种方式来安装go 1.18beta1。

使用go install安装

此安装方式要求环境中已经安装go,然后可以通过如下命令进行安装。

go install golang.org/dl/go1.18beta1@latest

注意:此种方式会将安装到$GOPATH/bin/目录下,且安装后的二进制名称为go1.18beta1而非go。

使用binary release版本进行安装

此安装方式对环境没有要求,具体操作如下:

$ go version
go version go1.18beta1 linux/amd64

从简单性上来说推荐使用此种方式进行安装,我本人也是采用此方式。


尝鲜Demo

在编程实践中我们经常遇到从一个数组(或切片)中筛选出满足条件元素的需求,之前大家一般会针对每一种类型的数组写一个筛选函数(filter),这些函数间除了名称及类型不同外其它几乎完全一样。对这些重复敏感的人,会选择使用反射进行重构,但基于反射的实现很难进行静态校验。泛型的到来,为优雅地解决此问题提供了便利条件。

创建go project

$ cd $GOPATH/src
$ mkdir hellogeneric
$ go mod init
go: creating new go.mod: module hellogeneric

非泛型时写法

过滤int及float64的函数如下:

func FilterInts(elems []int, predicate func(int) bool) []int {
    var r []int
    for _, e := range elems {
        if predicate(e) {
            r = append(r, e)
        }
    }
    return r
}

func FilterFloat64s(elems []float64, predicate func(float64) bool) []float64 {
    var r []float64
    for _, e := range elems {
        if predicate(e) {
            r = append(r, e)
        }
    }
    return r
}

相应的main函数及运行结果如下:

func main() {
    ints := []int{1, 2, 3, 4, 5, 6}
    predicateOfInt := func(i int) bool { return i%2 == 0 }

    float64s := []float64{1.1, 2.2, 3.3, 4.4, 5.5, 6.6}
    predicateOfFloat64 := func(f float64) bool { return f >= 5.0 }

    fmt.Printf("No-Generic filters: %v and %v\n",
        FilterInts(ints, predicateOfInt),
        FilterFloat64s(float64s, predicateOfFloat64))
}
$ go run .
No-Generic filters: [2 4 6] and [5.5 6.6]

使用反射的写法

filter函数实现如下:

func FilterByReflect(elems, predicate interface{}) interface{} {
    elemsValue := reflect.ValueOf(elems)

    if elemsValue.Kind() != reflect.Slice {
        panic("filter: wrong type, not a slice")
    }

    predicateValue := reflect.ValueOf(predicate)
    if predicateValue.Kind() != reflect.Func {
        panic("filter: wrong type, not a func")
    }

    if (predicateValue.Type().NumIn() != 1) ||
        (predicateValue.Type().NumOut() != 1) ||
        (predicateValue.Type().In(0) != elemsValue.Type().Elem()) ||
        (predicateValue.Type().Out(0) != reflect.TypeOf(true)) {
        panic("filter: wrong type, predicate must be of type func(" +
            elemsValue.Elem().String() + ") bool")
    }

    var indexes []int
    for i := 0; i < elemsValue.Len(); i++ {
        if predicateValue.Call([]reflect.Value{elemsValue.Index(i)})[0].Bool() {
            indexes = append(indexes, i)
        }
    }

    r := reflect.MakeSlice(elemsValue.Type(), len(indexes), len(indexes))
    for i := range indexes {
        r.Index(i).Set(elemsValue.Index(indexes[i]))
    }
    return r.Interface()
}

相应的main函数及运行结果如下:

func main() {
    ints := []int{1, 2, 3, 4, 5, 6}
    predicateOfInt := func(i int) bool { return i%2 == 0 }

    float64s := []float64{1.1, 2.2, 3.3, 4.4, 5.5, 6.6}
    predicateOfFloat64 := func(f float64) bool { return f >= 5.0 }

    fmt.Printf("Reflect filters: %v and %v\n",
        FilterByReflect(ints, predicateOfInt),
        FilterByReflect(float64s, predicateOfFloat64))
}
$ go run .
Reflect filters: [2 4 6] and [5.5 6.6]

使用泛型的写法

filter函数的实现如下:

func FilterByGeneric[V int | float64](elems []V, predicate func(V)bool) []V {
    var r []V
    for _, e := range elems {
        if predicate(e) {
            r = append(r, e)
        }
    }
    return r
}

泛型函数相对传统函数参数而言,增加了类型参数(type parameters),这些类型参数使得函数具备了泛型能力。在调用时,也需要指定类型参数和函数参数,由于go语言具有类型推断能力通常可以省略类型参数的填写。

对于每一个类型参数,都有一个类型限定(type constraint)用于描述类型参数的元数据,以指定调用时可允许的类型。类型限定通常代表一组类型,如本例中的int | float64就表示允许int和float64两种类型,对于多个泛型函数/结构而言其类型限定可能一样,此时我们就可以将类型限定单独定义然而达到复用的目的。具体语法如下:

type FilterElem interface {
    int | float64
}

也即可将类型限定定义为接口(interface),然后再修改相应泛型函数定义即可:

func FilterByGeneric[V FilterElem](elems []V, predicate func(V)bool) []V

对于filter而言,除了int和float64之外,对于其它作意类型应该也是适用的,此时我们可以使用any类型限定来表达此能力:

func FilterByGeneric[V any](elems []V, predicate func(V)bool) []V

相应的main函数及运行结果如下:

func main() {
    ints := []int{1, 2, 3, 4, 5, 6}
    predicateOfInt := func(i int) bool { return i%2 == 0 }

    float64s := []float64{1.1, 2.2, 3.3, 4.4, 5.5, 6.6}
    predicateOfFloat64 := func(f float64) bool { return f >= 5.0 }

    fmt.Printf("Generic filters: %v and %v\n",
        FilterByGeneric(ints, predicateOfInt),
        FilterByGeneric(float64s, predicateOfFloat64))
}
$ go run .
Generic filters: [2 4 6] and [5.5 6.6]

后记

到这里我们就基本上对go的泛型特性进行了尝鲜体验,其在一定程度上确实会使我们写出来的代码更加简洁,但是否能真正在正式项目中使用我们还需要对此特性进行深入学习、并对其进行商用评估。

完整代码见:https://github.com/skholee/hellogeneric

references

Go Programming Patterns: Gopher China 2020 陈皓(左耳朵)

https://golang.google.cn/doc/tutorial/generics

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

推荐阅读更多精彩内容

  • 在 Go1.17 中,千呼万唤的泛型终于出来了,但又没有完全出来。在 Go1.17 的发布文档中,并没有提到泛型,...
    rayjun阅读 5,018评论 0 5
  • gRPC是由Google主导开发的RPC框架,使用HTTP/2协议并用ProtoBuf作为序列化工具。其客户端提供...
    CZ_Golang阅读 82,148评论 9 71
  • 在安装之间,我们先了解Golang Go语言是谷歌2009发布的第二款开源编程语言。Go语言专门针对多处理器系统应...
    技术流刘阅读 805评论 0 0
  • 最近在学习golang,看网上很多人都喜欢爬豆瓣,今天我就写了一个golang版的爬虫。对于python爬虫,我很...
    若与阅读 10,628评论 1 18
  • GoPlus为国内云厂商七牛云推出的一门静态类型语言,与 Go 语言完全兼容。其代码样式类似脚本,并且比 Go 更...
    写个代码容易么阅读 6,995评论 3 6