【1-7 Golang】Go语言快速入门—泛型

tomato01 · · 612 次点击 · · 开始浏览    

  Golang在1.18版本支持了泛型,写过java/c++等语言的可能对泛型有一定的了解。那么泛型到底是什么呢?他有什么作用呢? ## 为什么需要泛型   为什么需要泛型呢?Golang是强类型语言,任何变量或者函数参数,都需要定义明确的参数类型。假设我们需要实现这么一个函数,输入两个参数,函数返回其相加的值,输入参数可以是两个整型int,浮点数float,还有可能是字符串等等,这时候通常怎么办?定义多个函数实现吗?如下面程序所示: ``` //定义多个函数实现 func twoIntValueSum(a, b int) int { return a + b } func twoFloatValueSum(a, b float32) float32 { return a + b } func twoStrValueSum(a, b string) string { return a + b } //定义一个函数,类型是interface{} ```   这样就可能导致存在大量重复代码,而且调用方还需要根据参数类型决定调用哪一个方法。还能怎么办呢?只定义一个函数,只是参数是interface{},函数内部通过反射等方式,执行对应的操作,如下面程序: ``` func twoValueSum(a, b interface{}) (interface{}, error) { if reflect.TypeOf(a).Kind() != reflect.TypeOf(b).Kind() { return nil, errors.New("two value type different") } switch reflect.TypeOf(a).Kind() { case reflect.Int: return reflect.ValueOf(a).Int() + reflect.ValueOf(b).Int(), nil case reflect.Float64: return reflect.ValueOf(a).Float() + reflect.ValueOf(b).Float(), nil case reflect.String: return reflect.ValueOf(a).String() + " " + reflect.ValueOf(b).String(), nil default: return nil, errors.New("unknow value type") } } ```   使用反射实现的话,依赖反射性能较低,二来可以看到输入参数和返回值都是interface{},使用方还需要多执行一步返回值类型转换,而且反射相对而言还是比较复杂的。 ## 泛型初体验   那么还有其他什么办法吗?这就要说到Go 1.18版本实现的泛型了,泛型相当于定义了一个函数模板,真正调用函数的时候,再确定参数以及返回值等具体类型,基于泛型实现上述功能如下: ``` package main import "fmt" func main() { ret := twoValueSum[int](100, 200) fmt.Println(ret) ret1 := twoValueSum[string]("hello ", "world") fmt.Println(ret1) } func twoValueSum[T int | float64 | string](a T, b T) T { return a + b } ```   泛型类型或者泛型函数定义的语法格式可以描述为[Identifier TypeConstraint],上述程序中的T就是标识符(Identifier),int等就是TypeConstraint(类型限制,也就是说twoValueSum函数的输入参数类型只能是这几种,不能是其他的),注意在调用具体函数时,需要声明真正的类型。   下面举几个泛型程序事例,介绍下泛型类型常见定义方式(泛型函数参考twoValueSum函数定义): ``` //定义切片类型,元素类型可以是int,float64或者string type Slice[T int|float64|string ] []T //实例化变量arr1,注意声明了切片元素类型为int var arr1 Slice[int] = []int{1, 2, 3} //实例化变量arr2,注意声明了切片元素类型为string var arr2 Slice[string] = []string{"Hello", "World"} //自定义map类型,key只能是string,value可以是int、float32或者float64 type DefineMap[KEY string, VALUE int | float32 | float64] map[KEY]VALUE var m DefineMap[string, int] = map[string]int{ "zhangsan": 89, "lisi": 80, } ```   再举一个例子,假设想实现一个切片比较的函数(类似于字符串的字典序比较),该怎么定义呢?参数可以是[]int,[]float,[]string等等,只要元素可比较即可。参考官方给的事例: ``` //定义浮点数类型集合, type Float interface { ~float32 | ~float64 } //定义有符号整数集合 type Signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } //定义无符号整数集合 type Unsigned interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr } //定义整数集合 type Integer interface { Signed | Unsigned } //定义可比较类型集合:整数,浮点数,字符串 type Ordered interface { Integer | Float | ~string } //Compare函数需要输入两个切片,切片元素类型必须是可排序的,这里限制为类型Ordered func Compare[E Ordered](s1, s2 []E) int //二分法实现函数,输入切片以及查找元素,切片元素类型必须是可排序的,这里限制为类型Ordered func BinarySearch[E Ordered](x []E, target E) (int, bool) ```   注意目前Go语言标准库还没有使用泛型(Go作者不建议),不过有几个实验库使用了泛型,有兴趣的读者可以查阅: ``` golang.org/x/exp/constraints Constraints that are useful for generic code, such as constraints.Ordered. golang.org/x/exp/slices A collection of generic functions that operate on slices of any element type. golang.org/x/exp/maps A collection of generic functions that operate on maps of any key or element type. ```   另外,注意符号 ~ ,这是什么意思呢?假设我们定义一个泛型切片类型限制包含int,另外还有一个自定义类型(其实也是int),自定义类型能用来构造该切片吗?如下: ``` type Slice[T int | float64 | string] []T type Integer int var arr Slice[Integer] = []Integer{1,2,3} //Integer does not implement int|float64|string (possibly missing ~ for int in constraint int|float64|string) ```   注意虽然自定义类型Integer其实也就是int,但是这两种类型是不相等的,所以这里才有语法错误"Integer does not implement int",针对这种情况,Go语言给出的建议是使用符号 ~ 定义,如: ``` type Slice[T ~int| ~ float64 | ~string ] []T type Integer int //编译通过 var arr Slice[Integer] = []Integer{1,2,3} ```   更多的泛型语法,以及使用场景,有兴趣的读者继续研究,这里就不一一介绍了。 ## 泛型函数底层是怎么实现的   最后再思考一个问题,Go语言是如何实现上述泛型事例呢?为什么只定义一个函数实现,就能传递多种类型参数呢?为什么只定义一种变量类型,却能实例化多种类型的变量呢?   我们以下面的程序为例,看一下编译后的汇编代码,就明白其实现原理了: ``` package main import "fmt" func main() { ret := twoValueSum[int](100, 200) fmt.Println(ret) ret1 := twoValueSum[string]("hello ", "world") fmt.Println(ret1) } func twoValueSum[T int | float64 | string](a T, b T) T { return a + b } //go tool compile -S -N -l test.go "".main STEXT //main函数中的函数调用替换了! 0x0037 00055 (test.go:7) CALL "".twoValueSum[go.shape.int_0](SB) 0x00e4 00228 (test.go:10) CALL "".twoValueSum[go.shape.string_0] //编译阶段生成的两个具体的函数 "".twoValueSum[go.shape.int_0] STEXT "".twoValueSum[go.shape.string_0] STEXT ```   可以看到,我们只定义了一个函数实现twoValueSum,但是Go编译器为我们生成了两个具体的函数(因为我们调用了这两种函数实现),而针对twoValueSum的函数调用,也都在编译过程被替换了。 ## 总结   需要注意的是,Golang虽然在1.18版本支持了泛型,但是还是不建议在标准库使用,毕竟这次代码变动较大,而且后续泛型可能还会有较大变动,所以线上使用泛型需谨慎。

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

612 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传