设为首页 加入收藏

TOP

想在golang里用好泛型还挺难的(一)
2023-07-26 08:17:25 】 浏览:164
Tags:想在 golang

golang的泛型已经出来了一年多了,从提案被接受开始我就在关注泛型了,如今不管是在生产环境还是开源项目里我都写了不少泛型代码,是时候全面得回顾下golang泛型的使用体验了。

先说说结论,好用是好用,但问题也很多,有些问题比较影响使用体验,到了不吐不快的地步了。

这篇文章不会教你泛型的基础语法,并且要求你对golang的泛型使用有一定经验,如果你还是个泛型的新手,可以先阅读下官方的教程,然后再阅读本篇文章。

泛型的实现

实现泛型有很多种方法,常见的主流的是下面这些:

  1. 以c++为代表的,类型参数就是个占位符,最后实际上会替换成实际类型,然后以此为模板生成实际的代码,生成多份代码,每份的类型都不一样
  2. 以TypeScript和Java为代表的类型擦除,把类型参数泛化成一个满足类型约束的类型(Object或者某个interface),只生成一份代码
  3. 以c#为代表,代码里表现的像类型擦除,但运行的时候实际上和c++一样采用模板实例化对每个不同的类型都生成一份代码

那么golang用的哪种呢?哪种都不是,golang有自己的想法:gcshape

什么是gcshape?简单得说,所有拥有相同undelyring type的类型都算同一种shape,所有的指针都算一种shape,除此之外就算两个类型大小相同甚至字段的类型相同也不算同一个shape

那么这个shape又是什么呢?gc编译器会根据每个shape生成一份代码,拥有相同shape的类型会共用同一份代码。

看个简单例子:

func Output[T any]() {
	var t T
	fmt.Printf("%#v\n", t)
}

type A struct {
	a,b,c,d,e,f,g int64
	h,i,j string
	k []string
	l, m, n map[string]uint64
}

type B A

func main() {
	Output[string]()
	Output[int]()
	Output[uint]()
	Output[int64]()
	Output[uint64]() // 上面每个都underlying type都不同,尽管int64和uint64大小一样,所以生成5份不同的代码
	Output[*string]()
	Output[*int]()
	Output[*uint]()
	Output[*A]() // 所有指针都是同一个shape,所以共用一份代码
	Output[A]()
	Output[*B]()
	Output[B]() // B的underlying tyoe和A一样,所以和A共用代码
	Output[[]int]()
	Output[*[]int]()
	Output[map[int]string]()
	Output[*map[int]string]()
	Output[chan map[int]string]()
}

验证也很简单,看看符号表即可:

为啥要这么做?按提案的说法,这么做是为了避免代码膨胀同时减轻gc的负担,看着是有那么点道理,有相同shape的内存布局是一样的,gc处理起来也更简单,生成的代码也确实减少了——如果我就是不用指针那生成的代码其实也没少多少。

尽管官方拿不出证据证明gcshape有什么性能优势,我们还是姑且认可它的动机吧。但这么实现泛型后导致了很多严重的问题:

  1. 性能不升反降
  2. 正常来说类型参数是可以当成普通的类型来用的,但golang里有很多时候不能

正因为有了gcshape,想在golang里用对泛型还挺难的。

性能问题

这一节先说说性能。看个例子:

type A struct {
	num  uint64
	num1 int64
}

func (a *A) Add() {
	a.num++
	a.num1 = int64(a.num / 2)
}

type B struct {
	num1 uint64
	num2 int64
}

func (b *B) Add() {
	b.num1++
	b.num2 = int64(b.num1 / 2)
}

type Adder interface {
	Add()
}

func DoAdd[T Adder](t T) {
	t.Add()
}

func DoAddNoGeneric(a Adder) {
	a.Add()
}

func BenchmarkNoGenericA(b *testing.B) {
	obj := &A{}
	for i := 0; i < b.N; i++ {
		obj.Add()
	}
}

func BenchmarkNoGenericB(b *testing.B) {
	obj := &B{}
	for i := 0; i < b.N; i++ {
		obj.Add()
	}
}

func BenchmarkGenericA(b *testing.B) {
	obj := &A{}
	for i := 0; i < b.N; i++ {
		DoAdd(obj)
	}
}

func BenchmarkGenericB(b *testing.B) {
	obj := &B{}
	for i := 0; i < b.N; i++ {
		DoAdd(obj)
	}
}

func BenchmarkGenericInterfaceA(b *testing.B) {
	var obj Adder = &A{}
	for i := 0; i < b.N; i++ {
		DoAdd(obj)
	}
}

func BenchmarkGenericInterfaceB(b *testing.B) {
	var obj Adder = &B{}
	for i := 0; i < b.N; i++ {
		DoAdd(obj)
	}
}

func BenchmarkDoAddNoGeneric(b *testing.B) {
	var obj Adder = &A{}
	for i := 0; i < b.N; i++ {
		DoAddNoGeneric(obj)
	}
}

猜猜结果,是不是觉得引入了泛型可以解决很多性能问题?答案揭晓:

哈哈,纯泛型和正常代码比有不到10%的差异,而接口+泛型就慢了接近100%。直接用接口是这里最快的,不过这是因为接口被编译器优化了,原因参加这篇

你说谁会这么写代码啊,没事,我再举个更常见的例子:

func Search[T Equaler[T]](slice []T, target T) int {
	index := -1
	for i := range slice {
		if slice[i].Equal(target) {
			index = i
		}
	}
	return index
}

type MyInt int

func (m MyInt) Equal(rhs MyInt) bool {
	return int(m) == int(rhs)
}

type Equaler[T any] interface {
	Equal(T) bool
}

func SearchMyInt(slice []MyInt, target MyInt) int {
	index := -1
	for i := range slice {
		if slice[i].Equa
首页 上一页 1 2 3 下一页 尾页 1/3/3
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇Docker学习路线10:容器安全 下一篇Golang的基本数据类型-基本使用

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目