go语言基础之泛型

news2025/1/18 8:51:31

1.泛型

泛型是一种独立于所使用的特定类型的编写代码的方法。使用泛型可以编写出适用于一组类型中的任何一种的函数和类型。

1.1 为什么需要泛型

func reverse(s []int) []int {
	l := len(s)
	r := make([]int, l)

	for i, e := range s {
		r[l-i-1] = e
	}
	return r
}

fmt.Println(reverse([]int{1, 2, 3, 4}))  // [4 3 2 1]

可是这个函数只能接收[]int类型的参数,如果我们想支持[]float64类型的参数,我们就需要再定义一个reverseFloat64Slice函数。

func reverseFloat64Slice(s []float64) []float64 {
	l := len(s)
	r := make([]float64, l)

	for i, e := range s {
		r[l-i-1] = e
	}
	return r
}

如果要想支持[]string类型切片就要定义reverseStringSlice函数,如果想支持[]xxx就需要定义一个reverseXxxSlice.

一遍一遍地编写相同的功能是低效的,实际上这个反转切片的函数并不需要知道切片中元素的类型,但为了适用不同的类型我们把一段代码重复了很多遍。

Go1.18之前我们可以尝试使用反射去解决上述问题,但是使用反射在运行期间获取变量类型会降低代码的执行效率并且失去编译期的类型检查,同时大量的反射代码也会让程序变得晦涩难懂。

从Go1.18开始,使用泛型就能够编写出适用所有元素类型的“普适版”reverse函数。

func reverseWithGenerics[T any](s []T) []T {
	l := len(s)
	r := make([]T, l)

	for i, e := range s {
		r[l-i-1] = e
	}
	return r
}

1.2 泛型语法

泛型为Go语言添加了三个新的重要特性:

  1. 函数和类型的类型参数。
  2. 将接口类型定义为类型集,包括没有方法的类型。
  3. 类型推断,它允许在调用函数时在许多情况下省略类型参数。

类型参数

类型形参和类型实参

函数定义时可以指定形参,函数调用时需要传入实参。

func min(a, b int) int {//a,b两个形参 
if a <= b {
   return a
}
return b
}

min(10, 20)//调用函数min,传入两个实参 10,20

 现在,Go语言中的函数和类型支持添加类型参数。类型参数列表看起来像普通的参数列表,只不过它使用方括号([])而不是圆括号(())。

 借助泛型,我们可以声明一个适用于一组类型min函数。

func min[T int | float64](a, b T) T {
	if a <= b {
		return a
	}
	return b
}
类型实例化

这次定义的min函数就同时支持intfloat64两种类型,也就是说当调用min函数时,我们既可以传入int类型的参数。

m1 := min[int](1, 2)  // 1

也可以传入float64类型的参数。

m2 := min[float64](-0.1, -0.2)  // -0.2

向 min 函数提供类型参数(在本例中为intfloat64)称为实例化( instantiation )。

类型实例化分两步进行:

  1. 首先,编译器在整个泛型函数或类型中将所有类型形参(type parameters)替换为它们各自的类型实参(type arguments)。
  2. 其次,编译器验证每个类型参数是否满足相应的约束。

在成功实例化之后,我们将得到一个非泛型函数,它可以像任何其他函数一样被调用。例如:

fmin := min[float64] // 类型实例化,编译器生成T=float64的min函数
m2 = fmin(1.2, 2.3)  // 1.2

min[float64]得到的是类似我们之前定义的minFloat64函数——fmin,我们可以在函数调用中使用它。

类型参数的使用

除了函数中支持使用类型参数列表外,类型也可以使用类型参数列表。

type Slice[T int | string] []T

type Map[K int | string, V float32 | float64] map[K]V

type Tree[T interface{}] struct {
	left, right *Tree[T]
	value       T
}

在上述泛型类型中,TKV都属于类型形参,类型形参后面是类型约束,类型实参需要满足对应的类型约束。

泛型类型可以有方法,例如为上面的Tree实现一个查找元素的Lookup方法。

func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }

要使用泛型类型,必须进行实例化。Tree[string]是使用类型实参string实例化 Tree 的示例。

var stringTree Tree[string]
类型约束

普通函数中的每个参数都有一个类型; 该类型定义一系列值的集合。例如,我们上面定义的非泛型函数minFloat64那样,声明了参数的类型为float64,那么在函数调用时允许传入的实际参数就必须是可以用float64类型表示的浮点数值。

类似于参数列表中每个参数都有对应的参数类型,类型参数列表中每个类型参数都有一个类型约束。类型约束定义了一个类型集——只有在这个类型集中的类型才能用作类型实参。

