目录

前言

经过这几年的千呼万唤,简洁的Go语言终于在1.18版本迎来泛型编程。作为一门已经有了14年历史的强类型语言,很难相信它到现在才开始有一个正式的泛型。

虽然直到1.18版本才加入泛型,但是在2014年便有相关的讨论要在Go中加入泛型设计。但是由于各种原因没有实现。而之后的接口(interface)的提出,让泛型进一步搁置。但是由于接口的缺陷,最终Go团队还是在1.18的版本中加入了泛型。实际上,这一版本的泛型设计在语言层面和接口非常相似(在实现层面肯定是不一样的,泛型是编译时,接口是运行时),对于他们之间的差异,也会在后面提到。

本文主要讲述1.18beta1版本中的泛型,后续有改动,可能会更改文章。

泛型是什么

在我看来泛型其实用C++的模板一词来描述就非常的准确。在写代码的时候,我们经常需要写很多重复的逻辑,一般这个时候我们就会使用函数来对其进行封装。但是由于Go是一种强类型语言,所以在定义和书写函数的时候需要在调用前标明类型。当然如果这一重复的逻辑只需要固定的类型,这样就足够了,但是很多时候我们需要不同的类型进行类似的逻辑,譬如我们刚刚看到的GIF。对于普通开发人员来说这种情况可能遇到的比较少,但是在一些库开发人员来说,这种情况变得非常的普遍。

泛型程序设计(generic programming)是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。Ada、Delphi、Eiffel、Java、C#、F#、Swift 和 Visual Basic .NET 称之为泛型(generics);ML、Scala 和 Haskell 称之为参数多态(parametric polymorphism);C++ 和 D称之为模板。具有广泛影响的1994年版的《Design Patterns》一书称之为参数化类型(parameterized type)。 [1]

其中,C++的模版应该是做的最完善的,不仅支持简单的模板替换,还可以处理一些简单的逻辑,经过不断的迭代,已经形成了一种生成代码的编程方式,因此也叫做模板元编程(Template metaprogramming)[2]。当然由于其和C++编程方式完全不一致,所以可读性非常的差。而在Go的泛型设计中,为了保证泛型的简洁,Go并不支持模版元编程(心塞,还想试试在Go里面往往骚操作呢)。

Go的泛型

接下来就是Go泛型的使用介绍了,Go支持泛型函数和泛型类型。

泛型函数

先来一个最简单的泛型函数

func ink19FirstGen[T any](t T) {
fmt.Println(t)
}

这是一个非常简单的的函数,就是使用fmt.Println打印输入的参数。相比于以前的函数,多了[T any]部分,这就是Go泛型的参数列表。

参数列表中的参数由两部分组成,参数名和约束,其中T就是参数,any为参数的约束。从表达上来说,和Go语言一贯的风格相似,名在前,类型在后。

在Go语言中,使用接口interface做为类型的约束,其中any = interface{},即为无限制,但是以其说是无限制,倒不如说是完全限制,由于any里面没有定义任何的方法,所以在函数里面也没办法调用t的任何方法。

这里有一个非常重要的问题,就是相比较于C++的模板,Go会在定义函数的时候就对函数进行解析。所以在函数中使用了的方法,一定要在约束的接口中出现。

type ink19Inf interface {
Test()
} func ink19FirstGen[T ink19Inf](t T) {
t.Test()
}

和普通参数类似的,如果是相同的约束,参数类型也支持简化

func ink19FirstGen[T ,T2 ink19Inf](t T, t2 []T2) {
t.Test()
}

泛型类型

和C++中的模板类类似的,Go里面也有泛型类型,它的定义也很简单

type ink19Vector[T any] []T

结构相比与以前的类型定义多了[T any]部分,这一部分的结构和泛型函数那一部分类似就不多介绍了。

对于泛型类型,Go也可以定义相关的方法,譬如:

func (m *ink19Vector[T]) Push(v T) *ink19Vector[T] {
*m = append(*m, v)
return m
}

