Go语言进化之路:泛型的崛起与复用的新篇章

news2024/11/29 10:52:49

一、引言

泛型编程在许多编程语言中都是一项非常强大的特性,它可以使程序更加通用、具有更高的重用性。然而,Go语言在很长一段时间内一直没有提供泛型功能。在过去的一些版本中,Go语言开发者试图引入泛型,但最终都因为各种原因被取消或搁置了。直到Go 1.18版本,终于引入了泛型功能。在本文中,将会介绍这项新特性及其使用方法。

二、什么是泛型?

泛型是一种编程语言的特性,它可以将类型参数化,并以类型参数形式传递到不同的算法和数据结构中。泛型使得程序可以更加通用、安全且具有更高的重用性。不同的类型参数可以通过参数化类型类型来表示。例如,在Java中,可以使用ArrayList<Integer>来表示包含整数的动态数组,其中Integer是类型参数的类型。

在Go语言中,泛型的类型参数可以是任何类型,包括基本类型、引用类型、结构体和接口等。这些类型参数可以用在函数、方法、结构体、接口、通道和映射等语法结构中。

三、得一切从函数的形参和实参说起

当谈到泛型编程时,我们需要了解两个重要的概念:类型形参和类型实参。

  • 类型形参(Type Parameters):类型形参是一种在泛型代码中使用的占位符类型。它们允许我们定义函数、方法或数据结构,这些代码可以处理多种类型的数据而不是特定的类型。在 Go 语言中,类型形参使用方括号 [] 包围,并且可以在函数、方法或结构体的名称后面定义。例如,func Test[T any](x T) 中的 [T any] 就是一个类型形参。在使用泛型函数或结构体时,我们需要提供实际的类型实参来替换类型形参的位置。
  • 类型实参(Type Arguments):类型实参是在使用泛型代码时提供的具体类型。当我们调用泛型函数或实例化泛型结构体时,我们需要指定具体的类型实参,以替换泛型代码中的类型形参。类型实参可以是任何合法的类型,包括基本类型、结构体、接口类型等。例如,Test[int](3) 中的 [int] 就是一个类型实参。

使用类型形参和类型实参的一个典型例子是在泛型函数中定义类型形参,然后调用该函数时提供类型实参的类型。例如:

package main


import "fmt"


// 定义泛型函数
func PrintType[T any](x T) {
    fmt.Printf("Type: %T\n", x)
}


func main() {
    // 调用泛型函数,类型实参为 int
    PrintType[int](42)
    // 调用泛型函数,类型实参为 string
    PrintType[string]("hello")
}
输出结果:
Type: int
Type: string

在上面的示例中,我们定义了一个名为 PrintType 的泛型函数,并使用 [T any] 声明了一个类型形参。然后,在调用该函数时,我们使用类型实参来具体化类型形参,例如使用 int 和 string。这样,在函数内部,我们就可以使用具体的类型信息来打印数据的类型。

类型形参和类型实参的使用为我们提供了更大的灵活性和通用性,使得我们可以编写可处理多种类型的泛型代码。

四、Go的泛型

