【Golang】泛型与类型约束

文章目录

  • 一、环境
  • 二、没有泛型的Go
  • 三、泛型的优点
  • 四、理解泛型
    • (一)泛型函数(Generic function)
      • 1)定义
      • 2)调用
    • (二)类型约束(Type constraint)
      • 1)接口与约束
      • 2)结构体类型约束
      • 3)类型近似(Type approximations)
    • (三)泛型类型(Generic type)
      • 1)泛型切片
      • 2)泛型结构体
      • 3)泛型接口
    • (五)一些错误示例
      • 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{}取数据时也会有一个隐式的拆箱操作,而泛型就不存在装箱拆箱过程,没有额外的性能开销。

四、理解泛型

(一)泛型函数(Generic function)

1)定义

编写一个函数,输入ab两个泛型参数,返回它们的和:

// T的名字可以更改,改成K、V、MM之类的都可以,只是一般比较常用的是T
// 这是一个不完整的错误例子
func Sum(a, b T) T {return a + b
}

大写字母T的名字叫类型形参(Type parameter),代表ab参数是泛型,可以接受多种类型,但具体可以接受哪些类型呢?在上面的定义中并没有给出这部分信息,要知道,并不是所有的类型都可以相加的,因此这里就引出了约束的概念,我们需要对T可以接受的类型范围作出约束:

// 正确例子
func Sum[T interface{ int | float64 }](a, b T) T {return a + b
}

中括号[]之间的空间用于定义类型形参,支持定义一个或多个

  • T:类型形参的名字
  • interface{ int | float64 }:对T的类型约束(Type Constraint),必须是一个接口,约束T只可以是intfloat64

为了简化写法,类型约束中的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
}

2)调用

以上面的Sum泛型函数为例,完整的调用写法为:

Sum[int](1, 2)
Sum[float64](1.1, 2.2)

[]之间的内容称为类型实参(Type argument),是函数定义中的类型形参T的实际值,例如传int过去,那么T的实际值就是int

类型形参确定为具体类型的过程称为实例化(Instantiations),可以简单理解为将函数定义中的T替换为具体类型:
在这里插入图片描述
泛型函数实例化后,就可以像普通函数那样调用了。

大多数时候,编译器都可以自动推导出该具体类型,无需我们主动告知,这个功能叫函数实参类型推导(Function argument type inference)。所以可以简写成:

// 简写,跟调用普通函数一样的写法
Sum(1, 2)
Sum(1.1, 2.2)

需要注意的是,在调用这个函数时,ab两个参数的类型必须一致,要么两个都是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的类型作出约束,既可以约束为基本类型(intfloat32string…),也可以约束它必须实现一组方法,灵活性大大增加。

因此前面的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是接口
}

(三)泛型类型(Generic type)

1)泛型切片

假设现在有一个IntSlice类型:

type IntSlice []intvar s1 IntSlice = []int{1, 2, 3} // 正常
var s2 IntSlice = []string{"a", "b", "c"} // 报错,因为IntSlice底层类型是[]int,字符串无法赋值

很显然,因为类型不一致,s2是无法赋值的,如果想要支持其它类型,需要定义新类型:

type StringSlice []string
type Float32Slice []float32
type Float64Slice []float64
// ...

但是这样做的问题也显而易见,它们结构都是一样的,只是元素类型不同就需要重新定义这么多新类型,导致代码复杂度增加。

这时候就可以用泛型类型来解决这个问题:

// 只需定义一种新类型,就可以同时支持[]int/[]string/[]float32多种切片类型
// 新类型的名字叫 MySlice[T]
type MySlice[T int|string|float32] []T

类型定义中带 类型形参 的类型,称之为泛型类型(Generic type)

泛型切片的初始化:

var s1 MySlice[int] = MySlice[int]{1, 2, 3}
var s2 MySlice[string] = MySlice[string]{"a", "b", "c"}
s3 := MySlice[string]{"a", "b", "c"} // 简写

其它一些例子:

// 泛型Map
type MyMap[K int | string, V any] map[K]Vvar m1 MyMap[string, int] = MyMap[string, int]{"a": 1, "b": 2} // 完整写法
m2 := MyMap[int, string]{1: "a", 2: "b"} // 简写// 泛型通道
type MyChan[T int | float32] chan Tvar c1 MyChan[int] = make(MyChan[int]) // 完整写法
c2 := make(MyChan[float32]) // 简写

2)泛型结构体

假设现在要创建一个struct结构体,里面含有一个data泛型属性,类型是一个intfloat64的切片:

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方法:

// var list *List[int] = &List[int]{data: []int{1, 2, 3}} // 完整写法
list := &List[int]{data: []int{1, 2, 3}}
sum := list.Sum()
fmt.Println(sum) // 输出:6

