Go进阶概览 -【7.2 泛型的使用与实现分析】
7.2 泛型的使用与实现分析
泛型是Go 1.18
引入的概念,在引入这个概念前经过了好几年的考量最终才将这这个特性加进去。
泛型在多种语言中都是存在的,比如C++
、Java
等语言中都有泛型的概念。
本节我们将针对泛型的使用、实现原理进行整体的讲解。
本节代码存放目录为 lesson20
泛型基础
什么是泛型?
简单来说,泛型与空接口interface{}
相似但又有不同。我们知道空接口可以用来标识任意的类型,其实泛型也是干这件事情的。
那么既然有了接口,为什么还要出现泛型呢?这需要结合我们之前章节的反射来一起看待。
在我们使用interface{}
与反射来进行处理时,其实都是在运行时处理,运行时处理那么不可避免的就会出现性能开销与安全性问题。
而泛型则是在编译阶段进行处理的,而不是运行时处理,所以不管是从性能还是安全性来说,泛型都是一种更好的选择。
另外泛型主要用于函数与类型的的定义,而不能用于普通变量的定义,这也是与interface{}
的主要区别。
泛型的主要应用是在函数的定义上。当我们有一些公用函数,比如说:打印、求和等,在没有泛型的时候,我们需要定义一个参数为int
型的Print
、一个参数为string
型的Print
,但是如果是泛型的话我们中需要定义一个即可。
泛型函数的实现与应用
- 简单泛型函数
func Print[T any](input T) {fmt.Println(input)
}Print(1)
Print("hello")
在上面的代码中,我们通过泛型仅定义了一个函数,即实现了传递int
、string
参数的目的。
定义的格式也是固定的:func funcName[T any](arg T)
,其中[T any]
就标识这个函数是一个泛型函数。
- 多个类型参数
func Add[T int | float64](a, b T) T {return a + b
}fmt.Println(Add(1, 2))
fmt.Println(Add(1.5, 2.3))
在上面的代码中,我们传入了多个参数,同时我们可以看到,使用了[T int | float64]
这样的格式。
那么这种格式是什么意思呢?如果我们这样写,其实就代表这个函数接收的参数只能是int
、float64
两种类型的。
基于我们函数的功能,如果传入string
、结构体
等类型的参数,那么肯定是不符合的,所以我们可以在函数中就指定好传入的类型范围。
泛型结构体
在结构体中使用泛型也是比较常用的一个操作,比如我们的结构体字段是相同的,但是会接收不同类型的值,那么使用泛型也是一个很好的选择。
type Container[T any] struct {value T
}intContainer := Container[int]{value: 42}
fmt.Println(intContainer.value)stringContainer := Container[string]{value: "hello"}
fmt.Println(stringContainer.value)
结构体泛型使用会比较广泛,特别是在一些算法或数据结构类型的场景。比如说实现一个栈、一个队列,那么我们就可以使用泛型来实现,这样栈就可以存储多种数据类型。
type Stack[T any] struct {items []T
}func (s *Stack[T]) Push(item T) {s.items = append(s.items, item)
}func (s *Stack[T]) Pop() T {n := len(s.items)item := s.items[n-1]s.items = s.items[:n-1]return item
}// 创建一个整数栈
intStack := Stack[int]{}
intStack.Push(1)
intStack.Push(2)
fmt.Println(intStack.Pop())// 创建一个字符串栈
stringStack := Stack[string]{}
stringStack.Push("hello")
stringStack.Push("world")
fmt.Println(stringStack.Pop())
通过泛型我们就可以简单的进行处理,这种方法其实是比interface{}
高效很多的。
泛型的约束与接口
-
基本泛型约束
// Compare 约束 T 必须可比较(类型必须实现了comparable接口) func Compare[T comparable](a, b T) bool {return a == b }fmt.Println(Compare(1, 2)) fmt.Println(Compare("Go", "Go"))
在上面的代码中,
T
类型参数使用了comparable
作为约束,表示T
必须是支持比较操作的类型,例如整数、字符串等。comparable
是Go
的内置接口,用于表示可以比较的类型(支持==
和!=
操作)。 -
自定义接口作为约束
// Stringer 定义一个接口 type Stringer interface {String() string }// PrintString 泛型函数,T 必须实现 Stringer 接口 func PrintString[T Stringer](item T) {fmt.Println(item.String()) }// Person 实现 Stringer 接口的类型 type Person struct {Name string }func (p Person) String() string {return p.Name }
在这个例子中,泛型函数
PrintString
限定类型参数T
必须实现Stringer
接口。也就是说
PrintString
只能用于那些实现了Stringer
接口的类型,比如Person
。 -
内置的泛型约束
// Number 泛型约束 T 必须是 int 类型的别名 type Number interface {~int }func Sum[T Number](a, b T) T {return a + b }type MyInt int // MyInt 是 int 的别名var a MyInt = 10 var b MyInt = 20 fmt.Println(Sum(a, b))
在上面的代码中,
~int
表示类型参数T
可以是int
或任何int
的别名类型(如MyInt
)。
实现原理
如果了解Java
的话我们可以知道,Java
只要函数参数的类型不同,那么函数名称可以是相同的。
在Go
语言中,泛型其实差不多就是这么实现的。在编译的时候,编译器会生成多个类型的函数。
如下代码所示:
func Print[T any](input T) {fmt.Println(input)
}Print(1)
Print("hello")
在上面的代码中,我们实现了一个简单的打印函数,调用的时候传入了int
与string
类型的数据。
那么在我们编译的时候,编译器可能会生成下面的代码:
func Print1[int](input int) {fmt.Println(input)
}func Print2[string](input string) {fmt.Println(input)
}
执行的大概示意图如下所示:
泛型函数 Print[T]
+-----------------+
| 泛型代码 |
+-----------------+|Monomorphization(为不同值类型生成副本)|+------------+| PrintInt() | // 为 int 类型生成的函数副本+------------+| PrintStr() | // 为 string 类型生成的函数副本+------------+| PrintF64() | // 为 float64 类型生成的函数副本+------------+
Go
的泛型使用了多种方式,上面描述的属于其中的一种方式,也就是单态化,这种方式主要应用于参数是值的函数。
上面我们提到的这种方式虽然简单,但是如果函数副本太多的话,最终编译出来的二进制文件肯定是很大的,所以还采用了虚拟方法表的方式。
当泛型函数接收的是指针类型或接口类型时,编译器会为它生成一个字典表。这个表类似于虚拟方法表,记录了如何在运行时处理不同类型的操作。
我们可以通过下面的示意图来理解:
编译时:
+--------------------------------------------+
| 编译器检查到 Person 实现了 Stringer 接口 |
+--------------------------------------------+|v
+-----------------------------------------+
| 生成 Person 的虚拟方法表(VMT) |
| 包含 String() 指向 Person.String 的指针 |
+-----------------------------------------+运行时:
+-------------------------------------+
| 调用 PrintString(p) |
+-------------------------------------+|v
+---------------------------+
| 查找 p 的虚拟方法表 | --> 找到 Person.String() 方法
+---------------------------+|v
+----------------------+
| 调用 Person.String() |
+----------------------+
虚拟方法表比较抽象,我们以一句话理解就可以:调用的时候,会去查找PrintString(p)
中p
的方法表,最终找到了Person.String()
,这时候就直接执行就可以了。
Go
语言的泛型实现还在持续优化中,我们可以持续关注,现阶段掌握泛型的使用即可。
小结
本节我们讲解了泛型的基础概念、使用以及简单的实现原理。泛型为Go
语言带来了更大的灵活性,帮助开发者编写更具通用性的代码。
在框架开发、工具开发场景应用比较广泛,通过泛型我们可以简单的将代码合并优化。
我的GitHub:https://github.com/swxctx
书籍地址:https://d.golang.website/
书籍代码:https://github.com/YouCanGolang/GoDeeperCode
相关文章:
Go进阶概览 -【7.2 泛型的使用与实现分析】
7.2 泛型的使用与实现分析 泛型是Go 1.18引入的概念,在引入这个概念前经过了好几年的考量最终才将这这个特性加进去。 泛型在多种语言中都是存在的,比如C、Java等语言中都有泛型的概念。 本节我们将针对泛型的使用、实现原理进行整体的讲解。 本节代…...