在泛型结构体中,结构体也可以定义自己的类型的变量,形成链表

type List[T1, T2 any] struct {
next *List[T1, T2]
t1 T1
t2 T2
}

PS:依据提案中的说法,第二行的参数列表应该和定义中的顺序一致,以防止无限递归。但是在1.18beta1版本的实测中,顺序不一致的写法并不会报错。

Go暂时不支持方法的泛型。

类型集合

虽然通过接口限制类型可以满足绝大部分的要求,但是仍然有一些需求满足不了,譬如运算符。假如我们有一个函数,可以传入任意可比较的参数,然后返回较小的那一个。很自然的,我们可以写下如下的代码:

func whoismin[T any](a, b T) T {
if a < b {
return a
}
return b
}

但是,很遗憾的,由于我们对T的约束是any。所以其实来说,我们没办法对ab做任何的操作,对比也是。所以在这里,我们会收到报错

invalid operation: cannot compare a < b (operator < not defined on T)

为了解决这一问题,提案中提出了类型集合的概念。

对于一个类型,认为它代表的类型集合就是只包含这个类型的集合,即对于类型M来说,其代表的类型集合为{M}。而对于接口来说,其对应的类型集合是无限的,只要一个类型满足接口的所有方法签名,那么这个类型就是属于这个接口的类型集合中。其实很容易理解类型集合就是那个识别符可以代表的类型的集合。

考虑集合的操作,对于下面这个例子

type ink19Inf1 interface {
What1()
} type ink19Inf2 interface {
What2()
} type ink19Inf3 interface {
What1()
What2()
}

假设ink19Inf1的类型集合为\(A\),ink19Inf2的为\(B\),ink19Inf3的为\(C\)。那么很容得到\(C=A \bigcap B\)。即\(C\)为\(A\)和\(B\)的交集。当然只有交集是不行的,后面还有说明实现并集。

为了进一步的说明类型集合,我们先来回忆一下接口的定义,对于之前的接口来说,接口的元素一共有两种:方法签名和其他接口。

type ink19Inf1 interface {
What()
} type ink19Inf2 interface {
ink19Inf1
It()
}

比如ink19Inf2中的第一个元素就是其他接口,第二个元素是其他签名。但是仅仅只是有这两种元素,对于泛型约束来说是完全不够的。为此,提案中加入了另外三种不同的元素,需要注意的是,如果一个接口加入了这额外三种元素,那么这个接口就不能再作为普通的接口使用了,只能用作泛型。

第一个增加的是类型元素。以前的接口是不能用类型作为接口的,但是在作为约束中可以这样操作。作为元素的时候就是提供了一个只包含自己本身的类型作为元素的类型集合。

第二个是增加了近似约束元素,写法是在类型前面增加~符号,如

type ink19Inf1 interface {
~int
}

这一个元素的意义是为接口提供了一个所有以int为底层类型的集合。所以被~修饰的类型也应该是一个底层类型,不然提供的集合就是空集,没有任何意义。具体的区别可以看下面的这个例子。

type ink19Inf3 interface {
int
} type ink19Inf4 interface {
~int
} type MyInt int

首先我们定义了两个接口,第一个接口使用的是额外的第一种元素, 因此它的类型集合只包含了int。另一个使用了第二种元素,它的类型集合包含了所有以int为底层类型的类型。然后我们定义了一个MyInt类型,它是以int为底层类型的类型。需要注意的是,在Go中MyIntint是两种不同的类型。最后我们写两个方法来分别使用两个接口为约束。

func ink19Print1[T ink19Inf3](t T) {
fmt.Println(t)
} func ink19Print2[T ink19Inf4](t T) {
fmt.Println(t)
} var data MyInt = 1
ink19Print1(data) // 错误
ink19Print2(data)

第三个元素是联合约束。使用方法如下

type ink19Inf5 interface {
int | float32 | bool | ~string | ink19Inf3
}

使用方法非常简单,就是将并集的元素一个一个使用|连接就就好了。需要注意的是联合约束的元素只支持类型,近视约束和其他只包含以上三种额外元素的接口(即,不支持包含方法签名的接口)。