3)泛型接口

泛型也可以用在接口上:

type Human[T float32] interface {GetWeight() T
}

假设现在有两个结构体,它们都有GetWeight()方法,哪个结构体实现了上面Human[T]接口?

// 结构体1
type Person1 struct {Name string
}
func (p Person1) GetWeight() float32 {return 66.6
}// 结构体2
type Person2 struct {Name string
}
func (p Person2) GetWeight() int {return 66
}

注意观察两个GetWeight()方法的返回值类型,因为我们在Human[T]接口中约束了T的类型只能是float32,而只有Person1结构体的返回值类型符合约束,所以实际上只有Person1结构体实现了Human[T]接口。

p1 := Person1{Name: "Tim"}
var iface1 Human[float32] = p1 // 正常,因为Person1实现了接口,所以可以赋值成功p2 := Person2{Name: "Tim"}
var iface2 Human[float32] = p2 // 报错,因为Person2没有实现接口

(五)一些错误示例

下面列出一些错误使用泛型的例子。

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)一般接口只能用于泛型的类型约束

先解释下相关概念,引入泛型后,Go的接口分为两种类型:

  • 基本接口(Basic interface)
    只包含方法的接口,称为基本接口,其实就是引入泛型之前的那种传统接口。
  • 一般接口(General interface)
    由于引入泛型后,接口可以定义的元素大大丰富,如果一个接口里含有除了方法以外的元素,那么这个接口就称为一般接口

一般接口只能用于泛型的类型约束,不能用于变量、函数参数、返回值的类型声明,而基本接口则没有此限制:

type NoMethods interface {int
}// 错误,不能用于函数参数列表、返回值
func Foo(param NoMethods) NoMethods {return param
}// 错误,不能用来声明变量的类型
var param NoMethods// 正确
func Foo[T NoMethods](param T) T {return param
}

五、参阅

  • Go泛型全面讲解:一篇讲清泛型的全部
  • Golang泛型
  • An Introduction To Generics

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/74319.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

k8s常用总结

1. Kubernetes 架构概览 主节点(Master): 负责集群管理,包括 API Server、Controller Manager、Scheduler 和 etcd 存储。 工作节点(Node): 运行 Pod 和容器,包含 kubelet、kube-pr…

Android 单例模式全解析:从基础实现到最佳实践