Go语言中的类型约束是接口类型。

就以上面提到的min函数为例,我们来看一下类型约束常见的两种方式。

类型约束接口可以直接在类型参数列表中使用。

// 类型约束字面量,通常外层interface{}可省略
func min[T interface{ int | float64 }](a, b T) T {
	if a <= b {
		return a
	}
	return b
}

作为类型约束使用的接口类型可以事先定义并支持复用。

// 事先定义好的类型约束类型
type Value interface {
	int | float64
}
func min[T Value](a, b T) T {
	if a <= b {
		return a
	}
	return b
}

在使用类型约束时,如果省略了外层的interface{}会引起歧义,那么就不能省略。例如:

type IntPtrSlice [T *int] []T  // T*int ?

type IntPtrSlice[T *int,] []T  // 只有一个类型约束时可以添加`,`
type IntPtrSlice[T interface{ *int }] []T // 使用interface{}包裹

类型集

Go1.18开始接口类型的定义也发生了改变,由过去的接口类型定义方法集(method set)变成了接口类型定义类型集(type set)。也就是说,接口类型现在可以用作值的类型,也可以用作类型约束。

type set

把接口类型当做类型集相较于方法集有一个优势: 我们可以显式地向集合添加类型,从而以新的方式控制类型集。

Go语言扩展了接口类型的语法,让我们能够向接口中添加类型。例如

type V interface {
	int | string | bool
}

上面的代码就定义了一个包含 int、 string 和 bool 类型的类型集。

type set

从 Go 1.18 开始,一个接口不仅可以嵌入其他接口,还可以嵌入任何类型、类型的联合或共享相同底层类型的无限类型集合。

当用作类型约束时,由接口定义的类型集精确地指定允许作为相应类型参数的类型。

  • |符号

    T1 | T2表示类型约束为T1和T2这两个类型的并集,例如下面的Integer类型表示由SignedUnsigned组成。

    type Integer interface {
    	Signed | Unsigned
    }
    
  • ~符号

    ~T表示所以底层类型是T的类型,例如~string表示所有底层类型是string的类型集合。

    type MyString string  // MyString的底层类型是string
    

    注意:~符号后面只能是基本类型。

接口作为类型集是一种强大的新机制,是使类型约束能够生效的关键。目前,使用新语法表的接口只能用作类型约束。

any接口

空接口在类型参数列表中很常见,在Go 1.18引入了一个新的预声明标识符,作为空接口类型的别名。

// src/builtin/builtin.go

type any = interface{}

由此,我们可以使用如下代码:

func foo[S ~[]E, E any]() {
	// ...
}

 

类型推断

最后一个新的主要语言特征是类型推断。从某些方面来说,这是语言中最复杂的变化,但它很重要,因为它能让人们在编写调用泛型函数的代码时更自然。

函数参数类型推断

对于类型参数,需要传递类型参数,这可能导致代码冗长。回到我们通用的 min函数:

func min[T int | float64](a, b T) T {
	if a <= b {
		return a
	}
	return b
}

类型形参T用于指定ab的类型。我们可以使用显式类型实参调用它:

var a, b, m float64
m = min[float64](a, b) // 显式指定类型实参

在许多情况下,编译器可以从普通参数推断 T 的类型实参。这使得代码更短,同时保持清晰。

var a, b, m float64

m = min(a, b) // 无需指定类型实参

这种从实参的类型推断出函数的类型实参的推断称为函数实参类型推断。函数实参类型推断只适用于函数参数中使用的类型参数,而不适用于仅在函数结果中或仅在函数体中使用的类型参数。例如,它不适用于像 MakeT [ T any ]() T 这样的函数,因为它只使用 T 表示结果。

约束类型推断

Go 语言支持另一种类型推断,即约束类型推断。接下来我们从下面这个缩放整数的例子开始:

// Scale 返回切片中每个元素都乘c的副本切片
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 只是一个给出点坐标的整数列表。这种类型通常会实现一些业务方法,这里假设它有一个String方法。

type Point []int32

func (p Point) String() string {
    b, _ := json.Marshal(p)
    return string(b)
}

由于一个Point其实就是一个整数切片,我们可以使用前面编写的Scale函数:

func ScaleAndPrint(p Point) {
    r := Scale(p, 2)
    fmt.Println(r.String()) // 编译失败
}

