导语 | 泛型是一些语言的标配,可以极大地便利开发者,但Golang在之前并不支持泛型。在今年的Go1.17中已经发布了泛型的体验版,这一功能也是为1.18版本泛型正式实装做铺垫。本文将介绍一下泛型在Golang的使用样例及其泛型的发展历史,需要体验的同学可以使用:https://go2goplay.golang.org/或者自行在docker中安装版本。
一、泛型
谈泛型的概念,可以从多态看起,多态是同一形式表现出不同行为的一种特性,在编程语言中被分为两类,临时性多态和参数化多态。
临时性多态(Ad hoc Polymorphism),根据实参类型调用相应的版本,仅支持数量十分有限的调用,也被称作特设多态,例如函数重载。
func Add(a, b int) int { return a+b }func Add(a, b float64) float64 { return a+b } // 注意: Golang中不允许同名函数Add(1, 2) // 调用第一个Add(1.0, 2.0) // 调用第二个Add(“1”, “2”) // 编译时不检查,运行时找不到实现,崩溃或者编译时直接不通过
参数化多态(Parametric Polymorphism),根据实参生成不同的版本,支持任意数量的调用,即泛型,简言之,就是把元素类型变成了参数。
func Add(a, b T) T { return a+b }Add(1, 2) // 编译器生成 T = int 的 AddAdd(float64(1.0), 2.0) // 编译器生成 T = float64 的 AddAdd("1", "2") // 编译器生成 T = string 的 Add
泛型和其他特性一样不是只有好处,为编程语言加入泛型会遇到需要权衡的两难问题。语言的设计者需要在编程效率、编译速度和运行速度三者进行权衡和选择,编程语言要选择牺牲一个而保留另外两个。
在2009年的时候,Russ Cox提出来的一个关于泛型的问题叫做泛型困境,用来收集人们对Golang中泛型的一些意见和建议,对Golang泛型设计当中的问题进行解释,并表示他们并不急于去实现泛型,因为还没有找到一个合适的实现方案去解决困境。
而泛型困境的本质是,关于泛型,你想要缓慢的程序员、缓慢的编译器和臃肿的二进制文件,还是缓慢的执行时间。简单来说就是:要么苦了程序员,要么苦了编绎器,要么降低运行时效率。
以C、C++和Java为例,它们在泛型的设计上有着不同考量:
而C、C++和Java相比,Golang旨在作为一种编写服务器程序的语言,这些程序随着时间的推移易于维护,侧重于可伸缩性、可读性和并发性等多种方面。泛型编程在当时似乎对Golang的目标并不重要,因此为了简单起见被排除在外。
例如下面是一位程序猿自己写的一个实现类似泛型的代码:
二、Golang中的泛型
Go是一门强类型语言,意味着程序中的每个变量和值都有某种特定的类型,例如int、string等。没有泛型,很多人以此“鄙视”Golang。当然,也有人觉得根本不需要泛型。有泛型,不代表你一定要用。在复用代码等场景下,泛型还是很有必要和帮助的。比如:
func Add(a, b int) intfunc AddFloat(a, b float64) float64
在泛型的帮助下,上面代码就可以简化成为:
func Add[T any](a, b T) T
Golang团队一直在尝试泛型的设计,之前也有很多的努力和尝试,包括各种泛型提案和实现方式,但最后都被否决了。Golang核心作者给出的解释是泛型并不是不可或缺的特性,属于重要但不紧急,应该把精力集中在更重要的事情上,例如GC的延迟优化,编译器自举等。现在他们认为Goalng现在更加成熟了,加上目前泛型是Golang社区呼声最高的,希望被尽快实现的语言特性,因此,可以考虑某种形式的泛型编程。
目前,在1.17的版本中Golang终于推出来泛型的尝鲜版了,官方目前预计此更改将在2022年初的Go1.18版本中可用(We currently expect that this change will be available in the Go1.18 release in early 2022.)。而泛型,是Golang多年来最令人兴奋和根本性的变化之一。
三、Golang泛型案例
下面的例子是一个对泛型输出的基本例子。函数可以有一个额外的类型参数列表,它使用方括号,但看起来像一个普通的参数列表:func F[T any](p T) { ... },代码中的[T any]即为类型参数,意思是该函数支持任何T类型,当我们调用printSlice[string]([]string{“Hello”,“World”})时,会被类型推导为string类型,不过在编译器完全可以实现类型推导时,也可以省略显式类型,如:printSlice([]string{“Hello”,“World”}) ,这样也将会是对的;
package main
import ( "fmt")
func printSlice[T any](s []T) { for _, v := range s { fmt.Printf("%v ", v) } fmt.Print("\n")}
func main() { printSlice[int]([]int{1, 2, 3, 4, 5}) printSlice[float64]([]float64{1.01, 2.02, 3.03, 4.04, 5.05}) printSlice([]string{"Hello", "World"}) printSlice[int64]([]int64{5, 4, 3, 2, 1})}
输出为:1 2 3 4 5 1.01 2.02 3.03 4.04 5.05 Hello World 5 4 3 2 1
这个例子包含了一个类型约束。每个类型参数都有一个类型约束,就像每个普通参数都有一个类型:func F[T Constraint](p T) { ... },类型约束是接口类型。该提案扩展了interface语法,新增了类型列表(type list)表达方式,专用于对类型参数进行约束。
package main
import ( "fmt")
type Addable interface { type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, complex64, complex128, string}
func add[T Addable] (a, b T) T { return a + b}
func main() { fmt.Println(add(1,2)) fmt.Println(add("hello","world"))}
输出为:3helloworld
在官方的最新proposal里有提到,在Golang中,并不是所有的类型都满足+号运算。在1.17的版本中,泛型函数只能使用类型参数所能实例化出的任意类型都能支持的操作。
比如下面的add函数的类型参数T没有任何约束,它可以被实例化为任何类型;那么这些实例化后的类型是否都支持+操作符运算呢?显然不是;因此,报错了!对于没有任何约束的类型参数实例,允许对其进行的操作包括:
这就意味着,如果不用interface约束,直接使用的话,你讲得到如下的结果:
package main
import ( "fmt")func add[T any] (a, b T) T { return a + b}
func main() { fmt.Println(add(1,2)) fmt.Println(add("hello","world"))}
输出:type checking failed for mainprog.go2:8:9: invalid operation: operator + not defined for a (variable of type parameter type T)
在约束里,甚至可以放进去接口如下:
package main
import ( "fmt")
type Addable interface { type int,interface{}}
func add[T Addable] (a T) T { return a}
func main() { fmt.Println(add(1))}
接着假如我们去掉string,如下代码所示。以该示例为例,如果编译器通过类型推导得到的类型不在这个接口定义的类型约束列表中,那么编译器将允许这个类型参数实例化;否则就像类型参数实例化将报错!
package main
import ( "fmt")
type Addable interface { type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, complex64, complex128}
func add[T Addable] (a, b T) T { return a + b}
func main() { fmt.Println(add(1,2)) fmt.Println(add("hello","world"))}
输出为:type checking failed for mainprog.go2:19:14: string does not satisfy Addable (string or string not found in int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, complex64, complex128)
注意:我们自己定义的带有类型列表的接口将无法用作接口变量类型,如下代码将会报错
package main
type MyType interface { type int}
func main() { var n int = 6 var i MyType i = n _ = i}
输出为:type checking failed for mainprog.go2:9:8: interface contains type constraints (int)
package main
import ( "fmt" "strconv")
type MyStringer interface { String() string}
type StringInt inttype myString string
func (i StringInt) String() string {
return strconv.Itoa(int(i))}
func (str myString) String() string { return string(str)}
func stringify[T MyStringer](s []T) (ret []string) { for _, v := range s { ret = append(ret, v.String()) } return ret}
func stringify2[T MyStringer](s []T) (ret []string) { for _, v := range s { ret = append(ret, v.String()) } return ret}
func main() { fmt.Println(stringify([]StringInt{1, 2, 3, 4, 5})) fmt.Println(stringify2([]myString{"1", "2", "3", "4", "5"}))}
输出为:[1 2 3 4 5][1 2 3 4 5]
代码中我们声明了MyStringer接口,并且使用StringInt和myString类型实现了此接口;在范型方法中,我们声明了范型的类型为:任意实现了MyStringer接口的类型;只要实现了这个接口,那么你就可以直接使用,在现在某些需要传interface{}作为参数的函数里面,可以直接指定类型了。当你改为如下代码时
func main() { fmt.Println(Stringify([]int{1, 2, 3, 4, 5}))}
会报错:
输出为:type checking failed for mainprog.go2:27:14: int does not satisfy MyStringer (missing method String)
只有实现了Stringer接口的类型才会被允许作为实参传递给Stringify泛型函数的类型参数并成功实例化!当然也可以将MyStringer接口写成如下的形式:
type MySignedStringer interface { type int, int8, int16, int32, int64 String() string}
表示只有int, int8, int16, int32, int64,这样类型参数的实参类型既要在MySignedStringer的类型列表中,也要实现了MySignedStringer的String方法,才能使用。像这种不在里面的type StringInt uint就会报错。
可以看到在下面的例子里面,我们声明了一个可以存放任何类型的切片,叫做slice,如type slice[T any] []T。和泛型函数一样,使用泛型类型时,首先要对其进行实例化,即显式为类型参数赋值类型。如果在类型定义时,将代码改成vs:=slice{5,4,2,1},那么你会得到如note1中的结果。因为编译器并没有办法进行类型推导,也就是表示它并不知道,你输出的是那种类型。哪怕你在interface里面定义了约束。哪怕你在接口中定义了类型约束type int, string,同样会报错,如note2所示。
package main
import ( "fmt")
type slice[T any] []T
/*type any interface { type int, string}*/
func printSlice[T any](s []T) { for _, v := range s { fmt.Printf("%v ", v) } fmt.Print("\n")}
func main() { // note1: cannot use generic type slice[T interface{}] without instantiation // note2: cannot use generic type slice[T any] without instantiation vs := slice[int]{5, 4, 2, 1} printSlice(vs)
vs2 := slice[string]{"hello", "world"} printSlice(vs2)}
输出为:5 4 2 1 hello world
package main
import ( "fmt")
type minmax interface { type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64}
func max[T minmax](a []T) T { m := a[0] for _, v := range a { if m < v { m = v } } return m}
func min[T minmax](a []T) T { m := a[0] for _, v := range a { if m > v { m = v } } return m}
func main() { vi := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} result := max(vi) fmt.Println(result)
vi = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} result = min(vi) fmt.Println(result)}
输出为:101
当你写成如下代码时,便会报错:
package main
import ( "fmt")
func findFunc[T any](a []T, v T) int { for i, e := range a { if e == v { return i } } return -1}
func main() { fmt.Println(findFunc([]int{1, 2, 3, 4, 5, 6}, 5))}
输出为:type checking failed for mainprog.go2:9:6: cannot compare e == v (operator == not defined for T)
因为不是所有的类型都可以==比较,所以Golang内置提供了一个comparable约束,表示可比较的。参考下面代码:
package main
import ( "fmt")
func findFunc[T comparable](a []T, v T) int { for i, e := range a { if e == v { return i } } return -1}
func main() { fmt.Println(findFunc([]int{1, 2, 3, 4, 5, 6}, 5))}
输出为:4
package main
import ( "fmt")
func pointerOf[T any](v T) *T { return &v}
func main() { sp := pointerOf("foo") fmt.Println(*sp)
ip := pointerOf(123) fmt.Println(*ip) *ip = 234 fmt.Println(*ip)}
输出为:foo123234
在现实开发过程中,我们往往需要对slice中数据的每个值进行单独的处理,比如说需要对其中数值转换为平方值,在泛型中,我们可以抽取部分重复逻辑作为map函数:
package main
import ( "fmt")
func mapFunc[T any, M any](a []T, f func(T) M) []M { n := make([]M, len(a), cap(a)) for i, e := range a { n[i] = f(e) } return n}
func main() { vi := []int{1, 2, 3, 4, 5, 6} vs := mapFunc(vi, func(v int) string { return "<" + fmt.Sprint(v*v) + ">" }) fmt.Println(vs)}
输出为:[<1> <4> <9> <16> <25> <36>]
在现实开发过程中,我们有可能会需要一个队列去处理一些数据,在泛型中,我们可以抽取部分重复逻辑来实现
package main
import ( "fmt")
type queue[T any] []T
func (q *queue[T]) enqueue(v T) { *q = append(*q, v)}
func (q *queue[T]) dequeue() (T, bool) { if len(*q) == 0 { var zero T return zero, false } r := (*q)[0] *q = (*q)[1:] return r, true}
func main() { q := new(queue[int]) q.enqueue(5) q.enqueue(6) fmt.Println(q) fmt.Println(q.dequeue()) fmt.Println(q.dequeue()) fmt.Println(q.dequeue())}
输出为:&[5 6]5 true6 true0 false
官方也引入了一些官方包来方面泛型的使用,具体如下:
// constraints 定义了一组与类型参数一起使用的约束package constraints
// Signed是允许任何有符号整数类型的约束。type Signed interface { ... }
// Unsigned是允许任何无符号整数类型的约束。type Unsigned interface { ... }
// Integer是允许任何整数类型的约束。type Integer interface { ... }
// Float是一个允许任何浮点类型的约束。type Float interface { ... }
// Complex是允许任何复杂数值类型的约束。type Complex interface { ... }
// Ordered是一个约束,允许任何有序类型:任何支持操作符< <= >= >的类型。type Ordered interface { ... }
使用方式示例如下:
package main
import ( "constraints" "fmt")
type v[T constraints.Ordered] T
type Vector[T constraints.Ordered] struct { x, y T}
func (v *Vector[T]) Add(x, y T) { v.x += T(x) v.y += T(y)}
func (v *Vector[T]) String() string { return fmt.Sprintf("{x: %v, y: %v}", v.x, v.y)}
func NewVector[T constraints.Ordered](x, y T) *Vector[T] { return &Vector[T]{x: x, y: y}}
func main() { v := NewVector[float64](1, 2) v.Add(2, 3) fmt.Println(v)}
四、总结
尽管最新的proposal冗长而详尽,但总结起来如下:
此外,标准库中将会引入一系列新的package。
Golang的一大优点是它的简单性。显然,这种设计使语言更加复杂,对于泛型推出,无论采用什么技术,都会增加Golang的复杂性,提升其学习门槛,代码的可读性也可能会下降,官方对其增加的复杂性的解释如下:
官方目前尚不清楚人们期望从通用代码中获得什么样的效率,他们将其划分为泛型函数和泛型类型。
1.17版本的Golang,泛型玩玩就行,不要用到生产中。
五、Golang泛型的发展历史
type Lesser(t) interface { Less(t) bool}func Min(a, b t type Lesser(t)) t { if a.Less(b) { return a } return b}
关键设计
提案还包含一些其他的备选语法:
generic(t) func ..$t // 使用类型参数t // 实例化具体类型
评述
gen [T] type Greater interface { IsGreaterThan(T) bool}gen [T Greater[T]] func Max(arg0, arg1 T) T { if arg0.IsGreaterThan(arg1) { return arg0 } return arg1}
gen [T1, T2] ( type Pair struct { first T1; second T2 }
func MakePair(first T1, second T2) Pair { return &Pair{first, second}})
关键设计
评述
type [T] Greater interface { IsGreaterThan(T) bool}func [T] Max(arg0, arg1 T) T { if arg0.IsGreaterThan(arg1) { return arg0 } return arg1}type Int intfunc (i Int) IsGreaterThan(j Int) bool { return i > j}func F() { _ = Max(0, Int(1)) // 推导为 Int}
关键设计
评述
import "github.com/cheekybits/genny/generic"**
// cat 201401.go | genny gen "T=NUMBERS" > 201401_gen.go**
type T generic.Type
func MaxT(fn func(a, b T) bool, a, b T) T {** if fn(a, b) { return a } return b }
关键设计
评述
const func AsWriterTo(reader gotype) gotype { switch reader.(type) { case io.WriterTo: return reader default: type WriterTo struct { reader } func (t *WriterTo) WriteTo(w io.Writer) (n int64, err error) { return io.Copy(w, t.reader) } return WriterTo (type) }}
const func MakeWriterTo(reader gotype) func(reader) AsWriterTo(reader) { switch reader.(type) { case io.WriterTo: return func(r reader) AsWriterTo(reader) { return r } default: return func(r reader) AsWriterTo(reader) { return AsWriterTo(reader) { r } } }}
关键设计
评述
contract comparable(x T) { x == x}
func Contains(type T comparable)(s []T, e T) bool { for _, v := range s { if v == e { // now valid return true } } return false}
合约是一个描述了一组类型且不会被执行的函数体。
关键设计
评述
contract Ordered(T) { T int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, string}
func Min (type T Ordered) (a, b T) T { if a < b { return a } return b}
合约描述了一组类型的必要条件。
关键设计
评述
1.The go2go Playground
2.关于Go泛型 (Generics)
3.Contracts—Draft Design
4.Compile-time Functions and First Class Types
作者简介
喻佳鑫
腾讯后台开发工程师
腾讯后台开发工程师,毕业于中南大学,腾讯看点频道推荐文章索引构建等后端开发工作。
推荐阅读
大咖共探万物智联时代风云!Techo TVP物联网开发者峰会圆满落幕