单例模式(Singleton Pattern)是软件开发中常用的设计模式,其核心是确保一个类在全局范围内只有一个实例,并提供全局访问点。在 Android 开发中,单例模式常用于管理全局资源(如网络管理器、数据库助手、配置…

ffmpeg滤镜使用

ffmpeg实现画中画效果 FFmpeg中,可以通过overlay将多个视频流、多个多媒体采集设备、多个视频文件合并到一个界面中,生成画中画的效果 FFmpeg 滤镜 overlay 基本参数 x和y x坐标和Y坐标 eof action 遇到 eof表示时的处理方式,默认为重复。…

OpenAI即将开源!DeepSeek“逼宫”下,AI争夺战将走向何方?

OpenAI 终于要 Open 了。 北京时间 4 月 1 日凌晨,OpenAI 正式宣布:将在未来几个月内开源一款具备推理能力的语言模型,并开放训练权重参数。这是自 2019 年 GPT-2 部分开源以来,OpenAI 首次向公众开放核心模型技术。 【图片来源于…

贪心算法,其优缺点是什么?

什么是贪心算法? 贪心算法(Greedy Algorithm)是一种在每一步选择中都采取在当前状态下最优(局部最优)的选择,从而希望导致全局最优解的算法策略。 它不像动态规划那样考虑所有可能的子问题,而是做出局部最优选择,依赖这些选择来…

python string 类型字符拼接 +=的缺点,以及取代方法

在Python中,使用进行字符串拼接虽然语法简单,但在性能和代码维护方面存在明显缺陷。以下是详细分析及替代方案: 一、的缺点 性能低下 内存分配问题:字符串在Python中不可变,每次操作会创建新字符串对象,导…

web前端开发-JS

web前端开发-JS 什么是JavaScript Web标准也称网页标准,由一系列的标准组成,大部分由W3C(World Wide Web Consortium,万维网联盟)负责制定。三个组成部分: HTML:负责网页的结构(页面元素和内容)。CSS:负责网页的表现(页面元素的外观、位置等页面样式,如:颜色、大小等)。JavaS…

Turtle综合案例实战(绘制复杂图形、小游戏)

在学习了 Turtle 基本的绘图技巧后,我们可以通过结合多个概念和技巧,绘制复杂的图形或实现简单的小游戏。本章将介绍两个实战案例: 绘制复杂图形:结合前面所学的知识,绘制一个精美的多层次复杂图案。简单的游戏:利用 Turtle 实现一个简单的小游戏——蛇形游戏,这是一个经…

Python设计模式:克隆模式

1. 什么是克隆模式 克隆模式的核心思想是通过复制一个已有的对象(原型)来创建一个新的对象(克隆)。这种方式可以避免重复的初始化过程,从而提高效率。克隆模式通常涉及以下几个方面: 原型对象&#xff1a…

逻辑漏洞之越权访问总结

什么是越权访问漏洞? “越权访问漏洞” 是 “逻辑漏洞” 的一种,是由于网站系统的权限校验的逻辑不够严谨,没有对用户权限进行严格的身份鉴别,导致普通权限的用户做到了其它普通用户或管理员才能完成的操作,称之为“越…

超短波通信模拟设备:增强通信能力的关键工具

在全球信息化战争的背景下,通信系统扮演着至关重要的角色。为确保通信系统的稳定性和抗干扰能力,超短波通信模拟设备应运而生,为军事训练和通信干扰任务提供强大的支持。 设备特点及优势 便携性:设备体积小、重量轻,…

C++STL——容器-vector(含部分模拟实现,即地层实现原理)(含迭代器失效问题)

目录 容器——vector 1.构造 模拟实现 2.迭代器 模拟实现: ​编辑 3.容量 模拟实现: 4.元素的访问 模拟实现 5.元素的增删查改 迭代器失效问题: 思考问题 【注】:这里的模拟实现所写的参数以及返回值,都是…

Ubuntu交叉编译器工具链安装

声明 本博客所记录的关于正点原子i.MX6ULL开发板的学习笔记,(内容参照正点原子I.MX6U嵌入式linux驱动开发指南,可在正点原子官方获取正点原子Linux开发板 — 正点原子资料下载中心 1.0.0 文档),旨在如实记录我在学校学…

Tomcat 部署 Jenkins.war 详细教程(含常见问题解决)

在Tomcat中部署Jenkins.war文件是一个相对简单的过程,以下是详细步骤: 1. 准备工作 确保已安装JDK:Jenkins需要Java环境,建议安装JDK 8或更高版本。 下载Jenkins.war:https://pan.quark.cn/s/c4fd7711a1b3 下载Tomc…

DAY46 动态规划Ⅸ 股票问题Ⅱ

188. 买卖股票的最佳时机 IV - 力扣&#xff08;LeetCode&#xff09; class Solution { public:int maxProfit(int k, vector<int>& prices) {if(prices.size()0) return 0;vector<vector<int>>dp(prices.size(),vector<int>(2*k1,0));for(int i…

4月2日工作日志

一个朴实无华的目录 今日学习内容&#xff1a;1.UIAbility生命周期2.默认启动页面设置3.同模块唤起ability 今日实操内容&#xff1a; 今日学习内容&#xff1a; 1.UIAbility生命周期 2.默认启动页面设置 3.同模块唤起ability 今日实操内容&#xff1a; 通过分组件文件&#…

鸿蒙学习笔记(4)-Radio组件、弹框组件、组件内部状态、工具类

一、Radio组件 &#xff08;1&#xff09;简述 创建单选框组件。接收一个RadioOptions类型对象参数。 名称类型必填说明valuestring是 当前单选框的值。 groupstring是 当前单选框的所属群组名称&#xff0c;相同group的Radio只能有一个被选中。 indicatorType12RadioIndica…

111.在 Vue 3 中使用 OpenLayers 实现动态曲线流动图(类似 ECharts 迁徙状态)

在数据可视化领域&#xff0c;ECharts 提供的 迁徙图&#xff08;流动图&#xff09; 是一种直观展示数据流动的方式&#xff0c;如人口迁徙、物流流向等。我们可以使用 OpenLayers 结合 Vue 3 来实现类似的 动态曲线流动图&#xff0c;从而在 Web GIS 项目中提供更生动的可视化…

全栈开发项目实战——AI智能聊天机器人

文章目录 一&#xff1a;项目技术栈和代码分析1.前端技术栈&#xff08;1&#xff09;HTML&#xff08;index.html&#xff09;&#xff1a;&#xff08;2&#xff09;CSS&#xff08;styles.css&#xff09;&#xff1a;&#xff08;3&#xff09;JavaScript&#xff08;scrip…

无人机机体结构设计要点与难点!

一、无人机机体结构设计要点 1. 类型与应用场景匹配 固定翼无人机&#xff1a;需优化机翼升阻比&#xff0c;采用流线型机身降低气动阻力&#xff08;如大展弦比机翼设计&#xff09;。 多旋翼无人机&#xff1a;注重轻量化框架和对称布局&#xff08;如四轴/六轴碳纤维机…