罗德岛战记游戏源码(客户端+服务端+数据库+全套源码)游戏大小9.41G
罗德岛战记游戏源码(客户端服务端数据库全套源码)游戏大小9.41G 下载地址: 通过网盘分享的文件:【源码】罗德岛战记游戏源码(客户端服务端数据库全套源码)游戏大小9.41G 链接: https://pan.baidu.com/s/1y0…...

AI+教育|拥抱AI智能科技,让课堂更生动高效
AI在教育领域的应用正逐渐成为现实,提供互动性强的学习体验,正在改变传统教育模式。AI不仅改变了传统的教学模式,还为教育提供了更多的可能性和解决方案。从个性化学习体验到自动化管理任务,AI正在全方位提升教育质量和效率。随着…...
WebServer
一、服务器代码 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <unistd.h> #define PORT 80 #define BUFFER_SIZE 1024 void ha…...

java项目之基于spring boot的多维分类的知识管理系统的设计与实现源码
项目简介 基于spring boot的多维分类的知识管理系统的设计与实现实现了以下功能: 基于spring boot的多维分类的知识管理系统的设计与实现的主要使用者管理员可以管理用户信息,知识分类,知识信息等,用户可以查看和下载管理员发布…...

go的结构体、方法、接口
结构体: 结构体:不同类型数据集合 结构体成员是由一系列的成员变量构成,这些成员变量也被称为“字段” 先声明一下我们的结构体: type Person struct {name stringage intsex string } 定义结构体法1: var p1 P…...
力扣第一题——删除有序数组中的重复项
给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次,返回删除后数组的新长度。不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1)额外空间的条件下完成。 示例 1&#x…...

