对于是否会在以“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语言具有类型推断能力通常可以省略类型参数的填写。

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

references

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