当前位置: 首页 > news >正文

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)。

  1. 接口作为值类型:

当接口用作值类型时,它定义了一组方法,任何实现了这些方法的类型都可以赋值给这个接口变量。这是接口最常见的用法。

例如:

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 类型的变量。

  1. 接口作为元类型(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个整数和浮点数的大小&#xff0c;我们就要写2个函数。如下&#xff1a; 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个函数&#xff0c;除了数据类…...

【每日一练】python之sum()求和函数实例讲解

在Python中&#xff0c; sum()是一个内置函数&#xff0c;用于计算可迭代对象&#xff08;如列表、元组等&#xff09;中所有元素的总和。如下实例&#xff1a; """ 收入支出统计小程序 知识点:用户输入获取列表元素添加sum()函数&#xff0c;统计作用 "&…...

打造智慧校园德育管理,提升学生操行基础分

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

自定义函数---随机数系列函数

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

一文了解5G新通话技术演进与业务模型

5G新通话简介 5G新通话&#xff0c;也被称为VoNR&#xff0c;是基于R16及后续协议产生的一种增强型语音通话业务。 它在IMS网络里新增数据通道&#xff08;Data Channel&#xff09;&#xff0c;承载通话时的文本、图片、涂鸦、菜单等信息。它能在传统话音业务基础上提供更多服…...

视频使用操作说明书-T80002系列视频编码器如何对接海康NVR硬盘录像机,包括T80002系列高清HDMI编码器、4K超高清HDMI编码器

视频使用操作说明书-T80002系列视频编码器如何对接海康NVR硬盘录像机&#xff0c;包括T80002系列高清HDMI编码器、4K超高清HDMI编码器。 视频使用操作说明书-T80002系列视频编码器如何对接海康NVR硬盘录像机&#xff0c;包括T80002系列高清HDMI编码器、4K超高清HDMI编码器 同三…...

el-input-number计数器change事件校验数据,改变绑定数据值后change方法失效问题的原因及解决方法

在change事件中如果对el-input-number绑定的数据进行更改&#xff0c;会出现change事件失效的问题 试过&#xff1a;this.$set()及赋值等方法&#xff0c;都无法解决 解决方法&#xff1a;用$nextTick函数对绑定值进行更改&#xff08; this.$nextTick(() > { this.绑定…...

将vue项目整合到springboot项目中并在阿里云上运行

第一步&#xff0c;使用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题传送门 这道题不难发现&#xff0c;如果数字最终的和大于等于K&#xff0c;我们可以把这个原数列从大到小排序&#xff0c;得到最终答案。 如果和小于K&#xff0c;则从小到大排序&#xff0c;同时验证是否符合要求。 #pragma GCC optimize(3) //O2优化开启…...

开发编码规范笔记

前言 &#xff08;1&#xff09;该博客仅用于个人笔记 格式转换 &#xff08;1&#xff09;查看是 LF 行尾还是CRLF 行尾。 # 单个文件&#xff0c;\n 表示 LF 行尾。\r\n 表示 CRLF 行尾。 hexdump -c <yourfile> # 单个文件&#xff0c;$ 表示 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 是一款分布式的数据库生态系统&#xff0c; 可以将任意数据库转换为…...

Qt常用快捷键

Qt中的常用快捷键 F1查看帮助F2快速到变量声明 从cpp→hShift F2 函数的声明和定义之间快速切换 &#xff1b;选中函数名 &#xff0c;从h→cppF4在 cpp 和 h 文件切换 Shift F4在cpp/h文件与 界面文件中切换Ctrl /注释当前行 或者选中的区域Ctrl I自动缩进当前…...

关于RiboSeq分析流程的总结

最近关注了一下RiboSeq的分析方法&#xff0c;方法挺多的&#xff0c;但是无论哪种软件&#xff0c;都会存在或多或少的问题&#xff0c;一点问题不存在的软件不存在&#xff0c;问题的原因出在&#xff0c;1.有的脚本是用python2编写的&#xff0c;目前python2已经不能用了 2.…...

NLP任务:情感分析、看图说话

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

Linux桌面溯源

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

深入Linux:权限管理与常用命令详解

文章目录 ❤️Linux常用指令&#x1fa77;zip/unzip指令&#x1fa77;tar指令&#x1fa77;bc指令&#x1fa77;uname指令&#x1fa77;shutdown指令 ❤️shell命令以及原理❤️什么是 Shell 命令❤️Linux权限管理的概念❤️Linux权限管理&#x1fa77;文件访问者的分类&#…...

Mojo 编程语言:AI开发者的新宠儿

Mojo&#xff08;Meta Object Oriented programming for Java Objects&#xff09;是一种面向对象的编程语言&#xff0c;旨在简化和加速Java应用程序的开发过程。作为近年来新兴的编程语言&#xff0c;Mojo因其与Java的紧密集成以及AI开发领域的应用潜力而逐渐成为AI开发者的新…...

ARM/Linux嵌入式面经(十):极氪

开篇强调两个事情: pdf文件都在百度网盘群:911289806一定要把超链接里面的文章看了,那都是为了你们写的。老板!!!现在多学点,涨个2k工资,真的很值得。要不吃学习的苦,要不吃生活的苦。 1. 自我介绍 专开新篇,等我! 2. 项目介绍,提问 专开新篇,等我! 3. SPI通信和…...

【PVE】新增2.5G网卡作为主网卡暨iperf测速流程

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

Java 语言特性(面试系列1)

一、面向对象编程 1. 封装&#xff08;Encapsulation&#xff09; 定义&#xff1a;将数据&#xff08;属性&#xff09;和操作数据的方法绑定在一起&#xff0c;通过访问控制符&#xff08;private、protected、public&#xff09;隐藏内部实现细节。示例&#xff1a; public …...

2.Vue编写一个app

1.src中重要的组成 1.1main.ts // 引入createApp用于创建应用 import { createApp } from "vue"; // 引用App根组件 import App from ./App.vue;createApp(App).mount(#app)1.2 App.vue 其中要写三种标签 <template> <!--html--> </template>…...

MySQL中【正则表达式】用法

MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现&#xff08;两者等价&#xff09;&#xff0c;用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例&#xff1a; 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...

Redis数据倾斜问题解决

Redis 数据倾斜问题解析与解决方案 什么是 Redis 数据倾斜 Redis 数据倾斜指的是在 Redis 集群中&#xff0c;部分节点存储的数据量或访问量远高于其他节点&#xff0c;导致这些节点负载过高&#xff0c;影响整体性能。 数据倾斜的主要表现 部分节点内存使用率远高于其他节…...

以光量子为例,详解量子获取方式

光量子技术获取量子比特可在室温下进行。该方式有望通过与名为硅光子学&#xff08;silicon photonics&#xff09;的光波导&#xff08;optical waveguide&#xff09;芯片制造技术和光纤等光通信技术相结合来实现量子计算机。量子力学中&#xff0c;光既是波又是粒子。光子本…...

RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)

RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发&#xff0c;后来由Pivotal Software Inc.&#xff08;现为VMware子公司&#xff09;接管。RabbitMQ 是一个开源的消息代理和队列服务器&#xff0c;用 Erlang 语言编写。广泛应用于各种分布…...

CSS | transition 和 transform的用处和区别

省流总结&#xff1a; transform用于变换/变形&#xff0c;transition是动画控制器 transform 用来对元素进行变形&#xff0c;常见的操作如下&#xff0c;它是立即生效的样式变形属性。 旋转 rotate(角度deg)、平移 translateX(像素px)、缩放 scale(倍数)、倾斜 skewX(角度…...

PostgreSQL——环境搭建

一、Linux # 安装 PostgreSQL 15 仓库 sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-$(rpm -E %{rhel})-x86_64/pgdg-redhat-repo-latest.noarch.rpm# 安装之前先确认是否已经存在PostgreSQL rpm -qa | grep postgres# 如果存在&#xff0…...

macOS 终端智能代理检测

&#x1f9e0; 终端智能代理检测&#xff1a;自动判断是否需要设置代理访问 GitHub 在开发中&#xff0c;使用 GitHub 是非常常见的需求。但有时候我们会发现某些命令失败、插件无法更新&#xff0c;例如&#xff1a; fatal: unable to access https://github.com/ohmyzsh/oh…...

Linux安全加固:从攻防视角构建系统免疫

Linux安全加固:从攻防视角构建系统免疫 构建坚不可摧的数字堡垒 引言:攻防对抗的新纪元 在日益复杂的网络威胁环境中,Linux系统安全已从被动防御转向主动免疫。2023年全球网络安全报告显示,高级持续性威胁(APT)攻击同比增长65%,平均入侵停留时间缩短至48小时。本章将从…...