Go泛型详解
引子
如果我们要写一个函数分别比较2个整数和浮点数的大小,我们就要写2个函数。如下:
func Min(x, y float64) float64 {if x < y {return x}return y
}func MinInt(x, y int) int {if x < y {return x}return y
}
2个函数,除了数据类型不一样,其他处理逻辑完全一言。那有没有方法能一个函数完成上面的功能呢?有,那就是泛型。
func min2[T int | float64](x, y T) T {if x < y {return x}return y
}
泛型
官网文档:https://go.dev/blog/intro-generics
泛型为该语言添加了三个新的重要功能:
- 函数和类型的类型参数。
- 将接口类型定义为类型集,包括没有方法的类型。
- 类型推断,在许多情况下允许在调用函数时省略类型参数。
类型参数(Type Parameters)
现在允许函数和类型具有类型参数。类型参数列表看起来与普通参数列表类似,只是它使用方括号而不是圆括号。
package mainimport ("fmt""golang.org/x/exp/constraints"
)func GMin[T constraints.Ordered](x, y T) T {if x < y {return x}return y
}func main() {x := GMin[int](2, 3)fmt.Println(x) // 输出结果为2
}
其中constraints.Ordered是自定义类型(这里不展示源码)。
理解不了的,可以暂时把constraints.Ordered替换为 int | float64
。
向 GMin 提供类型参数(在本例中为 int)称为实例化(instantiation)。实例化分两步进行。
- 首先,编译器在整个泛型函数或类型中将所有类型实参替换为其各自的类型参数。
- 其次,编译器验证每个类型参数是否满足各自的约束。
成功实例化后,我们有一个非泛型函数,可以像任何其他函数一样调用它。例如,在类似的代码中
fmin := GMin[float64]
m := fmin(2.71, 3.14)
全部代码为
package mainimport ("fmt""golang.org/x/exp/constraints"
)func GMin[T constraints.Ordered](x, y T) T {if x < y {return x}return y
}func main() {fmin := GMin[float64] // 相当于func GMin(x, y float64) float64{...}m := fmin(2.71, 3.14)fmt.Println(m) // 输出结果为2.71
}
实例化 GMin[float64] 生成的实际上是我们原始的浮点 Min 函数,我们可以在函数调用中使用它。
类型参数也可以与类型一起使用。
type Tree[T interface{}] struct {left, right *Tree[T]value T
}func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }var stringTree Tree[string]
这里泛型类型 Tree 存储类型参数 T 的值。泛型类型可以有方法,就像本例中的 Lookup 一样。为了使用泛型类型,必须对其进行实例化; Tree[string] 是使用类型参数 string 实例化 Tree 的示例。
类型集(Type sets)
类型参数列表中的每个类型参数都有一个类型。由于类型参数本身就是一种类型,因此类型参数的类型定义了类型集。这种元类型称为类型约束。
在泛型方法GMin 中,类型约束是从约束包中导入的。 Ordered 约束描述了具有可排序值的所有类型的集合,或者换句话说,与 < 运算符(或 <= 、 > 等)进行比较。该约束确保只有具有可排序值的类型才能传递给 GMin。这也意味着在 GMin 函数体中,该类型参数的值可以用于与 < 运算符进行比较。
在 Go 中,类型约束必须是接口。也就是说,接口类型可以用作值类型(value type),也可以用作元类型(meta-type)。
- 接口作为值类型:
当接口用作值类型时,它定义了一组方法,任何实现了这些方法的类型都可以赋值给这个接口变量。这是接口最常见的用法。
例如:
type Stringer interface {String() string
}type Person struct {Name string
}func (p Person) String() string {return p.Name
}var s Stringer = Person{"Alice"} // Person 实现了 Stringer 接口
fmt.Println(s.String()) // 输出: Alice
在这个例子中,Stringer
接口被用作值类型,Person
类型实现了 String()
方法,因此可以赋值给 Stringer
类型的变量。
- 接口作为元类型(meta-type):
当接口用作元类型时,它定义了一组类型约束,用于泛型编程。
例如:
type Ordered interface {int | float64 | string
}func Min[T Ordered](a, b T) T {if a < b {return a}return b
}fmt.Println(Min(3, 5)) // 输出: 3
fmt.Println(Min(3.14, 2.71)) // 输出: 2.71
fmt.Println(Min("a", "b")) // 输出: a
在这个例子中,Ordered
接口被用作元类型,它定义了一组可以进行比较操作的类型(整数、浮点数和字符串)。Min
函数使用这个接口作为类型约束,可以接受任何满足 Ordered
约束的类型作为参数。
它们不仅可以定义对象的行为(作为值类型),还可以定义类型集合(作为元类型),从而在保持语言简洁性的同时,大大增强了代码的表达能力和复用性。
直到最近,Go 规范还说接口定义了一个方法集,大致就是接口中枚举的方法集。任何实现所有这些方法的类型都实现该接口。
但看待这个问题的另一种方式是说接口定义了一组类型,即实现这些方法的类型。从这个角度来看,作为接口类型集元素的任何类型都实现该接口。
这两种视图导致相同的结果:对于每组方法,我们可以想象实现这些方法的相应类型集,即由接口定义的类型集。
不过,就我们的目的而言,类型集视图比方法集视图有一个优势:我们可以显式地将类型添加到集合中,从而以新的方式控制类型集。
我们扩展了接口类型的语法来实现这一点。例如,interface{ int|string|bool } 定义了包含 int、string 和 bool 类型的类型集。
另一种说法是,该接口仅由 int、string 或 bool 满足。
现在让我们看看constraints.Ordered的实际定义:
type Ordered interface {Integer|Float|~string
}
该声明表示 Ordered 接口是所有整数、浮点和字符串类型的集合。竖线表示类型的联合(或本例中的类型集)。 Integer 和 Float 是在约束包中类似定义的接口类型。请注意,Ordered 接口没有定义任何方法。
对于类型约束我们通常不关心具体的类型,比如字符串;我们对所有字符串类型都感兴趣。这就是 ~
令牌的用途。表达式 ~string
表示基础类型为 string 的所有类型的集合。这包括类型 string 本身以及使用定义声明的所有类型,例如type MyString string
当然我们还是想在接口中指定方法,并且我们希望能够向后兼容。在 Go 1.18 中,接口可以像以前一样包含方法和嵌入接口,但它也可以嵌入非接口类型、联合和底层类型集。
用作约束的接口可以指定名称(例如 Ordered),也可以是内联在类型参数列表中的文字接口。例如:
[S interface{~[]E}, E interface{}]
这里S必须是一个切片类型,其元素类型可以是任何类型。
因为这是常见的情况,所以对于约束位置的接口,可以省略封闭的interface{},我们可以简单地编写(Go 语言中泛型的语法糖和类型约束的简化写法):
[S ~[]E, E interface{}]
由于空接口在类型参数列表以及普通 Go 代码中很常见,因此 Go 1.18 引入了一个新的预声明标识符 any 作为空接口类型的别名。这样,我们就得到了这个惯用的代码:
[S ~[]E, E any]
在使用类型约束时,如果省略了外层的interface{}会引起歧义,那么就不能省略。例如:
type IntPtrSlice1 [T * int][]T // 错误,这里会把*误会为乘号
type IntPtrSlice2[T *int,] []T // 正确,只有一个类型约束时可以添加`,`
type IntPtrSlice3[T interface{ *int }] []T // 正确,使用interface{}包裹
type IntPtrSlice4[T *int, T2 *int] []T // 正确
只有IntPtrSlice1是语法错误的,IntPtrSlice2-4语法正确
类型推断(Type inference)
函数参数类型推断
有了类型参数,就需要传递类型参数,这可能会导致代码冗长。回到我们的通用 GMin 函数:
func GMin[T constraints.Ordered](x, y T) T { ... }
类型参数 T 用于指定普通非类型参数 x 和 y 的类型。正如我们之前看到的,可以使用显式类型参数来调用它
var a, b, m float64m = GMin[float64](a, b) // explicit type argument
在许多情况下,编译器可以从普通参数推断出 T 的类型参数。这使得代码更短,同时保持清晰。
var a, b, m float64m = GMin(a, b) // no type argument
这是通过将参数 a 和 b 的类型与参数 x 和 y 的类型进行匹配来实现的。
这种从函数参数的类型推断出参数类型的推断称为函数参数类型推断。
约束类型推断
该语言支持另一种类型推断,即约束类型推断。为了描述这一点,让我们从缩放整数切片的示例开始:
// Scale returns a copy of s with each element multiplied by c.
// This implementation has a problem, as we will see.
func Scale[E constraints.Integer](s []E, c E) []E {r := make([]E, len(s))for i, v := range s {r[i] = v * c}return r
}
这是一个通用函数,适用于任何整数类型的切片。
现在假设我们有一个多维 Point 类型,其中每个 Point 只是给出点坐标的整数列表。这种类型自然会有一些方法。
type Point []int32func (p Point) String() string {// Details not important.
}
有时我们想要缩放一个点。由于 Point 只是整数切片,因此我们可以使用之前编写的 Scale 函数:
// ScaleAndPrint doubles a Point and prints it.
func ScaleAndPrint(p Point) {r := Scale(p, 2)fmt.Println(r.String()) // DOES NOT COMPILE
}
这无法编译,失败并出现类似 r.String undefined (type []int32 has no field or method String) 的错误。
完整代码见:https://gitee.com/qingfeng-169/blog-code/blob/master/csdn/generics/demo1/main.go
问题在于Scale(p, 2)
相当于Scale[int32](p, 2)
,Scale 函数返回 []E 类型的值,其中 E 是参数切片的元素类型(这里是int32)。当我们使用 Point 类型的值(其基础类型为 []int32)调用 Scale 时,我们返回的是 []int32 类型的值,而不是 Point 类型。这是通用代码的编写方式所遵循的,但这不是我们想要的。
为了解决这个问题,我们必须更改 Scale 函数以使用切片类型的类型参数。
// Scale returns a copy of s with each element multiplied by c.
func Scale[S ~[]E, E constraints.Integer](s S, c E) S {r := make(S, len(s))for i, v := range s {r[i] = v * c}return r
}
我们引入了一个新的类型参数 S,它是切片参数的类型。我们对其进行了约束,使基础类型为 S 而不是 []E,结果类型现在为 S。由于 E 被约束为整数,因此效果与之前相同:第一个参数必须是某种整数类型的切片。函数体的唯一变化是,现在当我们调用 make 时,我们传递 S,而不是 []E。
有助于理解的代码:https://gitee.com/qingfeng-169/blog-code/blob/master/csdn/generics/demo2/main.go
完整代码见:https://gitee.com/qingfeng-169/blog-code/blob/master/csdn/generics/demo3/main.go
有助于理解的代码:https://gitee.com/qingfeng-169/blog-code/blob/master/csdn/generics/demo4/main.go
但我们可以公平地问:为什么可以在不传递显式类型参数的情况下编写对 Scale 的调用?也就是说,为什么我们可以编写没有类型参数的 Scale(p, 2),而不是必须编写 Scale[Point, int32](p, 2)?我们的新 Scale 函数有两个类型参数,S 和 E。在不传递任何类型参数的 Scale 调用中,如上所述的函数参数类型推断可以让编译器推断 S 的类型参数是 Point。但该函数还有一个类型参数 E,它是乘法因子 c 的类型。对应的函数参数是 2,并且由于 2 是无类型常量,因此函数参数类型推断无法推断出 E 的正确类型(最多可能推断出 2 的默认类型为 int,这是不正确的)。相反,编译器推断 E 的类型参数是切片的元素类型的过程称为约束类型推断。
约束类型推断从类型参数约束中推导出类型实参。当一个类型参数具有根据另一类型参数定义的约束时使用它。当这些类型参数之一的类型参数已知时,约束用于推断另一个类型参数的类型参数。
应用这种情况的通常情况是,当一个约束对某种类型使用 ~type 形式,其中该类型是使用其他类型参数编写的。我们在 Scale 示例中看到了这一点。 S 是 ~[]E,即 ~ 后跟根据另一个类型参数编写的类型 []E。如果我们知道 S 的类型参数,我们就可以推断出 E 的类型参数。S 是切片类型,E 是该切片的元素类型。
这只是对约束类型推断的介绍。有关完整详细信息,请参阅提案文档或语言规范。
相关文章:

Go泛型详解
引子 如果我们要写一个函数分别比较2个整数和浮点数的大小,我们就要写2个函数。如下: func Min(x, y float64) float64 {if x < y {return x}return y }func MinInt(x, y int) int {if x < y {return x}return y }2个函数,除了数据类…...

【每日一练】python之sum()求和函数实例讲解
在Python中, sum()是一个内置函数,用于计算可迭代对象(如列表、元组等)中所有元素的总和。如下实例: """ 收入支出统计小程序 知识点:用户输入获取列表元素添加sum()函数,统计作用 "&…...

打造智慧校园德育管理,提升学生操行基础分
智慧校园的德育管理系统内嵌的操行基础分功能,是对学生日常行为规范和道德素养进行量化评估的一个创新实践。该功能通过将抽象的道德品质转化为具体可量化的指标,如遵守纪律、尊师重道、团结协作、爱护环境及参与集体活动的积极性等,为每个学…...

自定义函数---随机数系列函数
大家有没有发现平常在写随机数的时候,需要引入很多的头文件,然后还需要用一些复杂的函数,大家可能不太习惯,于是我就制作了一个头文件 // random_number.h #ifndef RANDOM_NUMBER_H // 预处理指令,防止头文件被重复包含…...

一文了解5G新通话技术演进与业务模型
5G新通话简介 5G新通话,也被称为VoNR,是基于R16及后续协议产生的一种增强型语音通话业务。 它在IMS网络里新增数据通道(Data Channel),承载通话时的文本、图片、涂鸦、菜单等信息。它能在传统话音业务基础上提供更多服…...
视频使用操作说明书-T80002系列视频编码器如何对接海康NVR硬盘录像机,包括T80002系列高清HDMI编码器、4K超高清HDMI编码器
视频使用操作说明书-T80002系列视频编码器如何对接海康NVR硬盘录像机,包括T80002系列高清HDMI编码器、4K超高清HDMI编码器。 视频使用操作说明书-T80002系列视频编码器如何对接海康NVR硬盘录像机,包括T80002系列高清HDMI编码器、4K超高清HDMI编码器 同三…...
el-input-number计数器change事件校验数据,改变绑定数据值后change方法失效问题的原因及解决方法
在change事件中如果对el-input-number绑定的数据进行更改,会出现change事件失效的问题 试过:this.$set()及赋值等方法,都无法解决 解决方法:用$nextTick函数对绑定值进行更改( this.$nextTick(() > { this.绑定…...

将vue项目整合到springboot项目中并在阿里云上运行
第一步,使用springboot中的thymeleaf模板引擎 导入依赖 <!-- thymeleaf 模板 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency> 在r…...
AC修炼计划(AtCoder Regular Contest 179)A~C
A - Partition A题传送门 这道题不难发现,如果数字最终的和大于等于K,我们可以把这个原数列从大到小排序,得到最终答案。 如果和小于K,则从小到大排序,同时验证是否符合要求。 #pragma GCC optimize(3) //O2优化开启…...

开发编码规范笔记
前言 (1)该博客仅用于个人笔记 格式转换 (1)查看是 LF 行尾还是CRLF 行尾。 # 单个文件,\n 表示 LF 行尾。\r\n 表示 CRLF 行尾。 hexdump -c <yourfile> # 单个文件,$ 表示 LF 行尾。^M$ 表示 CRLF …...
spring boot easyexcel
1.pom <!-- easyexcel 依赖 --><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.1.1</version></dependency><dependency><groupId>org.projectlombok</group…...

Docker 部署 ShardingSphere-Proxy 数据库中间件
文章目录 Github官网文档ShardingSphere-Proxymysql-connector-java 驱动下载conf 配置global.yamldatabase-sharding.yamldatabase-readwrite-splitting.yamldockerdocker-compose.yml Apache ShardingSphere 是一款分布式的数据库生态系统, 可以将任意数据库转换为…...

Qt常用快捷键
Qt中的常用快捷键 F1查看帮助F2快速到变量声明 从cpp→hShift F2 函数的声明和定义之间快速切换 ;选中函数名 ,从h→cppF4在 cpp 和 h 文件切换 Shift F4在cpp/h文件与 界面文件中切换Ctrl /注释当前行 或者选中的区域Ctrl I自动缩进当前…...
关于RiboSeq分析流程的总结
最近关注了一下RiboSeq的分析方法,方法挺多的,但是无论哪种软件,都会存在或多或少的问题,一点问题不存在的软件不存在,问题的原因出在,1.有的脚本是用python2编写的,目前python2已经不能用了 2.…...

NLP任务:情感分析、看图说话
我可不向其他博主那样拖泥带水,我有代码就直接贴在文章里,或者放到gitee供你们参考下载,虽然写的不咋滴,废话少说,上代码。 gitee码云地址: 卢东艺/pytorch_cv_nlp - 码云 - 开源中国 (gitee.com)https:/…...

Linux桌面溯源
X窗口系统(X Window System) Linux起源于X窗口系统(X Window System),亦即常说的X11,因其版本止于11之故。 X窗口系统(X Window System,也常称为X11或X)是一种以位图方式显示的软件窗口系统。…...

深入Linux:权限管理与常用命令详解
文章目录 ❤️Linux常用指令🩷zip/unzip指令🩷tar指令🩷bc指令🩷uname指令🩷shutdown指令 ❤️shell命令以及原理❤️什么是 Shell 命令❤️Linux权限管理的概念❤️Linux权限管理🩷文件访问者的分类&#…...
Mojo 编程语言:AI开发者的新宠儿
Mojo(Meta Object Oriented programming for Java Objects)是一种面向对象的编程语言,旨在简化和加速Java应用程序的开发过程。作为近年来新兴的编程语言,Mojo因其与Java的紧密集成以及AI开发领域的应用潜力而逐渐成为AI开发者的新…...
ARM/Linux嵌入式面经(十):极氪
开篇强调两个事情: pdf文件都在百度网盘群:911289806一定要把超链接里面的文章看了,那都是为了你们写的。老板!!!现在多学点,涨个2k工资,真的很值得。要不吃学习的苦,要不吃生活的苦。 1. 自我介绍 专开新篇,等我! 2. 项目介绍,提问 专开新篇,等我! 3. SPI通信和…...

【PVE】新增2.5G网卡作为主网卡暨iperf测速流程
【PVE】新增2.5G网卡作为主网卡暨iperf测速流程 新增网卡 新增网卡的首先当然需要关闭PVE母机,把新网卡插上,我用淘宝遥现金搞了个红包,花了26元买了块SSU的2.5G网卡。说实话这个价位连散热片都没有,确实挺丐的。稍后测下速度看…...

《基于Apache Flink的流处理》笔记
思维导图 1-3 章 4-7章 8-11 章 参考资料 源码: https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...

CVE-2020-17519源码分析与漏洞复现(Flink 任意文件读取)
漏洞概览 漏洞名称:Apache Flink REST API 任意文件读取漏洞CVE编号:CVE-2020-17519CVSS评分:7.5影响版本:Apache Flink 1.11.0、1.11.1、1.11.2修复版本:≥ 1.11.3 或 ≥ 1.12.0漏洞类型:路径遍历&#x…...

基于SpringBoot在线拍卖系统的设计和实现
摘 要 随着社会的发展,社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 在线拍卖系统,主要的模块包括管理员;首页、个人中心、用户管理、商品类型管理、拍卖商品管理、历史竞拍管理、竞拍订单…...

通过 Ansible 在 Windows 2022 上安装 IIS Web 服务器
拓扑结构 这是一个用于通过 Ansible 部署 IIS Web 服务器的实验室拓扑。 前提条件: 在被管理的节点上安装WinRm 准备一张自签名的证书 开放防火墙入站tcp 5985 5986端口 准备自签名证书 PS C:\Users\azureuser> $cert New-SelfSignedCertificate -DnsName &…...
DiscuzX3.5发帖json api
参考文章:PHP实现独立Discuz站外发帖(直连操作数据库)_discuz 发帖api-CSDN博客 简单改造了一下,适配我自己的需求 有一个站点存在多个采集站,我想通过主站拿标题,采集站拿内容 使用到的sql如下 CREATE TABLE pre_forum_post_…...

恶补电源:1.电桥
一、元器件的选择 搜索并选择电桥,再multisim中选择FWB,就有各种型号的电桥: 电桥是用来干嘛的呢? 它是一个由四个二极管搭成的“桥梁”形状的电路,用来把交流电(AC)变成直流电(DC)。…...

Vue3 PC端 UI组件库我更推荐Naive UI
一、Vue3生态现状与UI库选择的重要性 随着Vue3的稳定发布和Composition API的广泛采用,前端开发者面临着UI组件库的重新选择。一个好的UI库不仅能提升开发效率,还能确保项目的长期可维护性。本文将对比三大主流Vue3 UI库(Naive UI、Element …...

C++--string的模拟实现
一,引言 string的模拟实现是只对string对象中给的主要功能经行模拟实现,其目的是加强对string的底层了解,以便于在以后的学习或者工作中更加熟练的使用string。本文中的代码仅供参考并不唯一。 二,默认成员函数 string主要有三个成员变量,…...
2025年低延迟业务DDoS防护全攻略:高可用架构与实战方案
一、延迟敏感行业面临的DDoS攻击新挑战 2025年,金融交易、实时竞技游戏、工业物联网等低延迟业务成为DDoS攻击的首要目标。攻击呈现三大特征: AI驱动的自适应攻击:攻击流量模拟真实用户行为,差异率低至0.5%,传统规则引…...
41道Django高频题整理(附答案背诵版)
解释一下 Django 和 Tornado 的关系? Django和Tornado都是Python的web框架,但它们的设计哲学和应用场景有所不同。 Django是一个高级的Python Web框架,鼓励快速开发和干净、实用的设计。它遵循MVC设计,并强调代码复用。Django有…...