回到之前的问题,对于需要使用操作符的情况,有了以上的工具后就可以解决了。

纵观整个Go语言,由于并不支持操作符,所以有操作符(除了==!=)的其实只有有限的几种类型,譬如:intfloat32string等等。

所以对于需要使用比较运算符的约束的时候,可以使用如下的一个约束接口:

type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}

为了方便使用,Go标准库里面提供了一个constraints来提供相关的约束。

上面提到,对于除了==!=以外的操作符可以通过对所有的类型进行枚举来实现。但是对于这两个操作符,用户自定义的类型也会有这两个操作符,没办法枚举实现。官方给出的方法是通过使用一个一个内建的约束comparable来完成操作。譬如

func IsSame[T comparable](a T, b T) bool {
return a == b
}

和接口的差异

由于本人对于Go的接口使用并不多,所以如果有不足的地方请及时指正。

  1. 实现方法上,泛型是编译时,接口是运行时;
  2. 可以实现操作符的约束;
  3. 返回的参数可以是特定的类型,而接口只能返回固定的接口类型;
  4. 相比较于接口,泛型的约束可以有更多的操作。

总结

以上就是Go语言泛型的使用。总的来说,比较完整,实现了大部分的功能,相比于接口,有一定的差异。从体验上来说有较高的提升,但是其缺点也非常的多。首先是其后面提出的三种元素,它将接口和类型限制隔离开了,这是一个特别奇葩的操作,感觉不符合Go语言的简洁实现。添加的三种元素中,我们主要来看第三种,联合。代码在分析的时候会对每一个元素测试,看看能不能通过编译。所以从集合的角度上来看,如果我们把一个类型可以进行的操作可做是一个集合,那么这一个联合就是在一个限定的类型集合里面(枚举出的)对每一个类型的操作集合进行一个交集操作。回到原来,其实出现这个语法特性的最大原因就是Go语言不支持操作符重载,所以没办法对操作符进行枚举,那为什么不直接在这个版本实现操作符重载呢?或者直接不考虑这一部分,让传入的结构体只能使用方法,不能使用操作符。并且,即使加入了这三种元素,还是有两种操作符!===无法使用现在有的实现,只能使用一个内建的符号来代表这一类的方法,个人感觉非常丑陋。


  1. 泛型编程 - 维基百科,自由的百科全书

  2. 模板元编程 - 维基百科,自由的百科全书

Go1.18中的泛型编程的更多相关文章

  1. scikit-learn 0.18中的cross_validation模块被移除

    环境:scikit-learn 0.18 , python3 from sklearn.cross_validation import train_test_split from sklearn.gr ...

  2. mint 18中安装最新的R

    mint 中默认的R版本有点老,升级最新版方法如下: 先卸载 sudo apt-get remove r-base-core 添加mint 18 识别的源 sudo echo "deb ht ...

  3. 【记录一个问题】go1.17中,把代码文件放在main.go的同级目录,导致无法编译

    写了类似目录结构的代码: myproxy - main.go - server.go 编译的时候总是出现main.go中找不到类型定义.但是用goland却可以直接执行. 最后调整了目录结构后解决: ...

  4. go1.18泛型的简单尝试

    今天golang终于发布了1.18版本,这个版本最大的一个改变就是加入了泛型.虽然没有在beta版本的时候尝试泛型,但是由于在其他语言的泛型经验,入手泛型不是件难事~ 官方示例 Tutorial: G ...

  5. 浅谈cocos2dx(18) 中工厂模式

    ----我的生活.我的点点滴滴! ! cocos2d-x中也有工厂模式.何为工厂模式,顾名思义就是用来产生产品的,工厂就是用来创建其它类对象的类,我们把这个创建其它类对象的类叫做工厂类.而这些被创建的 ...

  6. struts从2.3.X升级到2.5.18中遇到的问题及解决办法

    1,2.5.X版本不再提供xwork.jar ,整合到了 struts-core包中.2,需要升级替换的jar文件:commons-langcommons-lang3ognl其他所有struts2开头 ...

  7. Golang 基础之基础语法梳理 (三)

    大家好,今天将梳理出的 Go语言基础语法内容,分享给大家. 请多多指教,谢谢. 本次<Go语言基础语法内容>共分为三个章节,本文为第三章节 Golang 基础之基础语法梳理 (一) Gol ...

  8. Java 18 新增@snipppet标签,注释中写样例代码更舒适了!

    在这次的Java 18中,新增了一个@snipppet标签,主要用于JavaDoc中需要放示例代码的场景.其实在Java 18之前,已经有一个@code标签,可以用于在JavaDoc中编写小段的代码内 ...

  9. Go 1.14 中 Cleanup 方法简介

    目录 一般的测试 使用 defer 清除依赖 使用 Cleanup 关于t.Parallel 总结 原文:What's New In Go 1.14: Test Cleanup 单元测试通常遵循某些步 ...

