Golang(1.18 )泛型尝鲜

2022-09-27  本文已影响0人  zhaif

从一个实际例子开始

有这样一个需求:

实现两个输入求和的能力,输入可能是 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
【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
}

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

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 有不同的类型定义:

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
}

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

官方提供的一些泛型库

泛型的实现原理

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

在 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/

总结

参考

上一篇下一篇

猜你喜欢

热点阅读