Tuxera NTFS for Mac 2023绿色版
在数字化时代,数据的存储和传输变得至关重要。Mac用户经常需要在Windows NTFS格式的移动硬盘上进行读写操作,然而,由于MacOS系统默认不支持NTFS的写操作,这就需要我们寻找一款高效的读写软件。Tuxera NTFS for Mac 2023便是其中…...

LeetCode[中等] 155. 最小栈
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。 实现 MinStack 类: MinStack() 初始化堆栈对象。void push(int val) 将元素val推入堆栈。void pop() 删除堆栈顶部的元素。int top() 获取堆栈顶部的元素。int get…...
Python青少年简明教程目录
Python青少年简明教程目录 学习编程语言时,会遇到“开头难”和“深入难”的问题,这是许多编程学习者都会经历的普遍现象。 学习Python对于青少年来说是一个很好的编程起点,相对容易上手入门,但语言特性复杂,应用较广&…...
Revit学习记录-版本2018【持续补充】
将墙面拆分并使用不同材料 【修改】>【几何图形】>【拆分面】(SF), 然后再使用【修改】>【几何图形】>【填色】(PT)进行填充 楼板漏在墙外 绘制楼板时最好选择墙体绘制,如果标高上不显示墙体,可以先选…...

深度学习01-概述
深度学习是机器学习的一个子集。机器学习是实现人工智能的一种途径,而深度学习则是通过多层神经网络模拟人类大脑的方式进行学习和知识提取。 深度学习的关键特点: 1. 自动提取特征:与传统的机器学习方法不同,深度学习不需要手动…...

leetcode232. 用栈实现队列
leetcode232. 用栈实现队列 请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty): 实现 MyQueue 类: void push(int x) 将元素 x 推到队列的末尾 int pop() 从队列的开头移除并返回元…...

智慧火灾应急救援航拍检测数据集(无人机视角)
智慧火灾应急救援。 无人机,直升机等航拍视角下火灾应急救援检测数据集,数据分别标注了火,人,车辆这三个要素内容,29810张高清航拍影像,共31GB,适合森林防火,应急救援等方向的学术研…...

eureka.client.service-url.defaultZone的坑
错误的配置 eureka: client: service-url: default-zone: http://192.168.100.10:8080/eureka正确的配置 eureka: client: service-url: defaultZone: http://192.168.100.10:8080/eureka根据错误日志堆栈打断电调试 出现两个key,也就是defaultZone不支持snake-c…...

统信服务器操作系统【d版字符系统升级到dde图形化】配置方法
统信服务器操作系统d版本上由字符系统升级到 dde 桌面系统的过程 文章目录 一、准备环境二、功能描述安装步骤1. lightdm 安装2. dde 安装 一、准备环境 适用版本:■UOS服务器操作系统d版 适用架构:■ARM64、AMD64、MIPS64 网络:连接互联网…...

