天天看点

go int 转string_Go语言泛型的进化

最近看到 Ian Lance Taylor 在 golang-nuts 论坛发布了 Go 语言泛型的最新改动,去掉了 type 关键字[1]。

本文所介绍的内容已经于2020年8月24日更新到最新的设计草案[1]

为了弄明白他讲的内容,我周末又研究了一下Go语言泛型设计[2]。在开始之前,先写一个最新的泛型例子,好让大家有一个感性的认识。

// Map 对 []T1 的每个元素执行函数 f 得到新的 []T2
    func Map[T1, T2 any](s []T1, f func(T1) T2) []T2 {
    	r := make([]T2, len(s))
    	for i, v := range s {
    		r[i] = f(v)
    	}
    	return r
    }
           

有别于常见的 c++/java 泛型,Go没有使用尖括号(<>)表示泛型列表,而是使用了方括号([],最开始使用圆括号)。

Map后面的[T1, T2 any]表示参数类型。Map函数需要两个参数,对参数类型没有任任限制,所以参数类型的类型是any。

下面是一个使用示例

s := []int{1, 2, 3}
    
    floats := Map[int, float64](s, func(i int) float64 { return float64(i) })
    // 现在 floats 的值是 []float64{1.0, 2.0, 3.0}.
           

参数的类型同样使用方括号指定,

Map[int, float64]

等价于

func Map(s []int, f func(int) float64) []float64

,其功能则是将

[]int

转化成

[]float64

为了简化调用,Go还支持

泛型推导

,也就是根据实际调用参数确定泛型的具体类型。前面的例子还可以简化成

floats := Map(s, func(i int) float64 { return float64(i) })
           

T1和T2的类型可以分别通过 s 和 f 的实际入参推导出来。这样的设计可以

尽量减少泛型函数和普通函使用上的差异

,让Go代码看起来更加一致。

好了,让我们总结一下 Go 泛型的特点

  1. 使用[]而非<>
  2. 泛型都有类型
  3. 支持泛型推导

这基本上是目前最好的设计了。下面我们说说Go语言的泛型是如何演化成如今这个样子的。

最开始,设计者希望使用圆括号表示泛型。可是圆括号在Go语言里用途极广,函数的参数列表就是用圆括号表示的,怎么区分参数列表跟泛型列表呢?

最简单的办法就是插入关键字做区分。于是设计者选用了

type

关键字。一个典型的泛型函长这样

func Print(type T)(t T) {
    	fmt.Println(t)
    }
           

请注意,泛型列表的左圆括号之后紧跟着type,而函数参数列表的左括号之后直接跟参数名,这样就可以把两者分开。So far so good。

我们再考虑另一个问题,

泛型有没有类型

?泛型不是代表所有类型吗?怎么泛型还需要类型呢?让我们看一段伪代码

// Stringify 将 []T 的所有成员拼接成一整个字符串
    func Stringify(type T)(s []T) (ret []string) {
    	for _, v := range s {
    		ret = append(ret, v.String())
    	}
    	return ret
    }
           

请大家注意这里的

v.String()

,v的类型为T,我们在函数Stringfy中要调用v的

String()

方法,T又是不确定的,我们怎么保证 v 一定实现了

String()

方法呢?显然,这种写法是不能的。我们需要对T的取值(也就是 v 的类型)作一下

限制

(constraint),这种限制在最初设计[3]中叫

contract

。如果你想限制T一定要实现

String()

方法,你可以定义如下 contract

contract stringer(T) {
    	T String() string
    }
           

然后将

Stringify

定义为

func Stringify(type T stringer)(s []T) (ret []string)

,这样编译器就能确保所有 v 都实现

String()

方法了。

大家有没有觉得 contract 跟 interface 有点像呢?确实,当contract设计方案发布的时候大家都说很容易跟interface混淆。可为什么设计者一开始没有使用 interface 呢?那是interface只能规定实现的方法,无法限制

运算符

len() 函数

等 go 语言内部的操作。我们举一个例子。

// Max 返回两个参数中较大一个
    func Max(type T)(a, b T) T {
        if a > b {
            return a
        }
        return b
    }
           

这里需要比较a和b的大小。问题来了,在Go语言里

只有少部分内建类型才能比较大小

,你没法直接比较两个 struct 或者 map,Go语言本身又

不支持运算符重载

,所以你没法使用 interface 来确保 T 是支持比较运算符的。据此,设计者才引入了 contract 的概念。对于这一类类型,你可以声明如下 contract

contract Ordered(T) {
    	T int, int8, int16, int32, int64,
        	uint, uint8, uint16, uint32, uint64, uintptr,
    		float32, float64,
    		string
    }
           

说白了就是把 go 语言中支持

>

运算符的类型都

列出来

,虽然看起来有点 Low,但有效。这个时候你可以以把 Max 函数改写成

func Max(type T Ordered)(a, b T) T
           

当你尝试调用

Max([]int{1}, []int{2})

的时候,编译器就能确定

[]int{}

不符合

Ordered

规定,进而给出具体的报错信息。

