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 并暴露给…...

初写MySQL四张表:(3/4)
我们已经完成了四张表的创建,学会了创建表和查看表字段信息的语句。 初写MySQL四张表:(1/4)-CSDN博客 初写MySQL四张表:(2/4)-CSDN博客 接下来,我们来学点对数据的操作:增 删 查(一部分)改 先来看这四张表以及相关…...

【Java】线程暂停比拼:wait() 和 sleep()的较量
欢迎浏览高耳机的博客 希望我们彼此都有更好的收获 感谢三连支持! 在Java多线程编程中,合理地控制线程的执行是至关重要的。wait()和sleep()是两个常用的方法,它们都可以用来暂停线程的执行,但它们之间存在着显著的差异。本文将详…...

CQRS模型解析
简介 CQRS中文意思为命令于查询职责分离,我们可以将其了解成读写分离的思想。分为两个部分 业务侧和数据侧,业务侧主要执行的就是数据的写操作,而数据侧主要执行的就是数据的读操作。当然两侧的数据库可以是不同的。目前最为常用的CQRS思想方…...

qt-C++笔记之作用等同的宏和关键字
qt-C笔记之作用等同的宏和关键字 code review! Q_SLOT 和 slots: Q_SLOT是slots的替代宏,用于声明槽函数。 Q_SIGNAL 和 signals: Q_SIGNAL类似于signals,用于声明信号。 Q_EMIT 和 emit: Q_EMIT 是 Qt 中用于发射…...

java(3)数组的定义与使用
目录 1.前言 2.正文 2.1数组的概念 2.2数组的创建与初始化 2.2.1数组的创建 2.2.1数组的静态初始化 2.2.2数组的动态初始化 2.3数组是引用类型 2.3.1引用类型与基本类型区别 2.3.2认识NULL 2.4二维数组 2.5数组的基本运用 2.5.1数组的遍历 2.5.2数组转字符串 2.…...

Integer 源码记录
Integer 公共方法结构 注意: 通过构造函数创建一个Integer对象,每次都会返回一个新的对象,如果使用 进行对象的比较,那么结果是false。 public Integer(int value) {this.value value;}与之对应的是,valueOf 方法…...

【RocketMQ】一、基本概念
文章目录 1、举例2、MQ异步通信3、背景4、Rocket MQ 角色概述4.1 主题4.2 队列4.3 消息4.4 生产者4.5 消费者分组4.6 消费者4.7 订阅关系 5、消息传输模型5.1 点对点模型5.2 发布订阅模型 1、举例 以坐火车类比MQ: 安检大厅就像是一个系统的门面,接受来…...

笔记9.18
线程之间的通信是指在多线程程序中,不同线程之间如何交换数据或协调工作。这种通信对于实现复杂的并发程序是至关重要的。以下是几种常见的线程间通信方式: 共享内存: 这是最直接的方式,多个线程通过读写同一块内存区域࿰…...

时间序列8个基准Baseline模型及其详细解读
我是从去年11月份开始,选定时间序列预测这个方向,准备在工作之余继续独立进行一些科学研究。选定这个方向是因为我对金融量化一直挺感兴趣,希望能把时间序列中的深度学习算法模型,用到金融数据。现在看来,我太过于理想…...

将相机深度图转接为点云的ROS2功能包
depth_image_proc 是一个 ROS(Robot Operating System)包,它包含了一系列节点,用于处理来自深度相机的图像数据,并将其转换为点云。以下是 depth_image_proc 包中各个节点的作用: convert_metric_node&…...