随机推荐

  1. HDU 6984 - Tree Planting(数据分治+状压 dp)

    题面传送门 傻逼卡常屑题/bs/bs,大概现场过得人比较少的原因就是它比较卡常罢(Fog 首先对于这样的题我们很难直接维护,不过注意到这个 \(n=300\) 给得很灵性,\(k\) 比较小和 \(k ...

  2. 【GS文献】植物育种中基因组选择的方法、模型及展望

    目录 1. GS/GP在植物育种中的角色 2. GP模型应用 3. GP模型的准确性 4. 植物育种的GS展望 5. 小结 Genomic SelectioninPlant Breeding: Met ...

  3. nohup使用

    nohup:不挂断运行 在忽略挂起信号的情况下运行给定的命令,以便在注销后命令可以在后台继续运行. 可以这么理解:不挂断的运行,注意并没有后台运行的功能,就是指,用nohup 运行命令可以是命令永远运 ...

  4. 自定义char类型字符,django中事务

    自定义char类型字符 # 自定义char类型,继承Field父类 class MyCharField(Field): def __init__(self, max_length, *args, ** ...

  5. do{...}while(0)的用法

    零.导引第一次见到 do{...}while(0)是在学习libevent的时候,看到里面有很多类似#define TT_URI(want) do { \ char *ret = evhttp_uri ...

  6. kubectl logs查看日志时出现failed to create fsnotify watcher: too many open files

    因为系统默认的 fs.inotify.max_user_instances=128 太小,在查看日志的pod所在节点重新设置此值: 临时设置 sudo sysctl fs.inotify.max_us ...

  7. 『学了就忘』Linux文件系统管理 — 65、LVM逻辑卷管理介绍

    目录 1.LVM逻辑卷管理的简介 2.LVM逻辑卷管理的原理 3.总结建立LVM分区的步骤 1.LVM逻辑卷管理的简介 LVM是Logical Volume Manager的简称,中文就是逻辑卷管理. ...

  8. SimpleNVR如何把安防监控画面推流到微信公众号直播

    背景需求 进入移动互联网时代以来,微信已成为许多企业除官网以外必备的宣传渠道,当3.2亿直播用户与九亿微信用户的势能增加,在微信上开启直播已成为越来越多企业的不二选择. 需求分析 微信公众号作为平台来 ...

  9. Hadoop 相关知识点(二)

    1.HDFS副本机制 Hadoopde 默认副本布局策略是: (1)在运行客户端的节点上放置第一个副本(如果客户端运行在集群之外,就随机选择一个节点,不过系统会避免选择那些存储太满或者太忙的节点): ...

  10. SpringCloud微服务实战——搭建企业级开发框架(三十二):代码生成器使用配置说明

    一.新建数据源配置 因考虑到多数据源问题,代码生成器作为一个通用的模块,后续可能会为其他工程生成代码,所以,这里不直接读取系统工程配置的数据源,而是让用户自己维护. 参数说明 数据源名称:用于查找区分 ...