学习IEC 62055付费系统标准
1.IEC 62055 国际标准 IEC 62055 是目前关于付费系统的唯一国际标准,涵盖了付费系统、CIS 用户信息系统、售电系统、传输介质、数据传输标准、预付费电能表以及接口标准等内容。 IEC 62055-21 标准化架构IEC 62055-31 1 级和 2 级有功预付费电能表IEC 62055-41 STS…...

如何在Markdown写文章上传到wordpress保证图片不丢失
如何在Markdown写文章上传到wordpress保证图片不丢失 写文日期,2023-11-16 引文 众所周知markdown是一款nb的笔记软件,本篇文章讲解如何在markdown编写文件后上传至wordpress论坛。并且保证图片不丢失(将图片上传至云端而非本地方法) 一&…...

html,css基础知识点笔记(二)
9.18(二) 本文主要教列表的样式设计 1)文本溢出 效果图 文字限制一行显示几个字,多余打点 line-height: 1.8em; white-space: nowrap; width: 40em; overflow: hidden; text-overflow: ellipsis;em表示一个文字的大小单位&…...
(k8s)kubernetes 部署Promehteus学习之路
整个Prometheus生态包含多个组件,除了Prometheus server组件其余都是可选的 Prometheus Server:主要的核心组件,用来收集和存储时间序列数据。 Client Library::客户端库,为需要监控的服务生成相应的 metrics 并暴露给…...

智慧医疗能源事业线深度画像分析(上)
引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...

跨链模式:多链互操作架构与性能扩展方案
跨链模式:多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈:模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展(H2Cross架构): 适配层…...

【网络安全】开源系统getshell漏洞挖掘
审计过程: 在入口文件admin/index.php中: 用户可以通过m,c,a等参数控制加载的文件和方法,在app/system/entrance.php中存在重点代码: 当M_TYPE system并且M_MODULE include时,会设置常量PATH_OWN_FILE为PATH_APP.M_T…...

【 java 虚拟机知识 第一篇 】
目录 1.内存模型 1.1.JVM内存模型的介绍 1.2.堆和栈的区别 1.3.栈的存储细节 1.4.堆的部分 1.5.程序计数器的作用 1.6.方法区的内容 1.7.字符串池 1.8.引用类型 1.9.内存泄漏与内存溢出 1.10.会出现内存溢出的结构 1.内存模型 1.1.JVM内存模型的介绍 内存模型主要分…...
作为测试我们应该关注redis哪些方面
1、功能测试 数据结构操作:验证字符串、列表、哈希、集合和有序的基本操作是否正确 持久化:测试aof和aof持久化机制,确保数据在开启后正确恢复。 事务:检查事务的原子性和回滚机制。 发布订阅:确保消息正确传递。 2、性…...
LOOI机器人的技术实现解析:从手势识别到边缘检测
LOOI机器人作为一款创新的AI硬件产品,通过将智能手机转变为具有情感交互能力的桌面机器人,展示了前沿AI技术与传统硬件设计的完美结合。作为AI与玩具领域的专家,我将全面解析LOOI的技术实现架构,特别是其手势识别、物体识别和环境…...
Kafka主题运维全指南:从基础配置到故障处理
#作者:张桐瑞 文章目录 主题日常管理1. 修改主题分区。2. 修改主题级别参数。3. 变更副本数。4. 修改主题限速。5.主题分区迁移。6. 常见主题错误处理常见错误1:主题删除失败。常见错误2:__consumer_offsets占用太多的磁盘。 主题日常管理 …...
前端中slice和splic的区别
1. slice slice 用于从数组中提取一部分元素,返回一个新的数组。 特点: 不修改原数组:slice 不会改变原数组,而是返回一个新的数组。提取数组的部分:slice 会根据指定的开始索引和结束索引提取数组的一部分。不包含…...

Spring AOP代理对象生成原理
代理对象生成的关键类是【AnnotationAwareAspectJAutoProxyCreator】,这个类继承了【BeanPostProcessor】是一个后置处理器 在bean对象生命周期中初始化时执行【org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization】方法时…...