golang 泛型详解

news2024/11/18 1:25:07

目录

概念

~int vs .int

常见的用途和错误

结论:


概念

Go 在1.18 中添加了泛型,这样Go 就可以在定义时不定义类型,而是在使用时进行类型的定义,并且还可以在编译期间对参数类型进行校验。Go 目前只支持泛型方法,还不支持泛型接口,下面我会详细介绍Go的泛型使用以及常见的使用错误。其他概念性的东西我不想说太多,大家可以自己百度。

我们先看一个例子,很简单就是从map[string]int 提出所有的键的函数:

func getKeys(m map[string]int) []string {
    var keys []string
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

如果我们现在需要的是map[int]int,那么我们该怎么做呢?在使用泛型之前,Go 开发人员有几种选择:使用代码生成、反射或重复代码。例如,我们可以编写两个函数,每个函数对应一种 map 类型,或者甚至尝试扩展 getKeys 以接受不同的 map 类型:

func getKeys(m any) ([]any, error) {                      ❶
    switch t := m.(type) {
    default:
        return nil, fmt.Errorf("unknown type: %T", t)     ❷
    case map[string]int:
        var keys []any
        for k := range t {
            keys = append(keys, k)
        }
        return keys, nil
    case map[int]string:
        // Copy the extraction logic
    }
}

❶ 接受和返回一个interface{}

❷如果不是我们需要的类型,返回一个错误

通过这个例子,我们注意到了一些问题。首先,它增加了模版代码。事实上我们想增加一个case 时,需要重复range 循环。同时函数现在接受任意类型,这意味着我们失去了GO作为类型化语言的一些优势。事实上,检查类型是否支持是在运行时而不是编译时进行的。因此如果类型不合法,我们还需要返回错误。最后,由于key 的类型是int或者字符串,我们必须返回interface{} 的切片。如果对切片进行处理,还需要断言操作。这种方法增加了调用方的工作量。现在有了泛型,我们可以重构这段代码。

下面是泛型的基本语法

类型参数是我们可以与函数和类型一起使用的通用类型。例如,以下函数接受一个类型参数:

func foo[T any](t T) {     ❶
    // ...
}

❶ T 是类型参数

在调用 foo 时,我们传递一个任意类型的类型参数。提供类型参数称为实例化,这项工作在编译时完成。这样,类型安全就成了核心语言特性的一部分,而 避免了运行时的开销。

让我们回到 getKeys 函数,使用类型参数编写一个通用版本,它可以接受任何类型的映射:

func getKeys[K comparable, V any](m map[K]V) []K {   ❶
    var keys []K                                     ❷
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

❶ K 是可以比较的,value 是 interface{}

❷ 创建一个K slice

我们这么定义的原因是map 的key 必须是可以比较的类型,不能是 any。比如我们不能用slices:

var m map[[]byte]int

这段代码会导致编译错误:映射键类型 []byte 无效。因此,我们不能接受任何键类型,而必须限制类型参数,使键类型符合特定要求。这里的要求是键类型必须是可比较的(我们可以使用 == 或 !=)。因此,我们将 K 定义为可比类型,而不是任意类型。

限制类型参数以满足特定要求称为约束。约束是一种接口类型,可以包含:一组行为(方法),任意类型 让我们举一个具体的例子来说明后者。假设我们不想接受任何可比较的映射键类型。例如,我们想将其限制为 int 或字符串类型。我们可以这样定义一个自定义约束:

type customConstraint interface {
    ~int | ~string                   ❶
}
func getKeys[K customConstraint,     ❷
         V any](m map[K]V) []K {
   
}

❶ 定义一个自定义类型,将类型限制为 int 和字符串 ❷ 将类型参数 K 改为自定义约束类型

首先,我们定义了一个 customConstraint 接口,使用联合运算符 | 将类型限制为 int 或字符串(稍后我们将讨论 ~ 的使用)。现在,K 是一个 customConstraint,而不是之前的可比类型。

getKeys 的签名强制要求我们可以使用任何值类型的 map 调用它,但键类型必须是 int 或字符串--例如,在调用方:

m = map[string]int{
    "one":   1,
    "two":   2,
    "three": 3,
}
keys := getKeys(m)

请注意,Go 可以推断出 getKeys 是以字符串类型参数调用的。之前的调用等同于此:

keys := getKeys[string](m)

~int vs .int

int 就是变量声明int 类型比如 var a int 而 ~int 指的是底层为int 类型,如 type b int , b 和 int 是两个不同的类型,但是b 的底层是int 类型

type customConstraint interface {
   ~int
   String() string
}
type customInt int
​
func (i customInt) String() string {
   return strconv.Itoa(int(i))
}
func getKeys[keys customConstraint, v any](m map[keys]v) {
   
}
func main() {
   t := customInt(1)
   getKeys(map[customInt]any{t: "a"})
}

由于 customInt 是一个 int 并实现了 String() 字符串方法,因此 customInt 类型满足已定义的约束。但是,如果我们将约束条件改为包含 int 而不是 ~int,那么使用 customInt 就会导致编译错误,因为 int 类型没有实现 String() 字符串。

到目前为止,我们已经讨论了在函数中使用泛型的示例。不过,我们也可以在数据结构中使用泛型。例如,我们可以创建一个包含任意类型值的链表。为此,我们将编写一个 Add 方法来追加一个节点:

type Node[T any] struct {                ❶
    Val  T
    next *Node[T]
}
 
func (n *Node[T]) Add(next *Node[T]) {   ❷
    n.next = next
}

❶ 用一个类型参数

❷ 实例化一个类型接受者

在示例中,我们使用类型参数来定义 T,并在 Node 中使用这两个字段。关于方法,接收器是实例化的。事实上,由于 Node 是泛型的,它也必须遵循定义的类型参数。

关于类型参数,最后需要注意的一点是,它们不能与方法参数一起使用,只能与函数参数或方法接收器一起使用。例如,下面的方法将无法编译:

如果我们想在方法中使用泛型,那么接收器就必须是一个类型参数。现在,让我们来看看应该和不应该使用泛型的具体情况。

常见的用途和错误

讲到这里大家可能会问泛型如何使用呢,现在我们就讨论泛型几种常见的用途

  1. 数据结构--例如,如果我们实现的是二叉树、链表或堆,我们可以使用泛型来确定元素类型。

  2. 处理任何类型的切片、映射和通道的函数--例如,合并两个通道的函数可以处理任何类型的通道。因此,我们可以使用类型参数来确定通道类型:

比如在通道中我们对channel 类型进行限制了:

type chantype interface {
   int | string
}
​
func merge1[T chantype](ch1, ch2 <-chan T) <-chan T {
   // ...
}
  1. 例如,排序软件包包含一个 sort.Interface 接口,其中有三个方法:

    type Interface interface {
        Len() int
        Less(i, j int) bool
        Swap(i, j int)
    }

该接口被不同的函数使用,如 sort.Ints 或 sort .Float64。我们该怎么做呢?很多时候我们可能会想到用interface{} 抽象然后断言,用了泛型我们就可以提供一个模版,实例化什么参数由用户去决定了。

type SliceFn[T any] struct {
   S       []T  
   Compare func(T, T) bool
}
​
func (s SliceFn[T]) Len() int {
   return len(s.S)
}
​
func (s SliceFn[T]) Less(i, j int) bool {
   return s.Compare(s.S[i], s.S[j])
}
​
func (s SliceFn[T]) Swap(i, j int) {
   s.S[i], s.S[j] = s.S[j], s.S[i]
}
​
func main() {
   s := SliceFn[int]{
      S: []int{1, 2, 3},
      Compare: func(a int, b int) bool {
         return a > b
      },
   }
   sort.Sort(s)
   fmt.Println(s.S)
}

在本例中,通过分解行为,我们可以避免为每种类型创建一个函数。可以发现其实泛型比interface 更为抽象

结论:

先看一个例子:

func foo[T io.Writer](w T) {
    b := getBytes()
    _, _ = w.Write(b)
}

在调用参数类型的方法时--例如,考虑一个接收 io.Writer 并调用 Write 方法的函数:

在这种情况下,使用泛型不会给我们的代码带来任何价值。我们应该直接将 w 参数设为 io.Writer。 当泛型使我们的代码更复杂时--泛型从来不是强制性的,作为 Go 开发者,我们已经在没有泛型的情况下生活了十多年。如果我们在编写泛型函数或结构时发现它并没有让我们的代码变得更清晰,那么我们或许应该重新考虑我们在该特定用例中的决定。 虽然泛型在特定情况下会有所帮助,但我们应该谨慎对待何时使用、何时不使用泛型。一般来说,如果我们想回答何时不使用泛型,我们可以找到与何时不使用接口的相似之处。事实上,泛型引入了一种抽象形式,而我们必须记住,不必要的抽象会带来复杂性。 我们还是那句话,不要用不必要的抽象来污染我们的代码,现在还是专注于解决具体问题吧。这意味着我们不应过早使用类型参数。等到我们要编写模板代码时,再考虑使用泛型吧。

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

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

相关文章

无源蜂鸣器驱动实验

1.原理 计划是&#xff0c;生成不同的7个频率控制蜂鸣器发声&#xff0c;每个音调发声0.5秒。 50MHZ计数0.5秒&#xff0c;50MHZ一个周期20ns&#xff0c;要计数0.5秒 &#xff0c;0.5/0.000_000_02s25000_000个时钟周期。因为是从0开始计数&#xff0c;所以计数的最大值是0-24…

docker版本 jenkins配置gitlab自动部署

前端项目 Build steps pwd npm config set registry https://registry.npm.taobao.org npm -v node -v #npm install npm run build:prod tar -czvf QASystem.tar.gz distpwd cd /data/zhouxy37/vue_deploy tar -zxvf QASystem.tar.gz sudo mv dist QASystem cp -r QASyste…

K8S常用kubectl命令汇总(持续更新中)

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

NGINX的重写与反向代理机制解析

目录 引言 一、重写功能 &#xff08;一&#xff09;if指令 1.判断访问使用的协议 2.判断文件 &#xff08;二&#xff09;return指令 1.设置返回状态码 2.返回指定内容 3.指定URL &#xff08;三&#xff09;set指令 1.手动输入变量值 2.调用其它变量值为自定义变…

web学习笔记(二十二)DOM开始

目录 1.DOM简介 2.DOM树 3.DOM节点 4.查找DOM节点方法汇总 5.查找子结点的属性 5.1父子关系 5.2兄弟关系 6.几个特殊元素的查找 1.DOM简介 DOM&#xff08;Document Object Model&#xff09; 也叫页面文档对象模型&#xff0c;是W3C组织推荐的处理可扩展标记语言(HTML…

蓝桥杯第十二届电子类单片机组程序设计

目录 前言 蓝桥杯大赛历届真题_蓝桥杯 - 蓝桥云课&#xff08;点击查看&#xff09; 单片机资源数据包_2023&#xff08;点击下载&#xff09; 一、第十二届比赛原题 1.比赛题目 2.题目解读 蓝桥杯第十四届电子类单片机组程序设计_蓝桥杯单片机哪一届最难-CSDN博客 二、…

TV-SAM 新型零样本医学图像分割算法:GPT-4语言处理 + GLIP视觉理解 + SAM分割技术

TV-SAM 新型零样本医学图像分割算法&#xff1a;GPT-4语言处理 GLIP视觉理解 SAM分割技术 提出背景TV-SAM 方法论 提出背景 论文&#xff1a;https://arxiv.org/ftp/arxiv/papers/2402/2402.15759.pdf 代码&#xff1a;https://github.com/JZK00/TV-SAM 利用了GPT-4的强大语…

深入理解Spring Security

第1章&#xff1a;Spring Security简介 大家好&#xff0c;我是小黑&#xff0c;在谈到网站安全的时候&#xff0c;Spring Security是个避不开的话题。它就像是个守门员&#xff0c;决定谁能进入我们的网站&#xff0c;又能在网站的哪些角落里走动。简单来说&#xff0c;Sprin…

语音合成(TTS) GPT-SoVITS认知

写在前面 小伙伴推荐&#xff0c;简单了解相对之前试过的其他的TTS项目&#xff0c;GPT-SoVITS的优点简单易用&#xff0c;文档完整&#xff0c;默认的模型效果就很好理解不足小伙伴帮忙指正 不必太纠结于当下&#xff0c;也不必太忧虑未来&#xff0c;当你经历过一些事情的时候…

智能驾驶控制规划理论学习02-基于搜索的路径规划算法

目录 一、路径搜索问题 二、图论基础 三、图搜索方法 1、广度优先搜索&#xff08;BFS&#xff09; bfs与dfs的区别 bfs的搜索过程 bfs的算法实现 2、迪杰斯特拉算法&#xff08;Dijkstra&#xff09; 核心思想 优先级队列 Dijkstra搜索过程 Dijkstra优缺点…

MySQL(基础篇)——函数、约束

一.函数 1.定义 函数是指一段可以直接被另一段程序调用的程序或代码。 2.字符串函数 常见如下&#xff1a; -- 字符串拼接 SELECT CONCAT(hello,MySql) AS CONCAT -- 将字符串全部转为小写 SELECT LOWER(HEllo MYSql) AS LOWER -- 将字符串全部转为大写 SELECT UPPER(Hello…

前端mock数据 —— 使用Apifox mock页面所需数据

前端mock数据 —— 使用Apifox 一、使用教程二、本地请求Apifox所mock的接口 一、使用教程 在首页进行新建项目&#xff1a; 新建项目名称&#xff1a; 新建接口&#xff1a; 创建json&#xff1a; 请求方法&#xff1a; GET。URL&#xff1a; api/basis。响应类型&#xff1…

用Flutter开发App:助力您的移动业务腾飞

一、Flutter简介 Flutter是Google推出的用于构建多平台应用程序的开源UI框架。它使用Dart语言编写&#xff0c;可以编译为原生机器代码&#xff0c;从而提供卓越的性能和流畅的用户体验。 二、Flutter的优势 一套代码&#xff0c;多平台部署&#xff1a;Flutter可以使用一套代…

183896-00-6,Biotin-C3-PEG3-C3-NH2,可以选择性降解靶蛋白

您好&#xff0c;欢迎来到新研之家 文章关键词&#xff1a;183896-00-6&#xff0c;Biotin-C3-PEG3-C3-NH2&#xff0c;Biotin-C3-PEG3-C3-amine&#xff0c;生物素-C3-PEG3-C3-胺 一、基本信息 【产品简介】&#xff1a;Biotin-PEG3-C3-NH2是一种PROTAC linker&#xff0c;…

Redis在中国火爆,为何MongoDB更受欢迎国外?

一、概念 Redis Redis&#xff08;Remote Dictionary Server&#xff09;是一个使用ANSI C编写的开源、支持网络、基于内存、分布式、可选持久性的键值对存储数据库。Redis是由Salvatore Sanfilippo于2009年启动开发的&#xff0c;首个版本于同年5月发布。 MongoDB MongoDB…

Apache Bench(ab )压力测试

目录 参数说明示例1&#xff1a;压力测试示例2&#xff1a;测试post接口post数据文件该如何编写&#xff1f; apr_pollset_poll: The timeout specified has expired (70007)apr_socket_recv: Connection reset by peer (104)参考 参数说明 官方文档参考这里。 ab -c 100 -n …

JVM——JVM与Java体系结构

文章目录 1、Java及JVM简介1.1、Java是跨平台的语言1.2、JVM是跨语言的平台 2、Java发展里程碑3、Open JDK和Oracle JDK4、虚拟机与JVM4.1、虚拟机4.2、JVM 5、JVM整体结构6、Java代码执行流程7、JVM的架构模型7.1、基于栈式架构的特点7.2、基于寄存器架构的特点 8、JVM的生命周…

使用pytorch实现一个线性回归训练函数

使用sklearn.dataset 的make_regression创建用于线性回归的数据集 def create_dataset():x, y, coef make_regression(n_samples100, noise10, coefTrue, bias14.5, n_features1, random_state0)return torch.tensor(x), torch.tensor(y), coef 加载数据集&#xff0c;并拆分…

蓝桥杯_定时器的基本原理与应用

一 什么是定时器 定时器/计数器是一种能够对内部时钟信号或外部输入信号进行计数&#xff0c;当计数值达到设定要求时&#xff0c;向cpu提出中断处理请求&#xff0c;从而实现&#xff0c;定时或者计数功能的外设。 二 51单片机的定时/计数器 单片机外部晶振12MHZ&#xff0c;…

leetcode 重复的子字符串

前要推理 以abababab为例&#xff0c;这里最主要的就是根据相等前后缀进行推导 s [ 0123 ] 如 t【 0123 】 f 【01 23 】 后两个分别是前后缀&#xff0c;第一个是总的字符串&#xff0c;然后可以推导 //首先还是算出…