写点什么

Golang 泛型浅析

用户头像
David Liu
关注
发布于: 2021 年 04 月 01 日
Golang 泛型浅析

本文尝试梳理下泛型的基本知识,并有限的分析下 Golang 泛型的实现原理和机制,期望能够帮助人们加深对泛型的理解,并能够对 golang 的泛型实现原理和实现机制有初步的了解。


什么是泛型


首先来说说什么是泛型,泛型其实是一个很宽泛的概念,不一而足。本文中的泛型特指计算机编程语言中的泛型, 即编程语言中的函数,方法,类定义等与特定的类型参数无关,相关的函数,方法和类的实例化是根据具体的调用参数来进行。泛型是和编译紧密相关的技术,尤其是针对 golang 这种静态类型的语言来说,更是如此。


为什么要实现泛型


作为一种提高编程效能的基本范式,目前市面上主流的编程语言都已经早早支持了泛型,比如 C++ 的 template, Java 的类型擦除技术等,其主要目的都是为了最大限度的避免重复代码,利用编译器来做类型检查,避免额外的装箱/拆箱等操作,来提高程序的性能。泛型最常用的地方就是集合类库,使得集合类针对不同的类型能够复用,并且最大化开发者的费效比,减少维护成本。


Golang 泛型现状


那为什么 golang 到现在才要实现泛型呢,从 2009 年正式发布至今,golang 已经走过了 13 个年头,而 golang 流行度也是一路走来,水涨船高。但是其实如果从编程语言的特性角度来看,golang 其实已经落后了太多。社区之前也有很多的努力和尝试,包括各种泛型提案和实现方式,但最后都被否决了。Golang 核心作者给出的解释是泛型并不是不可或缺的特性。属于重要但不紧急,应该把精力集中在更重要的事情上,例如 GC 的延迟优化,编译器自举等。

但是鉴于目前泛型是 golang 社区呼声最高的,希望被尽快实现的语言特性,因此,泛型最后被提上了实现的日程,相关提议已经是被接受的状态,具体细节在这https://github.com/golang/go/issues/43651

目前社区已经进入了紧锣密鼓的开发阶段,打算最快在 go1.17,最晚在 g1.18 的版本中发布。


Golang 泛型语法简介

下面我们来介绍几种基本的泛型语法。

通过内置的 any 接口类型化参数来实现泛型

比如下面的函数:


func Print[T any](s []T) {
       for _, v := range s {
              fmt.Println(v)
        }
 }
复制代码

这里的[T any]即为类型参数,意思是该函数支持任何类型的 slice 。但是在调用该函数的时候,需要显式指定类型参数类型。如果想用该函数打印字符串 slice,则需要显式指定类型参数为 string, 以帮助编译器实行类型推导。

Print([]string{"Hello, ", "World\n"})
复制代码

如果不显式指定,则编译报错。

Print([]{"Hello, ", "World\n"})

typeparam_basic.go2:18:14: expected type, found '{'
复制代码
通过扩展的接口类型实现泛型(对参数实现约束和限制)

例如下面的接口定义:

 type Addable interface {    type int, int8, int16, int32, uint, uint8, uint16, uint32}
复制代码


相关的泛型函数的类型参数指定为 Addable 类型:


func Add[T Addable](a, b T) T {    return a + b}
复制代码


则调用方式为:

c := Add(2, 3)
复制代码

顺利编译通过。如果将调用参数改为不在 Addable 接口中指定的 float 类型,如:

c := Add(2.1, 3.2)
复制代码

则编译报错:

extended_inf.go2:14:10: float64 does not satisfy Addable (float64 or float64 not found in int, int8, int16, int32, uint, uint8, uint16, uint32)
复制代码
通过内置的扩展接口类型实现泛型(对泛型参数实现约束和限制)

例如 golang 语言泛型 sample 代码中,有个 Set 的泛型类型, 该类型定义如下:

// A Set is a set of elements of some type.type Set[Elem comparable] struct {m map[Elem]struct{}}
复制代码

该类型定义中,comparable 也是一个编译器内置的特定的扩展接口类型,该类型必须支持“==“ 方法,也就是任何支持等于比较操作的类型,都是该 Set 所支持的类型。

在这里我们先简单介绍上面几种基本的语法,当然泛型的使用方式远不止这些,具体的语法规则,可以看示例代码和后续 golang 更新的官方 spec,这里不再赘述。


Golang 泛型的实现机制


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


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

      

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


编译完成后,执行 go tool go2go 命令:


 我们可以看到 go2go 工具支持的命令选项。

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


import( "fmt")
func Print[T any](s []T) { for _, v := range s { fmt.Println(v) } }
func main(){ Print([]string{"Hello, ", "World\n"})}
复制代码

输入命令:

go tool go2go translate typeparam_basic.go2
复制代码

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


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

// Code generated by go2go; DO NOT EDIT.

//line /Users/zhaoliu/Desktop/go_generics_demo/typeparam_basic.go2:1package main
//line /Users/zhaoliu/Desktop/go_generics_demo/typeparam_basic.go2:1import "fmt"
//line /Users/zhaoliu/Desktop/go_generics_demo/typeparam_basic.go2:13func main() {//line /Users/zhaoliu/Desktop/go_generics_demo/typeparam_basic.go2:13 instantiate୦୦Print୦string([]string{"Hello, ", "World\n"})//line /Users/zhaoliu/Desktop/go_generics_demo/typeparam_basic.go2:15}//line /Users/zhaoliu/Desktop/go_generics_demo/typeparam_basic.go2:7func instantiate୦୦Print୦string(s []string,) { for _, v := range s { fmt.Println(v) }}
//line /Users/zhaoliu/Desktop/go_generics_demo/typeparam_basic.go2:11type Importable୦ int
//line /Users/zhaoliu/Desktop/go_generics_demo/typeparam_basic.go2:11var _ = fmt.Errorf
复制代码


可以看到工具已经自动为我们插入注释,并且实例化了一个支持 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 { ......
复制代码

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


总结:

由于 golang 泛型的实现涉及到编译器前端的诸多技术细节和语言的历史背景,本人不可能也没有能力通过短短一篇文章把所有的方面讲解清楚,目前社区通过多个分支并行开发来提供支持,感兴趣的读者可以自行下载源码阅读研究,本文主要是抛砖引玉,期望有更多的读者参与到开源技术的研究和推广中来并与大家分享。

       泛型代码适用的范围主要在集合,数学库,以及一些通用的算法和框架类库中, 滥用泛型会增加代码的编写和维护成本,得不偿失。最后,我们用 golang 编译器核心作者之一 Robert Griesemer 的话来总结本文: “泛型是带类型检查的宏指令,使用宏指令前请三思”。

参考:

https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md

https://github.com/golang/go/issues/43651

https://blog.golang.org/generics-proposal


发布于: 2021 年 04 月 01 日阅读数: 228
用户头像

David Liu

关注

让人生回归本真,让技术回归初心。 2017.12.04 加入

刘昭 FreeWheel 高级研发工程师,任职于 FreeWheel 基础设施部门,有大型开源社区的工作经历。 热衷于云计算,云原生,开源技术研究和分享。 目前致力于云计算,开源技术/规范等在公司基础设施部门的应用和推广。

评论

发布
暂无评论
Golang 泛型浅析