泛型困境
泛型和其他特性一样不是只有好处,为编程语言加入泛型会遇到需要权衡的两难问题。语言的设计者需要在编程效率、编译速度和运行速度三者进行权衡和选择,编程语言要选择牺牲一个而保留另外两个。
Golang的泛型在早期没有支持,其实是刻意而为。在2009年的时候,Russ Cox提出来的一个关于泛型的问题叫做泛型困境,用来收集人们对Golang中泛型的一些意见和建议,对Golang泛型设计当中的问题进行解释,并表示他们并不急于去实现泛型,因为还没有找到一个合适的实现方案去解决困境。
而泛型困境的本质是,关于泛型,你想要缓慢的程序员、缓慢的编译器和臃肿的二进制文件,还是缓慢的执行时间。简单来说就是:要么苦了程序员,要么苦了编绎器,要么降低运行时效率。
以C、C++和Java为例,它们在泛型的设计上有着不同考量:
- C语言是系统级的编程语言,没有支持泛型,本身提供的抽象能力非常有限。结果是牺牲了程序员的开发效率,与Golang目前的做法一样,它们都需要手动实现不同类型的相同逻辑。但是不引入泛型的好处也显而易见,那就是降低了编译器实现的复杂度,也能保证源代码的编译速度;
- C++与C语言的选择完全不同,它使用编译期间类型特化实现泛型,提供了非常强大的抽象能力。虽然提高了程序员的开发效率,不再需要手写同一逻辑的相似实现,但是编译器的实现变得非常复杂,泛型展开会生成的大量重复代码也会导致最终的二进制文件膨胀和编译缓慢,我们往往需要链接器来解决代码重复的问题;
- Java在1.5版本引入了泛型,它的泛型是用类型擦除实现的。Java的泛型只是在编译期间用于检查类型的正确,为了保证与旧版本JVM的兼容,类型擦除会删除泛型的相关信息,导致其在运行时不可用。编译器会插入额外的类型转换指令,与C语言和C++在运行前就已经实现或者生成代码相比,Java类型的装箱和拆箱会降低程序的执行效率
而C、C++和Java相比,Golang旨在作为一种编写服务器程序的语言,这些程序随着时间的推移易于维护,侧重于可伸缩性、可读性和并发性等多种方面。泛型编程在当时似乎对Golang的目标并不重要,因此为了简单起见被排除在外。
Golang泛型介绍
parametric polymorphism((形式)参数多态)是Go此版泛型设计的基本思想。和Go设计思想一致,这种参数多态并不是通过像面向对象语言那种子类型的层次体系实现的,而是通过显式定义结构化的约束实现的。基于这种设计思想,该设计不支持模板元编程(template metaprogramming)和编译期运算。
虽然都称为泛型(generics),但是Go中的泛型(generics)仅是用于狭义地表达带有类型参数(type parameter)的函数或类型,这与其他编程语言中的泛型(generics)在含义上有相似性,但不完全相同。
由此可见,golang对于泛型的支持是克制的。在提升开发者效率的基础上,最小化对于泛型的支持,从而降低语言的复杂度。
Golang泛型使用方法
Golang使用泛型的几种方法包括
1. 泛型参数
通过为type和function增加类型参数(type parameters)的方式实现泛型
// https://go2goplay.golang.org/p/rDbio9c4AQI
package main
import "fmt"
func PrintSlice(type T)(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)([]string{"one", "two", "three", "four", "five"})
}
以上例子通过定义了type T 来支持了泛型
2. 泛型类型
通过扩展了的interface类型对类型参数进行约束和限制
1) 对泛型函数中类型参数的约束与限制
// https://go2goplay.golang.org/p/kMxZI2vIsk-
package main
import "fmt"
type PlusableInteger interface {
type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64
}
func Add(type T PlusableInteger)(a, b T) T {
return a + b
}
func main() {
c := Add(5, 6)
fmt.Println(c)
}
通过interface,来约束了泛型支持具体哪些数据类型
通过定义类型,我们还可以看到一个名为~的新操作符。这个操作符实际上返回给我们的是类型信息的接口,因此我们可以进行类型限制,让我们来编写相同的 min 函数来包含 int 和 float32,如下所示:
type OrderTypes interface {
~int | ~float32
}
func min[P OrderTypes](x, y P) P {
if x < y {
return x
} else {
return y
}
}
2) 引入comparable预定义类型约束
comparable可以看成一个由Go编译器特殊处理的、包含由所有内置可比较类型组成的type list的interface类型。
// https://go2goplay.golang.org/p/tea39NqwZGC
package main
import (
"fmt"
)
// Index returns the index of x in s, or -1 if not found.
func Index(type T comparable)(s []T, x T) int {
for i, v := range s {
// v and x are type T, which has the comparable
// constraint, so we can use == here.
if v == x {
return i
}
}
return -1
}
type Foo struct {
a string
b int
}
func main() {
fmt.Println(Index([]int{1, 2, 3, 4, 5}, 3))
fmt.Println(Index([]string{"a", "b", "c", "d", "e"}, "d"))
pos := Index(
[]Foo{
Foo{"a", 1},
Foo{"b", 2},
Foo{"c", 3},
Foo{"d", 4},
Foo{"e", 5},
}, Foo{"b", 2})
fmt.Println(pos)
}
3) 对泛型类型中类型参数的约束
// https://go2goplay.golang.org/p/O-YpTcW-tPu
// Package set implements sets of any comparable type.
package main
// Set is a set of values.
type Set(type T comparable) map[T]struct{}
// Make returns a set of some element type.
func Make(type T comparable)() Set(T) {
return make(Set(T))
}
// Add adds v to the set s.
// If v is already in s this has no effect.
func (s Set(T)) Add(v T) {
s[v] = struct{}{}
}
// Delete removes v from the set s.
// If v is not in s this has no effect.
func (s Set(T)) Delete(v T) {
delete(s, v)
}
// Contains reports whether v is in s.
func (s Set(T)) Contains(v T) bool {
_, ok := s[v]
return ok
}
// Len reports the number of elements in s.
func (s Set(T)) Len() int {
return len(s)
}
// Iterate invokes f on each element of s.
// It's OK for f to call the Delete method.
func (s Set(T)) Iterate(f func(T)) {
for v := range s {
f(v)
}
}
func main() {
s := Make(int)()
// Add the value 1,11,111 to the set s.
s.Add(1)
s.Add(11)
s.Add(111)
// Check that s does not contain the value 11.
if s.Contains(11) {
println("the set contains 11")
}
}
这个示例定义了一个数据结构:Set。该Set中的元素是有约束的:必须支持可比较。对应到代码中,我们用comparable作为泛型类型Set的类型参数的约束。上一个例子是直接入传入Comparable作为类型T的定义,这里的例子是把Comparable作为Set的限制条件。
4) 关于泛型类型的方法
泛型类型的方法目前不支持除泛型类型自身的类型参数之外的其他类型参数了。
目前提案仅是暂时不支持额外的类型参数(如果支持,会让语言规范和实现都变得异常复杂),Go核心团队也会听取社区反馈的意见,直到大家都认为支持额外类型参数是有必要的,那么后续会重新添加。
5) type *T Constraint
上面我们一直采用的对类型参数的约束形式是:
type T Constraint
假设调用泛型函数时某类型A要作为T的实参传入,A必须实现Constraint(接口)。
如果我们将上面对类型参数的约束形式改为:
type *T Constraint
那么这将意味着类型A要作为T的实参传入,
A必须满足Constraint(接口)。并且Constraint中的所有方法(如果有的话)都仅能通过A实例调用(即为必须通过对象指针调用)。
// https://go2goplay.golang.org/p/g3cwgguCmUo
package main
import (
"fmt"
"strconv"
)
type Setter interface {
Set(string)
}
func FromStrings(type *T Setter)(s []string) []T {
result := make([]T, len(s))
for i, v := range s {
result[i].Set(v)
}
return result
}
// Settable is a integer type that can be set from a string.
type Settable int
// Set sets the value of *p from a string.
func (p *Settable) Set(s string) {
i, _ := strconv.Atoi(s) // real code should not ignore the error
*p = Settable(i)
}
func main() {
nums := FromStrings(Settable)([]string{"1", "2"})
fmt.Println(nums)
}
参考资料
- go.1.18的特性介绍,这里面着重强调了go对于泛型的支持
- Go创始人之一Lance Taylor对于go支持泛型的看法
- go对于泛型的使用教程介绍