【Golang】泛型与类型约束
文章目录
- 一、环境
- 二、没有泛型的Go
- 三、泛型的优点
- 四、理解泛型
- (一)定义
- (二)调用
- (三)类型约束(Type Constraint)
- 1)接口与约束
- 2)结构体类型约束
- 3)类型近似(Type Approximations)
- 4)泛型与结构体
- (四)一些错误示例
- 1)联合约束中的类型元素限制
- 2)不含方法的接口只能用于类型参数
- 五、参阅
一、环境
Go 1.20.2
二、没有泛型的Go
假设现在我们需要写一个函数,实现:
1)输入一个切片参数,切片类型可以是[]int或[]float64,然后将所有元素相加的“和”返回
2)如果是int切片,返回int类型;如果是float64切片,返回float64类型
当然,最简单的方法是写两个函数SumSliceInt(s []int)、SumSliceFloat64(s []float64)来分别支持不同类型的切片,但是这样会导致大部分代码重复冗余,不是很优雅。那么有没有办法只写一个函数呢?
我们知道,在Go中所有的类型都实现了interface{}接口,所以如果想让一个变量支持多种数据类型,我们可以将这个变量声明为interface{}类型,例如var slice interface{},然后使用类型断言(.(type))来判断这个变量的类型。
interface{} + 类型断言:
// any是inerface{}的别名,两者是完全相同的:type any = interface{}
func SumSlice(slice any) (any, error) {switch s := slice.(type) {case []int:sum := 0for _, v := range s {sum += v}return sum, nilcase []float64:sum := float64(0)for _, v := range s {sum += v}return sum, nildefault:return nil, fmt.Errorf("unsupported slice type: %T", slice)}
}
从上述代码可见,虽然使用interface{}类型可以实现在同一个函数内支持两种不同切片类型,但是每个case块内的代码仍然是高度相似和重复的,代码冗余的问题没有得到根本的解决。
三、泛型的优点
幸运的是,在Go 1.18之后开始支持了泛型(Generics),我们可以使用泛型来解决这个问题:
func SumSlice[T interface{ int | float64 }](slice []T) T {var sum T = 0for _, v := range slice {sum += v}return sum
}
是不是简洁了很多?而且,泛型相比interface{}还有以下优势:
- 可复用性:提高了代码的可复用性,减少代码冗余。
- 类型安全性:泛型在编译时就会进行类型安全检查,可以确保编译出来的代码就是类型安全的;而
interface{}是在运行时才进行类型判断,如果编写的代码在类型判断上有bug或缺漏,就会导致Go在运行过程中报错。 - 性能:不同类型的数据在赋值给
interface{}变量时,会有一个隐式的装箱操作,从interface{}取数据时也会有一个隐式的拆箱操作,而泛型就不存在装箱拆箱过程,没有额外的性能开销。
四、理解泛型
(一)定义
编写一个函数,输入a、b两个泛型参数,返回它们的和:
// T的名字可以更改,改成K、V、MM之类的都可以,只是一般比较常用的是T
// 这是一个不完整的错误例子
func Sum(a, b T) T {return a + b
}
大写字母T的名字叫类型参数(Type parameter),代表a、b参数是泛型,可以接受多种类型,但具体可以接受哪些类型呢?在上面的定义中并没有给出这部分信息,要知道,并不是所有的类型都可以相加的,因此这里就引出了约束的概念,我们需要对T可以接受的类型范围作出约束:
// 正确例子
func Sum[T interface{ int | float64 }](a, b T) T {return a + b
}
中括号[]之间的空间用于定义类型参数,支持定义一个或多个
T:类型参数的名字interface{ int | float64 }:对T的类型约束(Type Constraint),必须是一个接口,约束T只可以是int或float64
为了简化写法,类型约束中的interface{}在某些情况下是可以省略的,所以可以简写成:
func Sum[T int | float64](a, b T) T {return a + b
}
interface{}不能省略的一些情况:
// 当接口中包含方法时,不能省略
func Contains[T interface{ Equal() bool }](num T) {
}
可以定义多个类型参数:
func Add[T int, E float64](a T, b E) E {return E(a) + b
}
(二)调用
以上面的Sum泛型函数为例,完整的调用写法为:
Sum[int](1, 2)
Sum[float64](1.1, 2.2)
[]之间的内容称为类型实参(Type argument),是告诉编译器传过去的函数实参具体是什么类型。但大多数时候,编译器都可以自动推导出该类型,无需我们主动告知,这个功能叫函数实参类型推导(Function argument type inference)。所以可以简写成:
// 简写,跟调用普通函数一样的写法
Sum(1, 2)
Sum(1.1, 2.2)
需要注意的是,在调用这个函数时,a、b两个参数的类型必须一致,要么两个都是int,要么都是float64,不能一个是int一个是float64:
Sum(1, 2.3) // 编译会报错
什么时候不能简写?
// 当类型参数T仅用在返回值,没有用在函数参数列表时
func Foo[T int | float64]() T {return 1
}
Foo() // 报错:cannot infer T
Foo[int]() // OK
Foo[float64]() // OK
(三)类型约束(Type Constraint)
1)接口与约束
Go 使用interface定义类型约束。我们知道,在引入泛型之前,interface中只可以声明一组未实现的方法,或者内嵌其它interface,例如:
// 普通接口
type Driver interface {SetName(name string) (int, error)GetName() string
}// 内嵌接口
type ReaderStringer interface {io.Readerfmt.Stringer
}
接口里的所有方法称之为方法集(Method set)。
引入泛型之后,interface里面可以声明的元素丰富了很多,可以是任何 Go 类型,包括基本类型、接口、方法等等,甚至struct结构体都可以,接口里的这些元素称为类型集(Type set):
// 基本类型约束
type MyInt interface {int
}// 结构体类型约束
type Point interface {struct{ X, Y int }
}// 内嵌其它约束
type MyNumber interface {MyInt
}// 联合(Unions)类型约束,不同类型元素之间是“或”的关系
// 如果元素是一个接口,这个接口不能包含任何方法!
type MyFloat interface {float32 | float64
}
有了丰富的类型集支持,我们就可以更加方便的使用接口对类型参数T的类型作出约束,既可以约束为基本类型(int、float32、string…),也可以约束它必须实现一组方法,灵活性大大增加。
因此前面的Sum函数还可以改写成:
// 原始例子:
// func Sum[T int | float64](a, b T) T {
// return a + b
// }type MyNumber interface {int | float64
}func Sum[T MyNumber](a, b T) T {return a + b
}
2)结构体类型约束
Go 还允许我们使用复合类型字面量来定义约束。例如,我们可以定义一个约束,类型元素是一个具有特定结构的struct:
type Point interface {struct{ X, Y int }
}
然而,需要注意的是,虽然我们可以编写受此类结构体类型约束的泛型函数,但在当前版本的 Go 中,函数无法访问结构体的字段,例如:
func GetX[T Point](p T) int {return p.X // p.X undefined (type T has no field or method X)
}
3)类型近似(Type Approximations)
我们知道,在Go中可以创建新的类型,例如:
type MyString string
MyString是一个新的类型,底层类型是string。
在类型约束中,有时候我们可能并不关心上层类型,只要底层类型符合要求就可以,这时候就可以使用类型近似符号:~。
// 创建新类型
type MyString string// 定义类型约束
type AnyStr interface {~string
}// 定义泛型函数
func Foo[T AnyStr](param T) T {return param
}func main() {var p1 string = "aaa"var p2 MyString = "bbb"Foo(p1)Foo(p2) // 虽然p2是MyString类型,但也可以通过泛型函数的类型约束检查
}
需要注意的是,类型近似中的类型,必须是底层类型,而且不能是接口类型:
type MyInt inttype I0 interface {~MyInt // 错误! MyInt不是底层类型, int才是~error // 错误! error是接口
}
4)泛型与结构体
前面都是以泛型函数为讲述例子,但其实泛型还可以用在struct结构体上。
假设我们现在要创建一个struct结构体,里面含有一个data泛型属性,类型是一个int或float64的切片:
type List[T int | float64] struct {data []T
}
给这个结构体增加一个Sum方法,用于对切片求和:
func (l *List[T]) Sum() T {var sum Tfor _, v := range l.data {sum += v}return sum
}
实例化结构体,并调用Sum方法:
list := &List[int]{data: []int{1, 2, 3}}
sum := list.Sum()
fmt.Println(sum) // 输出:6
(四)一些错误示例
下面列出一些错误使用泛型的例子。
1)联合约束中的类型元素限制
联合约束中的类型元素不能是包含方法的接口:
// 错误
type ReaderStringer interface {io.Reader | fmt.Stringer // 错误,io.Reader和fmt.Stringer是包含方法的接口
}// 正确
type MyInt interface {int
}
type MyFloat interface {float32
}
type MyNumber interface {MyInt | MyFloat // 正确,MyInt和MyFloat接口里面没有包含方法
}
联合约束中的类型元素不能含有comparable接口:
type Number interface {comparable | int // 含有comparable,报错
}
2)不含方法的接口只能用于类型参数
不含方法的接口只能用于类型参数,不能用于变量、函数参数、返回值的类型声明:
type NoMethods interface {int
}// 错误
func Foo(param NoMethods) NoMethods {return param
}// 错误
var param NoMethods// 正确
func Foo[T NoMethods](param T) T {return param
}
五、参阅
- Golang泛型
- An Introduction To Generics
相关文章:
【Golang】泛型与类型约束
文章目录 一、环境二、没有泛型的Go三、泛型的优点四、理解泛型(一)定义(二)调用(三)类型约束(Type Constraint)1)接口与约束2)结构体类型约束3)类…...
Uni-app页面信息与元素影响解析
获取窗口信息uni.getWindowInfo {pixelRatio: 3safeArea:{bottom: 778height: 731left: 0right: 375top: 47width: 375}safeAreaInsets: {top: 47, left: 0, right: 0, bottom: 34},screenHeight: 812,screenTop: 0,screenWidth: 375,statusBarHeight: 47,windowBottom: 0,win…...
CentOS(最小化)安装之后,快速搭建Docker环境
本文以VMware虚拟机中安装最小化centos完成后开始。 1. 检查网络 打开网卡/启用网卡 执行命令ip a查看当前的网络连接是否正常: 如果得到的结果和我一样,有ens网卡但是没有ip地址,说明网卡未打开 手动启用: nmcli device sta…...
【身份证证件OCR识别】批量OCR识别身份证照片复印件图片里的文字信息保存表格或改名字,基于QT和腾讯云api_ocr的实现方式
项目背景 在许多业务场景中,需要处理大量身份证照片复印件,手动输入其中的文字信息效率低下且容易出错。利用 OCR(光学字符识别)技术可以自动识别身份证图片中的文字信息,结合 QT 构建图形用户界面,方便用户操作,同时使用腾讯 OCR API 能够保证较高的识别准确率。 界面…...
IP属地和发作品的地址不一样吗
在当今这个数字化时代,互联网已经成为人们日常生活不可或缺的一部分。随着各大社交平台功能的不断完善,一个新功能——IP属地显示,逐渐走进大众视野。这一功能在微博、抖音、快手等各大平台上得到广泛应用,旨在帮助公众识别虚假信…...
Redis - 概述
目录 编辑 一、什么是redis 二、redis能做什么(有什么特点)? 三、redis有什么优势 四、Redis与其他key-value存储有什么不同 五、Redis命令 六、Redis数据结构 1、基础数据结构 2、高级数据结构 一、什么是redis 1、redis&#x…...
vue3 根据城市名称计算城市之间的距离
<template><div class"distance-calculator"><h1>城市距离计算器</h1><!-- 城市输入框 --><div class"input-group"><inputv-model"city1"placeholder"请输入第一个城市"keyup.enter"cal…...
html 列表循环滚动,动态初始化字段数据
html <div class"layui-row"><div class"layui-col-md4"><div class"boxall"><div class"alltitle">超时菜品排行</div><div class"marquee-container"><div class"scroll-…...
QT基础:安装与简介
QT初级 1、简介1.1 安装1.2 设置1.3 在VS中配置Qt1.3 帮助文档 2、Qt项目2.1 创建项目2.1 项目文件2.2 Qt中的窗口类窗口显示 2.3 坐标体系2.4 内存回收 1、简介 QT是一个跨平台的C应用程序开发框架。几乎支持所有的平台, 可用于桌面程序开发以及嵌入式开发。 Qt是标准 C 的扩…...
41、当你在 index.html 中引用了一个公共文件(比如 common.js),修改这个文件后,用户访问页面时仍然看到旧内容,因为浏览器缓存了旧版本
由于浏览器缓存导致公共文件无法更新。当用户修改了公共文件(如 JavaScript 或 CSS),但 index.html 中引用的文件名没有变化,浏览器会认为文件没有更新,继续使用缓存的旧版本。因此,需要通过某种方式让浏览…...
WEB安全-HTTPS
1 需求 结合Wireshark抓包实战,图文详解TCP三次握手及四次挥手原理(附下载) 结合Wireshark抓包分析,沉浸式体验HTTP请求的一次完整交互过程 https://mp.weixin.qq.com/s/f3LmUEtjIuLjkyjxJj7ebA 一文彻底了解DNS协议工作原理&…...
【宇宙回响】从Canvas到MySQL:飞机大战的全栈交响曲【附演示视频与源码】
🌟 这是星际大战系列的第三篇,感谢一路以来支持和关注这个项目的每一位朋友! 💡 文章力求严谨,但难免有疏漏之处,欢迎各位朋友指出,让我们一起在交流中进步。 🎁 项目代码、文档和相关资源都可以免费获取,希望能帮助到更多对游戏开发感兴趣的朋友。 💌 如果您有任…...
Linux内核内存管理 ARM32页表映射流程和案例分享
ARM32架构使用两级页表结构将虚拟地址转换为物理地址,以下为详细流程及案例分析: ARM32页表映射流程 1.获取页目录基地址 MMU通过TTBR(Translation Table Base Register)寄存器获取当前进程的一级页表(L1页表&#x…...
git push origin masterremote: [session-bd46a49f] The token username invalid
参考:如何把项目上传到Gitee(保姆级教程)_gitee上传项目-CSDN博客 1 新建仓库 username可以是登录账号的邮箱地址也可以是用户名 password可以是登录账号的密码也可以是私人令牌 2 创建分支 3 初始化 dev是你新建的分支 创建并切换分支 git init g…...
基于MCU实现的电机转速精确控制方案:软件设计与实现
本文将详细介绍一篇基于微控制器(MCU)的电机转速精确控制的软件方案。通过采样PWM信号控制和ADC采样技术,结合PID闭环控制算法,实现了电机转速的高效、稳定调节。以下是软件方案流程图,下文将对其进行展开讲解。 原图太…...
suse15 sp1使用华为云软件源yum源zypper源
登录suse15终端, cd /etc/zypp/repos.d/进入目录后执行以下命令: zypper ar -fcg https://mirrors.huaweicloud.com/opensuse/distribution/leap/15.1/repo/oss HuaWeiCloud:15.1:OSS zypper ar -fcg https://mirrors.huaweicloud.com/opensuse/distribu…...
amd64 架构机器如何拉取arm64的镜像
在 AMD 架构(通常是 x86_64 架构)的机器上拉取 ARM 架构的镜像 拉取指定架构的镜像 例如,要拉取 ARM64 架构的 nginx 镜像,可以使用以下命令: docker pull --platform linux/arm64 nginx...
【模拟CMOS集成电路笔记】轨到轨运放(Rail to Rail)基础(附带实例:基于1:3电流镜的轨到轨输入运放)
【模拟CMOS集成电路笔记】轨到轨运放(Rail to Rail)基础 0前言1 简介1.1轨到轨输入级(1)互补差分对:(2)输入范围切换: 1.2轨到轨输出级(1)推挽输出:(1)输出级偏置: 2轨到轨输入运放2.1基于电流倍增实现恒定…...
【零基础入门unity游戏开发——通用篇】图片相关设置
考虑到每个人基础可能不一样,且并不是所有人都有同时做2D、3D开发的需求,所以我把 【零基础入门unity游戏开发】 分为成了C#篇、unity通用篇、unity3D篇、unity2D篇。 【C#篇】:主要讲解C#的基础语法,包括变量、数据类型、运算符、流程控制、面向对象等,适合没有编程基础的…...
解决关于原生gmssl无法直接输出sm2私钥明文的问题
解决关于原生gmssl无法直接输出sm2私钥明文的问题 问题描述解决方法解决方法一解决方法二 问题描述 通过gmssl生成sm2公私钥对时,输出的是加密的sm2私钥,无法获取到SM2私钥明文。 解决方法 解决方法一 手动解密: 解决方法二 修改源码&…...
齐次线性方程组及python求解
齐次线性方程组的概念 齐次线性方程组是指所有常数项都为零的线性方程组,其一般形式为: a 11 x 1 a 12 x 2 ⋯ a 1 n x n 0 a_{11}x_1 a_{12}x_2 \cdots a_{1n}x_n 0 a11x1a12x2⋯a1nxn0 a 21 x 1 a 22 x 2 ⋯ a 2 n x n 0 a_…...
基于 Qt / HTTP/JSON 的智能天气预报系统测试报告
目录 一、项目概述 1.1项目背景 1.2项目目标 二、功能需求 2.1 用户界面功能 2.2 后台功能 三、技术选择 3.1 开发框架与工具 3.2 第三方 API 四、UI设计 4.1界面展示 4.2stylesheet样式 五、代码实现 1.构造函数 2.网络请求响应处理函数 3.处理json数据 4.更新…...
基于Real-Sim-Real循环框架的机器人策略迁移方法
编辑:陈萍萍的公主一点人工一点智能 基于Real-Sim-Real循环框架的机器人策略迁移方法本文通过严谨的理论推导和系统的实验验证,构建了一个具有普适性的sim-to-real迁移框架。https://mp.weixin.qq.com/s/cRRI2VYHYQUUhHhP3bw4lA 01 摘要 本文提出的Rea…...
Spring Boot 集成实战:AI 工具如何自动生成完整微服务模块
在数字化转型的浪潮中,开发效率和质量是企业竞争力的关键要素。飞算 JavaAI 作为一款创新的 AI 工具,能在 Spring Boot 开发中,自动生成完整微服务模块,极大提升开发效率。下面,我们就详细介绍如何借助飞算 JavaAI&…...
Vue React
Vue 的源码主要分为以下几个部分: 主要涉及 响应式、虚拟 DOM、组件系统、编译器、运行时。 ├── packages/ │ ├── compiler-core/ # 编译器核心 │ ├── compiler-sfc/ # 处理 .vue 单文件组件 │ ├── compiler-dom/ # 处理 DOM 相关…...
Java高频面试之并发编程-01
hello啊,各位观众姥爷们!!!本baby今天来报道了!哈哈哈哈哈嗝🐶 面试官:并行跟并发有什么区别? 并发 vs 并行:核心区别与场景 1. 定义对比 维度并发(Concu…...
(Kotlin)Android 高效底部导航方案:基于预定义 Menu 和 ViewPager2 的 Fragment 动态绑定实现
支持预定义 Menu 并绑定 Fragment,同时保留动态添加 Tab 的能力 BottomTabHelper.kt package smartconnection.com.smartconnect.home.utilimport android.content.Context import android.util.SparseArray import androidx.annotation.IdRes import androidx.fra…...
2024年零知识证明(ZK)研究进展
Sumcheck 整个领域正在转向更多地依赖于 Sumcheck Protocol Sumcheck是用于验证多项式承诺的协议,常用于零知识证明(ZKP)中,尤其是在可验证计算和扩展性上。它的主要目的是通过对多项式进行分段检查,从而保证某个多项…...
AI 驱动的安全分析的价值是什么?
作者:来自 Elastic Riya Juneja, Alyssa VanNice 与 Enterprise Strategy Group 一起量化经济影响 安全行业十分复杂,变化速度极快。攻击面、利益相关者需求、对手战术以及你使用的工具都在不断演变,导致许多安全团队不确定自己是否已做好准备…...
目标检测YOLO实战应用案例100讲-基于孤立森林算法的高光谱遥感图像异常目标检测(续)
目录 3.4 实验结果与分析 3.4.1 数据集介绍 3.4.2 实验参数分析 3.4.3 实验结果评价与讨论 基于高维孤立森林算法的高光谱图像异常检测 4.1 引言 4.2 基于高维孤立森林算法的异常检测模型 4.2.1 面向高维数据的改进策略 4.2.2 基于光谱有效信息率和目标-背景分离…...