不幸的是,这代码会编译失败,输出r.String undefined (type []int32 has no field or method String的错误。

问题是Scale函数返回类型为[]E的值,其中E是参数切片的元素类型。当我们使用Point类型的值调用Scale(其基础类型为[]int32)时,我们返回的是[]int32类型的值,而不是Point类型。这源于泛型代码的编写方式,但这不是我们想要的。

为了解决这个问题,我们必须更改 Scale 函数,以便为切片类型使用类型参数。

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

现在这个Scale函数,不仅支持传入普通整数切片参数,也支持传入Point类型参数。

这里需要思考的是,为什么不传递显式类型参数就可以写入 Scale 调用?也就是说,为什么我们可以写 Scale(p, 2),没有类型参数,而不是必须写 Scale[Point, int32](p, 2) ?

新 Scale 函数有两个类型参数——S 和 E。在不传递任何类型参数的 Scale(p, 2) 调用中,如上所述,函数参数类型推断让编译器推断 S 的类型参数是 Point。但是这个函数也有一个类型参数 E,它是乘法因子 c 的类型。相应的函数参数是2,因为2是一个非类型化的常量,函数参数类型推断不能推断出 E 的正确类型(最好的情况是它可以推断出2的默认类型是 int,而这是错误的,因为Point 的基础类型是[]int32)。相反,编译器推断 E 的类型参数是切片的元素类型的过程称为约束类型推断

约束类型推断从类型参数约束推导类型参数。当一个类型参数具有根据另一个类型参数定义的约束时使用。当其中一个类型参数的类型参数已知时,约束用于推断另一个类型参数的类型参数。

通常的情况是,当一个约束对某种类型使用 ~type 形式时,该类型是使用其他类型参数编写的。我们在 Scale 的例子中看到了这一点。S 是 ~[]E,后面跟着一个用另一个类型参数写的类型[]E。如果我们知道了 S 的类型实参,我们就可以推断出E的类型实参。S 是一个切片类型,而 E是该切片的元素类型。

2.什么时候使用泛型

2.1 泛型使用方式

使用语言定义的容器类型时

当我们编写的是操作 Go 语言定义的特殊容器类型(slice、map和chennel)的函数。如果函数具有包含这些类型的参数,并且函数的代码并不关心元素的类型,那么使用类型参数可能是有用的。

例如:返回任何类型map中所有的key

// MapKeys 返回m中所有key组成的切片
func MapKeys[Key comparable, Val any](m map[Key]Val) []Key {
    s := make([]Key, 0, len(m))
    for k := range m {
        s = append(s, k)
    }
    return s
}

通用数据结构

类型参数另一个适用场景就是用于通用数据结构。通用数据结构类似于slice或map,但不是内置在语言中的,例如链表或二叉树。

对于类型参数,优先选择函数而不是方法

Tree 示例说明了另一个一般原则:当你需要比较函数之类的东西时,更喜欢使用函数而不是方法。

实现通用方法

类型参数可能有用的另一种情况是,不同类型需要实现某些公共方法,而不同类型的实现看起来都是相同的。

例如,考虑标准库的 sort.Interface,它要求类型实现三个方法: Len、 Swap 和 Less

下面是一个泛型类型 SliceFn 的示例,它为切片类型实现 sort.Interface

// SliceFn 为T类型切片实现 sort.Interface 
type SliceFn[T any] struct {
    s    []T
    less func(T, T) bool
}

func (s SliceFn[T]) Len() int {
    return len(s.s)
}
func (s SliceFn[T]) Swap(i, j int) {
    s.s[i], s.s[j] = s.s[j], s.s[i]
}
func (s SliceFn[T]) Less(i, j int) bool {
    return s.less(s.s[i], s.s[j])
}

2.2 不应该使用类型参数

不要用类型参数替换接口类型

众所周知,Go有接口类型。接口类型允许一种通用编程。

例如,广泛使用的io.Reader接口提供了一种通用机制,用于从包含信息(例如文件)或产生信息(例如随机数生成器)的任何值读取数据。如果对某个类型的值只需要调用该值的方法,则使用接口类型,而不是类型参数。io.Reader易于阅读、高效且有效。不需要使用类型参数,通过调用read方法从值中读取数据。

例如,你可能会尝试将这里的第一个函数签名(仅使用接口类型)更改为第二个版本(使用类型参数)。

func ReadSome(r io.Reader) ([]byte, error)

func ReadSome[T io.Reader](r T) ([]byte, error)

不要做出那种改变。省略type参数使函数更容易编写,更容易读取,并且执行时间可能相同。

最后一点值得强调。虽然可以用几种不同的方式实现泛型,而且随着时间的推移,实现也会发生变化和改进,但在许多情况下,Go 1.18中使用的实现将处理类型为类型参数的值,就像处理类型为接口类型的值一样。这意味着使用类型参数通常不会比使用接口类型快。因此,不要为了速度而从接口类型更改为类型参数,因为它可能不会运行得更快。

如果方法实现不同,不要使用类型参数

在决定是否使用类型参数或接口类型时,请考虑方法的实现。前面我们说过,如果一个方法的实现对于所有类型都是相同的,那么就使用一个类型参数。相反,如果每种类型的实现都不同,则使用接口类型并编写不同的方法实现,不要使用类型参数。

例如,从文件读取的实现与从随机数生成器读取的实现完全不同。这意味着我们应该编写两个不同的Read方法,并使用像io.Reader这样的接口类型。

在适当的地方使用反射

Go具有运行时反射。反射允许一种泛型编程,因为它允许你编写适用于任何类型的代码。

如果某些操作甚至必须支持没有方法的类型(不能使用接口类型),并且每个类型的操作都不同(不能使用类型参数),请使用反射。

encoding/json包就是一个例子。我们不想要求我们编码的每个类型都有MarshalJSON方法,所以我们不能使用接口类型。但对接口类型的编码与对结构类型的编码不同,因此我们不应该使用类型参数。相反,该包使用反射。代码不简单,但它有效。有关详细信息,请参阅源代码。

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

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

相关文章

Docker—入门及Centos7安装

1、Docker入门 1.1、Docker是什么&#xff1f; Docker是基于Go语言实现的云开源项目。 Docker的主要目标是“Build&#xff0c;Ship&#xff0c;and Run Any App,Anywhere”&#xff0c;也就是通过对应组件的封装、分发、部署、运行等生命周期的管理&#xff0c;使用户的APP&…

指数计算(填空)

解题思路&#xff1a; 7的2020次方很大&#xff0c;需要用BigInteger来进行运算。 pow用来算指数运算。 remainder用来算BigInteger之间的取余。 解题代码&#xff1a; public static void main(String[] args) {BigInteger xnew BigInteger ("7");BigInteger ynew…

Tools工具安装及模板制作

Tools工具&#xff1a; 如何安装tools 1、右键单击——tools——挂载tools 2、把光盘里面的内容挂载到系统的临时挂载目录里面 mount /dev/cdrom /mnt 先卸载光盘 安装zip包 ********************************************** 使用yum源 1、挂载光盘 2、配置yum 写文件 安装…

java servlet勤工助学家教管系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 java servlet 勤工助学家教管系统是一套完善的java web信息管理系统 serlvetdaobean mvc 模式开发 &#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myecli…

Vite+Vue3使用Vue-i8n笔记

一、下载依赖 vue-i18n yarn add vue-i18n创建存放语言文件的目录 以及配置文件的配置 我是在src/lang 新建index.ts、cn.ts、en.ts以及test文件夹其中再分别新建cn.ts以及en.ts /lang/index.ts 用于导出vue-i18n需要的配置对象 import en from "./en.ts"; import…

(八)循环控制中的break和continue关键字

文章目录 break提前结束整个循环示例代码现象 continue提前结束本次循环示例代码现象 break提前结束整个循环 示例 代码 #include <stdio.h>int main() {int numOfPerson ;int totalmoney 0;int money ;printf("这是一个使用break关键字的程序\n");for(numO…

2024年阿里云幻兽帕鲁Palworld游戏服务器优惠价格表

自建幻兽帕鲁服务器租用价格表&#xff0c;2024阿里云推出专属幻兽帕鲁Palworld游戏优惠服务器&#xff0c;配置分为4核16G和4核32G服务器&#xff0c;4核16G配置32.25元/1个月、10M带宽66.30元/1个月、4核32G配置113.24元/1个月&#xff0c;4核32G配置3个月339.72元。ECS云服务…

【Git配置代理】Failed to connect to github.com port 443 问题解决方法

前言&#xff1a; 在学习代码审计时&#xff0c;有时会需要使用git去拉取代码&#xff0c;然后就出现了如下错误 看过网上很多解决方法&#xff0c;觉得问题的关键还是因为命令行在拉取/推送代码时并没有使用VPN进行代理。 解决办法 &#xff1a; 配置http代理&#xff1a;…

前端canvas项目实战——简历制作网站(三)——右侧属性栏(线条宽度样式)

目录 前言一、效果展示二、实现步骤1. 实现线条宽度&#xff08;strokeWidth&#xff09;的属性模块2. 实线线条样式&#xff08;strokeDashArray&#xff09;的属性模块3. 意料之外的“联动” 三、Show u the code后记 前言 上一篇博文中&#xff0c;我们初步实现了右侧属性栏…

理顺 QR 分解算法

咱们网站的这个公式编辑器&#xff0c;估计是后台生成图片后贴回来的&#xff0c;固定分辨率而且分辨率不高。 还不如先离线 latex 生成 pdf 后再截图上来 1. Why QR When A and b are known, to solver the minimization of , where . The reduction of A to various canoni…

【计算机网络】概述|分层体系结构|OSI参考模型|TCP/IP参考模型|网络协议、层次、接口

目录 一、思维导图 二、计算机网络概述 1.计算机网络定义、组成、功能 2.计算机网络分类 3.计算机网络发展历史 &#xff08;1&#xff09;计算机网络发展历史1&#xff1a;ARPANET->互联网 &#xff08;2&#xff09;计算机网络发展历史2&#xff1a;三级结构因特网 …

时序预测 | MATLAB实现ICEEMDAN-SSA-GRU、ICEEMDAN-GRU、SSA-GRU、GRU时间序列预测对比

时序预测 | MATLAB实现ICEEMDAN-SSA-GRU、ICEEMDAN-GRU、SSA-GRU、GRU时间序列预测对比 目录 时序预测 | MATLAB实现ICEEMDAN-SSA-GRU、ICEEMDAN-GRU、SSA-GRU、GRU时间序列预测对比预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 时序预测 | MATLAB实现ICEEMDAN…

详解SpringCloud微服务技术栈:ElasticSearch实践2——RestClient查询并处理文档

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位大四、研0学生&#xff0c;正在努力准备大四暑假的实习 &#x1f30c;上期文章&#xff1a;详解SpringCloud微服务技术栈&#xff1a;ElasticSearch搜索结果处理&#xff08;排序、分页、高亮&#xff09; &#x1f4da;订…

kali系统入侵电脑windows(win11系统)渗透测试,骇入电脑教学

本次渗透测试将使用kali虚拟机&#xff08;攻击机&#xff09;对本机&#xff08;靶机&#xff09;进行入侵并监控屏幕 声明&#xff1a;本篇仅仅是将本机作为靶机的一次简易渗透测试&#xff0c;实际情况中基本不可能出现如此简单的木马骇入&#xff08;往往在上传木马时就被防…

Unity中URP下逐顶点光照

文章目录 前言一、之前额外灯逐像素光照的数据准备好后&#xff0c;还有最后的处理二、额外灯的逐顶点光照1、逐顶点额外灯的光照颜色2、inputData.vertexLighting3、surfaceData.albedo 前言 在上篇文章中&#xff0c;我们分析了Unity中URP下额外灯&#xff0c;逐像素光照中聚…

vue3之echarts3D环柱图

vue3之echarts3D环柱图 效果&#xff1a; 核心代码&#xff1a; <template><div class"container"><div ref"chartRef" class"charts"></div></div> </template><script lang"ts" setup&g…

通讯录小项目(上)

Start And Stick 通讯录的实现有很多种方式&#xff0c;今天我们将用结构体实现简单的通讯录项目功能。包括通讯录的增、删、查、改等功能。 思路&#xff1a; 此次代码文件分别为&#xff1a; 文件名用途sqlist.h用于函数和结构体的声明sqlist.c用于函数的实现test.c用于通讯…

【数据结构四】栈与Stack详解

目录 栈与Stack 1.实现一个自己的栈 2.Stack的基本使用 3.栈的一些oj题训练 4.栈&#xff0c;虚拟机栈&#xff0c;栈帧的区别 栈与Stack 栈 &#xff1a;一种特殊的线性表&#xff0c;其 只允许在固定的一端进行插入和删除元素操作 。进行数据插入和删除操作的一端称为栈顶…

Java集合总览

1.总览 Java中的集合分List、Set、Queue、Map 4种类型。 List&#xff1a;大多数实现元素可以为null&#xff0c;可重复&#xff0c;底层是数组或链表的结构&#xff0c;支持动态扩容 Set&#xff1a;大多数实现元素可以为null但只能是1个&#xff0c;不能重复&#xff0c; …

LeetCode(1)

目录 时间复杂度分析&#xff1a; 递归 题1&#xff1a;爬楼梯 解法1&#xff1a;递归 解法2&#xff1a;循环 题2&#xff1a;两数之和 解法1&#xff1a;暴力枚举 解法2&#xff1a; 哈希表 题3&#xff1a;合并两个有序数组 解法1&#xff1a;直接合并后排序 解法2&…