通过上面的代码,我们对Go的泛型编程有了最初步也是最重要的认识——类型形参 和类型实参。而Go 1.18也是通过这种方式实现的泛型,但是单纯的形参实参是远远不能实现泛型编程的,所以Go还引入了非常多全新的概念:

  • 类型形参 (Type parameter):用于定义泛型类型、泛型函数等模板中,形参类型的占位符。在Go中用[T any]这样的方式表示。
  • 类型实参(Type argument):在使用泛型类型或泛型函数的时候,为泛型中的类型参传递具体的类型实参。比如,如果一个结构体类型定义了一个字段类型是泛型类型 T,在使用这个结构体类型的时候可以指定 T 的类型实参,如 MyStruct[int]。
  • 类型形参列表( Type parameter list):泛型函数、泛型类型等中声明的形参列表,语法形如:[T any,U any]
  • 类型约束(Type constraint):为泛型类型参与约束其类型范围的限制,以确保对应的类型实具有部分或者接口关系后代等。仅在Go 1.18版本及更高版本中支持。
  • 实例化(Instantiations):根据泛型类型的模板和类型实参生成具体类型的过程,本质上是传统意义下函数调用时的实参传递和函数执行的过程。
  • 泛型类型(Generic type):包含一个或多个类型形参的类型。在定义时可以通过使用type关键字进行,例如 type MyStruct[T any] struct {},表示定义了一个名为MyStruct的泛型结构体。
  • 泛型接收器(Generic receiver):用于为泛型类型声明方法,可以通过定义泛型接收器来为泛型类型定义具有泛型类型参数的方法,实现代码复用的目的。
  • 泛型函数(Generic function):包含一个或多个类型参参的函数,在调用时可以传递类型实参,确定具体类型的函数实例。在使用时,可以通过像调用普通函数一样调用它,但需要在函数名后面使用 [T any] 等形式声明其类型形参。
type MySlice[T int|float32|float64 ] []T


var mySlice MySlice[int]
上面这段代码定义了一个具有类型约束的泛型类型MySlice,T为类型参,必须是int、float32或float64之一,表示只能用这个明确的类型代替T。MySlice[T]表示一个元素类型为T切片类型。
T 就是类型形参(Type parameter),类似一个占位符
int|float32|float64 就是类型约束(Type constraint),中间的 | 就是或的意思,表示类型形参 T 只接收 int 或 float32 或 float64 这三种类型的实参
中括号里的 T int|float32|float64 这一整串因为定义了所有的类型形参(在这个例子里只有一个类型形参T),所以我们称其为 类型形参列表(Type parameter list)
在使用MySlice时,如MySlice[int]表示元素类型为int切片类型,int 就是类型实参(Type argument)
上面只是个最简单的例子,实际上类型形参的数量可以远远不止一个,如下:
// CostMap类型定义了两个类型形参 KEY 和 VALUE。分别为两个形参指定了不同的类型约束
// 这个泛型类型的名字叫:CostMap[KEY, VALUE]
type CostMap[KEY int | string, VALUE float32 | float64] map[KEY]VALUE  


// 用类型实参 string 和 flaot64 替换了类型形参 KEY 、 VALUE,
// 泛型类型被实例化为具体的类型:CostMap[string, float64]
var a CostMap[string, float64] = map[string]float64{
    "dept1_cost": 8913.34,
    "dept2_cost": 4295.64,
}

用上面的例子重新复习下各种概念:

  • KEY和VALUE是类型形参。
  • int|string 是KEY的类型约束, float32|float64 是VALUE的类型约束。
  • KEY int|string, VALUE float32|float64 整个一串文本因为定义了所有形参所以被称为类型形参列表。
  • Map[KEY, VALUE] 是泛型类型,类型的名字就叫 Map[KEY, VALUE]。
  • var a CostMap[string, float64] 中的string和float64是类型实参,用于分别替换KEY和VALUE,实例化出了具体的类型 CostMap[string, float64]。

用如下一张图就能简单说清楚:

图片

五、Go泛型实现方式

在Go语言中,泛型的实现方式是使用类型参数化函数和类型参数化结构体。类型参数化函数是一种函数,接受类型参数作为输入,并根据这些类型参数返回不同的结果。类型参数化结构体是一种结构体,其中一些或全部成员字段由类型参数确定。

以下是一个用于从切片中查找元素并返回其索引的类型参数化函数的代码示例:

func Find[T comparable](slice []T, value T) int {
    for i, v := range slice {
        if v == value {
            return i
        }
    }
    return -1
}

这个函数接收一个任意类型的切片和一个具有相同类型的值,并返回第一次出现该值的索引。类型参数T必须是“comparable”类型,也就是说,它必须是可比较的类型,这是Go泛型的一个限制。

以下是一个用于实现一个类型安全的栈的类型参数化结构体代码示例:

