• golang中使用泛型


    泛型支持

    go >= 1.18

    为什么需要泛型

    编写一个函数用来比较两个数的大小,对于golang这种强类型的语言,要么针对不同的类型分别实现一遍,要么使用 interface{} 类型。

    func CompareInt64(a, b int64) bool {
    	if a >= b {
    		return true
    	} else {
    		return false
    	}
    }
    
    func CompareFloat64(a, b float64) bool {
    	if a >= b {
    		return true
    	} else {
    		return false
    	}
    }
    
    func Compare(a, b interface{}) bool {
    	switch a.(type) {
    	case int64:
    		a1 := a.(int64)
    		b1 := b.(int64)
    		if a1 >= b1 {
    			return true
    		} else {
    			return false
    		}
    	case float64:
    		a1 := a.(float64)
    		b1 := b.(float64)
    		if a1 >= b1 {
    			return true
    		} else {
    			return false
    		}
    	default:
    		return false
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    显然 interface{} 类型会打破强类型的约束,使程序暴露更多的错误。

    如果引入泛型就可以将 CompareInt64CompareFloat64 整合成一个函数。

    定义泛型函数

    golang支持泛型函数和泛型类型。

    // 泛型函数
    func GenericFunc[T any](args T) {
    	  
    }
    
    • 1
    • 2
    • 3
    • 4

    [T any]为类型约束,any 表示任意类型,(args T)为参数。

    如果只想支持特定的几个类型可以这样写。

    func GenericFunc[T int64|float64](args T) {
    	  
    }
    
    • 1
    • 2
    • 3

    如果类型太多了,可以这样表示

    type Number interface{
    	int | int32 | int64 | float64 | float32 
    }
    
    func GenericFunc[T Number](args T) {
    	  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    有时候需要类型可以进行算数运算,但是any中有些类型又是不支持的,因此需要用到编译器内置的约束 comparable,该类型必须支持== 方法。

    func GenericFunc[T comparable](args T) {
    	  
    }
    
    • 1
    • 2
    • 3

    如果使用了自定义的类型

    type MyInt int8
    func GenericFunc[T int64|float64|~int8](args T) {
    	  
    }
    
    • 1
    • 2
    • 3
    • 4

    如果只想支持一个类型,那就不需要使用泛型了。

    最终改造为

    func Compare[T int64|float64](a, b T) bool {
    	if a >= b {
    		return true
    	} else {
    		return false
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    多个参数多个类型

    func GenericFunc[A int64|float64, B int32|float32](a A, b B) {
    	  
    }
    
    • 1
    • 2
    • 3

    调用泛型函数

    在调用的时候一定要传入具体类型,或者依靠类型推断

    var a int64
    a = 10
    GenericFunc[int64](a)
    
    GenericFunc(a)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    定义泛型类型

    指的是复合类型,它包含了泛型。

    type KvMap[K comparable, V Number] map[K]V 
    
    func (kv KvMap[K,V]) Set(k K, v V) (KvMap[K,V]) {
    	kv[k] = v
    	return kv
    }
    
    type Slice[V Number] []V 
    
    func (s Slice[V]) Append(v V) (Slice[V]) {
    	s = append(s, v)
    	return s
    }
    
    type Kv [Vt Number] struct {
    	K string 
    	V Vt
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    比如定义一个结构体,使其可以满足 Number类型

    type Stack[V Number] struct {
        size int
        value []V
    }
    
    func (s *Stack[V]) Push(v V) {
        s.value = append(s.value, v)
        s.size++
    }
    
    func (s *Stack[V]) Pop() V {
        e := s.value[s.size-1]
        if s.size != 0 {
            s.value = s.value[:s.size-1]
            s.size--
        }
        return e
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    调用泛型类型

    // INT STACK
    strS := &Stack[int64]{}
    strS.Push(1)
    strS.Push(2)
    strS.Push(3)
    fmt.Println(strS.Pop())
    fmt.Println(strS.Pop())
    fmt.Println(strS.Pop())
    
    // FLOAT STACK
    floatS := &Stack[float64]{}
    floatS.Push(1.1)
    floatS.Push(2.2)
    floatS.Push(3.3)
    fmt.Println(floatS.Pop())
    fmt.Println(floatS.Pop())
    fmt.Println(floatS.Pop())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    泛型 OR interface

    两者的区别很明显,泛型虽然可以接受多个类型,但是最终类型是要固定下来的,他相当于是一个语法糖,经过词法分析和语法分析,最终的类型是明确的,并不需要反射,对性能没影响。而 interface 代表无类型限制,需要使用反射来知道其类型。

    泛型实现机制

    通常,把高级语言编译成机器本地可以执行的汇编代码,大致需要进行词法分析,语法分析,语义分析,生成中间代码,优化,以及最终生成目标代码等几个步骤。其中词法分析,语法分析,语义分析属于前端,而 golang 支持泛型只是前端的改动,本质上是语法糖。例如词法分析器要能正确解析泛型新引入的’[’ ‘]’ 括号,语法分析器能正确识别并判断代码是否符合泛型的语法规则,并构造正确的语法树 AST。而到了语义分析阶段,编译器需要能根据前面提到的类型参数和接口限制,来正确的推导出参数的实际类型,检查类型是否实现了相关接口定义的方法,实例化支持特定类型的函数,以及进行函数调用的类型检查等等。

    幸运的是,golang 团队已经给我们提供了两种途径来预先感受下泛型新特性,一种是通过https://go2goplay.golang.org/ 网站,用户可以在上面写合法的泛型代码,并编译执行,但是可能需要翻墙,且没有太多编译细节,这里不展开。

    我们重点讲下通过本地下载编译 go2go 工具来编译泛型代码。具体的 go2go 工具的编译过程,可以参考这篇文档, https://golang.org/doc/install/source。(使用go源码分支dev.go2go)

    下面我们来编译一个最基本的泛型示例代码,内容如下:

    import(
       "fmt"
    )
    func Print[T any](s []T) {
        for _, v := range s {
            fmt.Println(v)
        }
     }
    func main(){
         Print([]string{"Hello, ", "World
    "})
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    输入命令

    go tool go2go translate typeparam_basic.go2
    
    • 1

    注意 go2go 工具目前只支持.go2 后缀的源码文件。

    编译完成后,我们看代码长这个样子

    // Code generated by go2go; DO NOT EDIT.
    
    
    //line /Users/abc/work/go_generics_demo/typeparam_basic.go2:1
    package main
    
    //line /Users/abc/work/go_generics_demo/typeparam_basic.go2:1
    import "fmt"
    
    //line /Users/abc/work/go_generics_demo/typeparam_basic.go2:13
    func main() {
    //line /Users/abc/work/go_generics_demo/typeparam_basic.go2:13
     instantiate??Print?string([]string{"Hello, ", "World
    "})
    //line /Users/abc/work/go_generics_demo/typeparam_basic.go2:15
    }
    //line /Users/abc/work/go_generics_demo/typeparam_basic.go2:7
    func instantiate??Print?string(s []string,) {
    	for _, v := range s {
    		fmt.Println(v)
    	}
    }
    
    //line /Users/abc/work/go_generics_demo/typeparam_basic.go2:11
    type Importable? int
    
    //line /Users/abc/work/go_generics_demo/typeparam_basic.go2:11
    var _ = fmt.Errorf
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    可以看到工具已经自动为我们插入注释,并且实例化了一个支持 string slice 类型的函数,且为了避免和已有代码中的其它函数重名,造成错误,工具引入了两个不常用的 Unicode 字符,并插入到实例化的函数名称中,最后工具把生成的代码,重新命名为.go 后缀的文件,并写到文件系统。接下来我们就可以正常的编译执行生成的.go 代码。

    进一步的,我们可以通过编译 debug go2go 的源码,来看看究竟工具如何做这些做事情的,通过 debug go2go 工具,我们发现,其实 go2go 帮我们把使用泛型的 golang 代码,通过重写 AST 的方式,转换成 go 1.x 版本的代码, 如下所示:

    // rewriteAST rewrites the AST for a file.
    func rewriteAST(fset *token.FileSet, importer *Importer, importPath string, tpkg *types.Package, file *ast.File, addImportableName bool) (err error) {
    	t := translator{
    		fset:         fset,
    		importer:     importer,
    		tpkg:         tpkg,
    		types:        make(map[ast.Expr]types.Type),
    		typePackages: make(map[*types.Package]bool),
    	}
    	t.translate(file)
    
    	// Add all the transitive imports. This is more than we need,
    	// but we're not trying to be elegant here.
    	imps := make(map[string]bool)
    
    	for _, p := range importer.transitiveImports(importPath) {
    		imps[p] = true
    	}
    	for pkg := range t.typePackages {
        ......
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    上面的 AST 转换工具相关的代码和思路应该会被正式的 golang 编译器实现所借鉴。

    泛型实现对比

    说明

    C++

    C#

    Java

    Go

    实现

    使用宏生成对应类型的类/函数代码

    生成中间语言IL,运行时创建类型的专用类

    编译擦拭法,泛型当作object处理,只有一个类型

    编译为代码中所有类型的具体函数

    实际类型数量

    编译后,所有代码引用的类型

    所有的引用类型的泛型实例共享一个模板,而为一个不同的值类型,产生独立的代码。

    只有一个类型

    代码中引用的所有类型

    类型支持范围

    类,虚拟类,接口,虚拟接口,函数参数

    类,接口,委托,结构以及方法成员;

    类,函数,接口

    支持函数,结构体,map,slice

    优点

    无运行时负担,运行效率快。C++模板基于签名的隐式约束,灵活性高。

    1不会导致C++中代码膨胀的问题;2因为是JIT编译时实例化,可以应用于反射;3可以使用泛型参数约束来实现对类型参数的显式约束;4类型安全,不用向下转换,尤其是装箱拆箱操作。

    不会导致代码膨胀

    不影响运行效率。类型安全,编译期检查。

    缺点

    会导致代码膨胀。

    无相关资料

    只能使用参数Object的接口,对泛型支持比较弱;运行时生成类,效率较低。

    会导致代码膨胀

    先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

  • 相关阅读:
    linux之iptables防火墙
    【算法】Java-使用数组模拟单向链表,双向链表
    应用开发平台集成工作流系列之16——办理意见设计与实现
    c++|引用
    索引三星结构
    windows7中安装docker
    OpenShift 4 - 用 KEDA 实现基于定制指标的弹性部署扩展
    多个Python包懒得import,那就一包搞定!
    JAVA复习【11】单列集合Collection:ArrayList、 LinkedList、HashSet、TreeSet学习与使用
    Spring Cloud Gateway的使用总结
  • 原文地址:https://blog.csdn.net/m0_67393828/article/details/126101013