到现在一切都好~唯一的问题就是需要引入 contract 这个新概念。概念越多越不容易学习,社区也普遍表示了对 contract 的反对。大约一年后,设计者又给一个新的设计[2],这次

移除了 contract

。但正如我们前面所讲,原来的 interface 并不能满足泛型的需要,

必需对 interface 做一下扩展

于是,新的草案给 interface 引入了 type list 支持,说白了就是

把 contract 的功能合并到了 interface 中

。你可以在定义 interface 的时候通过 type 指定一组类型列表。前面的 Ordered contract 可以定义为

type Ordered interface{
        type int, int8, int16, int32, int64,
        	uint, uint8, uint16, uint32, uint64, uintptr,
    		float32, float64,
    		string
    }
           

这样你可以像使用 contract 那样使用

Ordered interface

作为泛型限制了。如此,便不再需要引入新的 contract 概念。

到现在,Go泛型的主要设计就讲完了。除了写起来

括号有点多

之外,没有什么大毛病。

但大毛病没有,

几个小毛病却又影响到了泛型的语法设计

我们前面说过,为了跟函数列表相区分,设计者给泛型列表引入了 type 关键字。除此之外,泛型在编译的时候还有几处二义性需要处理。

如果

T

是泛型,则

T(int)

表示将

T

具体化(instantiating)

int

。单纯这样看是没有歧义的。但是如果跟其他语法写到一起就不一样定了。

比如

func f(T(int))

是表示

func f(T int)

呢(在这里T是参数名,其类型为int)还是表示

func ((T(int)))

呢(在这里省去了函数的参数名,T为泛型类型,并具体化为int)?

再比如

struct{ T(int) }

是表示

struct { T int }

呢(在这里 T 是属性名,其类型为int)还是表示

struct { (T(int)) }

呢(在这里T为泛型类型,具体化为int后嵌入struct)?

再比如

interface{ T(int) }

是表示

interface{ T(int) }

呢(在这里T为函数名,入参为int)还是表示

interface{ (T(int)) }

呢(在这里T为泛型类型,具体化为int后也可能是一个 interface,并嵌入原interface)?

最后比如

[]T(int){}

是表示

([]T)(int){}

呢(在这里表示初始化slice,值的类型为int)还是表示

[](T(int)){}

呢(在这里表示初始化为slice,值的类型是泛型T具体化为int后的类型,不一定是int)?

如何消除这种二义性呢?加括号!所以,为了能正常使用泛型,你不得不写成这个样子

func ((T(int)))
struct { (T(int)) }
interface{ (T(int)) }
[](T(int)){}
           

括号太多了。为了少写点括号,设计者绞尽脑汁,最终发现只有方括号可堪此重任。

最开始不选用方括号是因为会带来更多的二义性问题。

一个歧义就是

type A [T] int

是表示

type A[T] int

呢(A是泛型类型,泛型T没有用到)还是

type A [T]int

(A是长度为T的[]int)。不过这个问题可以能运引入 type 消除。

另一个就是编译器在分析

func f(A[T]int)

func f(A[T], int)

两种定义的时候需要适当

向后看

(lookahead),会增加分析器的复杂度。

最开始设计者没有想到前面所讲的

T(int)

二义性问题,于是选用了圆括号。现在回头看,发现与其写那么多括号,不如稍稍扩展一下分析器,于是就可以消除

T(int)

的二义性问题了,你可以这样写

func (T[int]))
struct { T[int] }
interface{ T[int] }
[]T[int]{}
           

一下子少好多括号,棒棒的!就是他了!原来的

Print

方法写成这个样子

func Print[type T](t T)
           

设计者还是觉得这个 type 关建字不清真,没有办法将 type 也省掉呢(真是得陇望蜀)?设计者想到了一个妙招,

强行规定所有泛型都必须写 constraint

(这样可以跟函数参数列表保持统一,所有参数都有类型)。分析器碰到左方括号之后会继续向前看,如果发现有 constraint 就能确定是泛型列表。所以,

Print

方法需要改写为

func Print[T interface{}](t T)
           

这下好了,不用写 type。可是慢着,不写 type 省4个字符,强制写 constraint 需要写 interface{},这是11个字符呀,得不尝失!设计者又开动大脑,想了一个绝招,

我们给 interface{} 设一个别名吧,就叫 any,这样比写 type 还少写一个字符呢

,于是

Print

方法最终变成了

func Print[T any](t T)
           

不管你服不服,反正我是服了!

对于 any,社区还有一些争议,但感觉问题不大。期待 Go 泛型正式上线。

最后提一下,Go语言的设计草案[2]很值得一读,里面记录了各种

设计上的取舍

,很有启发意义。

  • 如果你想查看更多泛型示例,可以移步这里[4]。
  • 如果你对泛型的设计还有疑问,可以先看看这里[5]。
  • 如果你想测试泛型代码,则可移步此处[6]。
  1. https://groups.google.com/g/golang-nuts/c/iAD0NBz3DYw
  2. https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md
  3. https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md
  4. https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#examples
  5. https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#discarded-ideas
  6. https://go2goplay.golang.org/

继续阅读