type Stack[T any] struct {
    data []T
}


func (s *Stack[T]) Push(v T) {
    s.data = append(s.data, v)
}


func (s *Stack[T]) Pop() (t T, err error) {
   if len(s.data) == 0 {
      return t, errors.New("stack is empty")
   }
   res := s.data[len(s.data)-1]
   s.data = s.data[:len(s.data)-1]
   return res, nil
}


func main() {
   var stack Stack[int]
   stack.Push(1)
   stack.Push(2)
   stack.Push(3)
   item, err := stack.Pop()if err != nil {
      fmt.Println("Error:", err)
   } else {
      fmt.Println("Pop item:", item)
   }
   item, err = stack.Pop()if err != nil {
      fmt.Println("Error:", err)
   } else {
      fmt.Println("Pop item:", item)
   }
}

这个结构体表示栈,其中T是元素类型,并且在Push和Pop函数中使用。注意,这里的类型参数T没有任何限制,因此可以传递任何类型。var stack Stack[int] 在初始化实例时,就把类型设置好了。

以上是一些示例代码,展示了Go泛型的使用。在复杂的程序中,泛型的使用可以使代码更加通用、易于阅读、安全且具有更高的重用性。

六、Go语言和其他语言在泛型上的对比

Go语言的泛型实现与其他编程语言(如Java、C++、C#等)的泛型实现有一些不同的地方。以下是它们在一些方面的对比:

  1. 语法:Go泛型的语法相对简单,采用了类似接口的方式声明泛型类型参数,用[Tany]这样的方式表示。而其他语言的泛型语法则比较复杂,涉及到泛型类、泛型型式方法等多个方面。
  2. 实现方式:Go泛型的实现方式采用了代码生成(代码生成)的方式,即在编译时自动生成特定类型的代码。而其他语言则采用了编译时静态类型检查的方式,即在编译时对泛型类型参数进行类型检查,并生成相应的代码。
  3. 类型限制:泛型的类型限制比较广泛,可以使用任意类型作为泛型类型参数。而其他语言则通常需要对泛型类型参数进行限制,以确保其满足特定的类型要求(如继承关系、实现接口等)。
  4. 性能:Go泛型的性能比其他的泛型实现要低一些,因为其采用了代码生成的方式,在运行时需要额外生成和加载对应的代码。而其他语言则采用了预编译的方式,在编译时已经生成了相应的代码,运行时不需要再进行额外的操作。

总的来说,Go泛型的实现方式比较简单、灵活,但在性能方面有些损失。但同时,Go语言也在持续地改进其泛型实现,以提高其性能,并加入更多的功能特性。

七、Go的实战应用

以下代码是Go中用泛型实现Set无序集合,包含了添加,删除,是否存在,转成列表等方法。

type Set[T comparable] struct {
   m map[T]struct{}
}


func (s *Set[T]) Add(t T) {
   s.m[t] = struct{}{}
}


func (s *Set[T]) Remove(t T) {
   delete(s.m, t)
}


func (s *Set[T]) Exist(t T) bool {
   _, ok := s.m[t]
   return ok
}


func (s *Set[T]) List() []T {
   t := make([]T, len(s.m))
   var i int
   for k := range s.m {
      t[i] = k
      i++
   }
   return t
}


func (s *Set[T]) ForEach(f func(T)) {
   for k, _ := range s.m {
      f(k)
   }
}

八、Go泛型的优势

Go泛型的出现,使得我们可以更加通用、安全且具有更高的重用性。它的出现具有以下优势:

  1. 更加通用:泛型使得我们可以创建能够操作任何类型的数据结构和算法,从而使得代码可以更加通用。
  2. 安全性:类型参数化函数和类型参数化结构体使得编译器可以对代码进行更严格的类型检查,从而减少了许多类型相关的运行时错误。
  3. 可读性:类型参数化使得代码可以更加清晰、简洁和易于阅读。在不同的数据结构和算法中,使用相同的代码模板可以减少代码量。

九、总结

在Golang中,泛型功能的引入提高了Go的通用性、可读性和安全性。使用类型参数化的方式,我们可以编写出可以处理任何类型的代码。尽管Go泛型的实现方式略有不同于其他语言,但仍然可以为程序员提供实用的工具和功能,使代码更加通用、安全、易读和易于维护。

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

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

相关文章

分布式系统——分布式系统知识脑图

摘要 本博文主要介绍分布式系统知识脑图&#xff0c;帮助大家更好的快速的了解分布式系统相关知识。同时也是为大家在工作中应对分布式系统设计提供相关参考。 一、分布式系统知识脑图 博文参考

origin自定义颜色

点击图例&#xff0c;然后点击菜单栏的颜色右边的三角形。选择自定义颜色 在红绿蓝里输入自己想要的颜色配比。点击确定 如图&#xff0c;定义好五组颜色。我这里用的是比较经典的五色配图 蓝&#xff1a;1,86,153 黄&#xff1a;250,192,15 橙&#xff1a;243,118,74 浅蓝…

mysql workbench常见问题

1、No database selected Select the default DB to be used by double-clicking its name in the SCHEMAS list in the sidebar 方法一&#xff1a;双击你要使用的库 方法二&#xff1a;USE 数据库名 2、复制表名&#xff0c;字段名 3、保存链接

【PostgreSQL内核学习(十一)—— (CreatePortal)】

CreatePortal 概述CreatePortal 函数GetPortalByName 函数PortalHashTableLookup 函数 MemoryContextAllocZero 函数 AllocSetContextCreate 函数ResourceOwnerCreatePortalHashTableInsert总结 声明&#xff1a;本文的部分内容参考了他人的文章。在编写过程中&#xff0c;我们…

AI大模型服务应用场景

大模型是指模型具有庞大的参数规模和复杂程度的机器学习模型。在深度学习领域&#xff0c;大模型通常是指具有数百万到数十亿参数的神经网络模型。这些模型通常在各种领域&#xff0c;例如自然语言处理、图像识别和语音识别等&#xff0c;表现出高度准确和广泛的泛化能力。伴随…

2023-9

内核向应用层发送netlink单播消息&#xff1a; nlmsg_unicast -> netlink_unicast -> netlink_sendskb -> __netlink_sendskb -> 把skb链入struct sock 的 sk_receive_queue 链表中&#xff0c;再调用sk->sk_data_ready(sk); -> sock_def_readable -> wak…

排序算法:归并排序(递归和非递归)

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家解读一下有关排序算法的相关知识点&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从入门到精通…

关于使用API接口获取商品数据的那些事(使用API接口获取商品数据的步骤和注意事项。)

随着电商行业的不断发展&#xff0c;越来越多的企业和个人需要获取各大电商平台上的商品数据。而最常用的方法是使用API接口获取商品数据。本文将为您介绍使用API接口获取商品数据的步骤和注意事项。 一、选择API接口 首先需要了解各大电商平台提供的API接口&#xff0c;目前…

算法通关村第14关【黄金】| 数据流的中位数

思路&#xff1a;使用一个小根堆一个大根堆来找中位数 小根堆保存较大的一半数字&#xff0c;大根堆保存较小的一半数字 奇数queMin的队头即为中位数&#xff0c;偶数queMin和queMax队头相加/2为中位数 初始状态&#xff1a; queMin: [] queMax: [] 添加数字 1&#xff1a; …

【Java 基础篇】Java 进程详解:从基础到实践

Java 是一种广泛应用于各种类型的软件开发的编程语言&#xff0c;而与 Java 紧密相关的一个概念就是进程。本篇博客将从基础开始&#xff0c;详细介绍 Java 进程的概念、创建、管理以及一些实际应用场景。无论您是初学者还是有一定经验的开发者&#xff0c;都能从本文中获取有关…

如何高效批量查询快递单号,提高工作效率?

在日常生活中&#xff0c;快递单号的查询是一项常规任务。过去&#xff0c;这项任务需要通过人工一个一个地在快递平台上查询&#xff0c;既耗时又费力。然而&#xff0c;随着科技的发展&#xff0c;我们有了更多的工具可以帮助我们高效地完成这项任务。本文将介绍如何使用固乔…

【List篇】LinkedList 详解

目录 成员变量属性构造方法add(), 插入节点方法remove(), 删除元素方法set(), 修改节点元素方法get(), 取元素方法ArrayList 与 LinkedList的区别Java中的LinkedList是一种实现了List接口的 双向链表数据结构。链表是由一系列 节点(Node)组成的,每个节点包含了指向 上一个…

Java“牵手”1688商品评论数据采集+1688商品评价接口,1688商品追评数据接口,行业商品质检接口,1688API接口申请指南

1688商品评论平台是阿里巴巴集团旗下的一个在线服务市场平台&#xff0c;为卖家提供商品评价服务。平台上有多种评价工具和数据支持&#xff0c;可以帮助卖家更好地了解商品的质量和特点&#xff0c;从而做出更明智的采购决策。 1688商品评论平台支持多种评价方式&#xff0c;…

R语言画多变量间的两两相关性图

语言代码&#xff1a; setwd("D:/Desktop/0000/R") #更改路径df<-read.csv("kaggle/Seed_Data.csv") head(df) df$target<-factor(df$target) # 因为目标是数字&#xff0c;所以加他&#xff0c;不加会报错 cols<-c("steelblue","…

《动手学深度学习 Pytorch版》 7.1 深度卷积神经网络(LeNet)

7.1.1 学习表征 深度卷积神经网络的突破出现在2012年。突破可归因于以下两个关键因素&#xff1a; 缺少的成分&#xff1a;数据 数据集紧缺的情况在 2010 年前后兴起的大数据浪潮中得到改善。ImageNet 挑战赛中&#xff0c;ImageNet数据集由斯坦福大学教授李飞飞小组的研究人…

OpenCV中的HoughLines函数和HoughLinesP函数到底有什么区别?

一、简述 基于OpenCV进行直线检测可以使用HoughLines和HoughLinesP函数完成的。这两个函数之间的唯一区别在于,第一个函数使用标准霍夫变换,第二个函数使用概率霍夫变换(因此名称为 P)。概率版本之所以如此,是因为它仅分析点的子集并估计这些点都属于同一条线的概率。此实…

威胁的数量、复杂程度和扩散程度不断上升

Integrity360 宣布了针对所面临的网络安全威胁、数量以及事件响应挑战的独立研究结果。 数据盗窃、网络钓鱼、勒索软件和 APT 是最令人担忧的问题 这项调查于 2023 年 8 月 9 日至 14 日期间对 205 名 IT 安全决策者进行了调查&#xff0c;强调了他们的主要网络安全威胁和担忧…

评价指标分类

声明 本文是学习GB-T 42874-2023 城市公共设施服务 城市家具 系统建设实施评价规范. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本文件确立了城市家具系统建设实施的评价原则、评价流程&#xff0c;给出了评价指标&#xff0c;描述了 方…

nodejs定时任务

项目需求&#xff1a; 每5秒执行一次&#xff0c;多个定时任务错开&#xff0c;即cron表达式中斜杆前带数字&#xff0c;例如 ‘1/5 * * * * *’定时任务准时&#xff0c;延误低 搜索了nodejs的定时任务&#xff0c;其实不多&#xff0c;找到了以下三个常用的&#xff1a; n…

无涯教程-JavaScript - BETA.INV函数

描述 BETA.INV函数返回beta累积概率密度函数(BETA.DIST)的反函数。如果概率 BETA.DIST(x ... TRUE),则BETA.INV(概率...) x。 在预期的完成时间和可变性的情况下,可以在项目计划中使用beta分布来建模可能的完成时间。 语法 BETA.INV (probability,alpha,beta,[A],[B])争论 …