go泛型浅析

16 阅读3分钟

笔者是在学习C++之后转go的,在学习go语言的过程中发现go是使用接口实现泛型,使用interface{}接收任意类型,通过类型断言或反射处理具体类型。go语言中的接口使用了duck type的思想,不关注实际的对象是什么,只关注对象可以做什么,接口内部是使用类型指针和数据指针实现的。这两个指针就像C++中子类继承父类,父类中有虚函数时,子类内部所产生的虚表指针,虚表指针指向type_info和函数。都知道使用虚函数有开销,因为会发生一次间接寻址,会扰乱缓存,没那么好的空间和时间局部性。

所以在go中如果想大量使用interface{}实现类似泛型的效果,性能肯定不好,因为交给运行时指针寻址了,而不像C++那样在编译期就可以通过模板实例化出对应函数进行替换,所以go在1.18版本之后引入了泛型的概念。

我是参考这篇博客进行学习的:www.liwenzhou.com/posts/Go/ge…

下面举例说明,如果我想写一个翻转指定切片的函数,之前我可能需要写对于[]int[]double等等不同切片的重载,按照之前使用interface{}的方式我可以写出如下代码,使用反射解决:

func ReverseSlice(slice interface{}) {
	v := reflect.ValueOf(slice)

	if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Slice {
		panic("ReverseSlice: not a pointer to slice")
	}

	s := v.Elem()
	n := s.Len()

	for i := 0; i < n/2; i++ {
		temp := reflect.ValueOf(s.Index(i).Interface())
		s.Index(i).Set(s.Index(n - i - 1))
		s.Index(n - i - 1).Set(temp)
	}
}

但是在有了泛型之后就可以采用以下写法:

func reverseWithGenerics[T any](s []T) []T {
	l := len(s)
	r := make([]T, l)

	for i, e := range s {
		r[l-i-1] = e
	}
	return r
}

这段代码就是编译期实例化了,比反射更具性能优势。

go的泛型还支持类型约束,就像C++中的SFINAE和concept一样,下面给出示例:

type Number interface {
	int | int32 | int64 | float32 | float64
}

func Add[T Number](a, b T) T {
	return a + b
}

func Add[T int | float64](a, b T) T {
	return a + b
}

在这里Number表示允许的类型集合,在下面的泛型函数中[T Number]表示泛型函数的类型参数T必须满足Number约束,也就是在Number所允许的类型集合中。后面这段就是类型参数T必须是int或者float64。编译器会在调用时检查类型安全。

上述go代码和下面C++代码行为一致:

template<std::integral T>
T add(T a, T b) {
    return a + b;
}

上述函数在调用时遵循如下规范,其实就和C++模板函数差不多:

a := add[int](1, 2)

b := add[float64](0.1, 0.2)

fadd := add[float64]	// 类型实例化
c = fadd(1.2, 2.3)

上述是go中的模板函数,go还可以有模板类(C++的说法):

type Slice[T int | string] []T

type Map[K int | string, V float32 | float64] map[K]V

type Tree[T interface{}] struct {
	left, right *Tree[T]
	value       T
}

其中都有类似于模板函数中的类型形参和形参约束,类型实参需要满足对应的类型约束。泛型类型其实就相当于go中的struct,一样可以定义方法,这样只要实例化了对应类型再调用方法时就会自动填充模板参数T:

func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }

go的泛型还有类似于C++中CTAD的功能,即不需要显示写模板参数类型,编译期自动帮助你进行推导:

func min[T int | float64](a, b T) T {
	if a <= b {
		return a
	}
	return b
}

var a, b, m float64

m = min(a, b) // 无需指定类型实参