云原生系列Go语言篇-泛型Part 2

类型推导和泛型

就像在使用​​:=​​时支持类型推导一样,在调用泛型函数时Go同样支持类型推导。可在上面对​​Map​​、​​Filter​​和​​Reduce​​调用中看出。有些场景无法进行类型推导(如类型参数仅用作返回值)。这时,必须指定所有的参数类型。下面的代码演示了无法进行类型推导的场景:

type Integer interface {int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64
}func Convert[T1, T2 Integer](in T1) T2 {return T2(in)
}func main() {var a int = 10b := Convert[int, int64](a) // 无法推导返回类型fmt.Println(b)
}

可在​​The Go Playground​​ 或​​第8章的GitHub代码库​​的sample_code/type_inference目录下测试这段代码。

类型元素限定常量

类型元素也可指定哪些常量可赋值给泛型变量。和运算符一样,常需要对类型元素中的所有类型名有效。没有常量可同时赋值给​​Ordered​​中列出的所有类型,因此无法将一个常量赋值给该泛型类型的变量。如果使用​​Integer​​接口,以下代码无法编译通过,因为不能将1,000赋值给8位的整型:

// INVALID!
func PlusOneThousand[T Integer](in T) T {return in + 1_000
}

但下面的就是有效的:

// VALID
func PlusOneHundred[T Integer](in T) T {return in + 100
}

组合泛型函数和泛型数据结构

回到二叉树示例,来看如何使用所学的知识生成适用所有实体类型的树。

核心在于理解该树需要一个泛型函数,可比较两个值给出排序:

type OrderableFunc [T any] func(t1, t2 T) int

有了​​OrderableFunc​​,我们就可以稍稍修改树的实现。首先将其分成两种类型,​​Tree​​和​​Node​​:

type Tree[T any] struct {f    OrderableFunc[T]root *Node[T]
}type Node[T any] struct {val         Tleft, right *Node[T]
}

通过构造函数构造一个新​​Tree​​:

func NewTree[T any](f OrderableFunc[T]) *Tree[T] {return &Tree[T]{f: f,}
}

​Tree​​的方法非常简单,因为它调用​​Node​​来完成任务:

func (t *Tree[T]) Add(v T) {t.root = t.root.Add(t.f, v)
}func (t *Tree[T]) Contains(v T) bool {return t.root.Contains(t.f, v)
}

​Node​​的​​Add​​和​​Contains​​方法与之前的非常类似。唯一的区别是传递了用于排序元素的函数:

func (n *Node[T]) Add(f OrderableFunc[T], v T) *Node[T] {if n == nil {return &Node[T]{val: v}}switch r := f(v, n.val); {case r <= -1:n.left = n.left.Add(f, v)case r >= 1:n.right = n.right.Add(f, v)}return n
}func (n *Node[T]) Contains(f OrderableFunc[T], v T) bool {if n == nil {return false}switch r := f(v, n.val); {case r <= -1:return n.left.Contains(f, v)case r >= 1:return n.right.Contains(f, v)}return true
}

现在我们需要匹配​​OrderedFunc​​定义的函数。所幸我们已经见过一个:​​cmp​​包中的​​Compare​​。在对​​Tree​​使用它时是这样:

t1 := NewTree(cmp.Compare[int])
t1.Add(10)
t1.Add(30)
t1.Add(15)
fmt.Println(t1.Contains(15))
fmt.Println(t1.Contains(40))

对于结构体,有两种选项。可以编写一个函数:

type Person struct {Name stringAge int
}func OrderPeople(p1, p2 Person) int {out := cmp.Compare(p1.Name, p2.Name)if out == 0 {out = cmp.Compare(p1.Age, p2.Age)}return out
}

然后在创建树进传递该函数:

t2 := NewTree(OrderPeople)
t2.Add(Person{"Bob", 30})
t2.Add(Person{"Maria", 35})
t2.Add(Person{"Bob", 50})
fmt.Println(t2.Contains(Person{"Bob", 30}))
fmt.Println(t2.Contains(Person{"Fred", 25}))

不使用函数,我们了可以为​​NewTree​​提供一个方法。在​​方法也是函数​​中我们讨论过,可以使用方法表达式来将方法看作函数。下面上手操作。首先编写方法:

func (p Person) Order(other Person) int {out := cmp.Compare(p.Name, other.Name)if out == 0 {out = cmp.Compare(p.Age, other.Age)}return out
}

然后使用该方法:

t3 := NewTree(Person.Order)
t3.Add(Person{"Bob", 30})
t3.Add(Person{"Maria", 35})
t3.Add(Person{"Bob", 50})
fmt.Println(t3.Contains(Person{"Bob", 30}))
fmt.Println(t3.Contains(Person{"Fred", 25}))

可在​​The Go Playground​​ 或​​第8章的GitHub代码库​​的sample_code/generic_tree目录下测试这段代码。

再谈可比较类型

在​​接口可比较​​一节中我们学到,接口中也是Go中一种可比较类型。这也就表示在对接口类型变量使用​​==​​和​​!=​​时要小心。如果接口的底层类型不可比较,代码会在运行时panic。

这个坑在使用带泛型的可比较接口时依然存在。假设我们定义了一个接口以及一些实现:

type Thinger interface {Thing()
}type ThingerInt intfunc (t ThingerInt) Thing() {fmt.Println("ThingInt:", t)
}type ThingerSlice []intfunc (t ThingerSlice) Thing() {fmt.Println("ThingSlice:", t)
}

还需要定义一个泛型函数仅接收可比较的值:

func Comparer[T comparable](t1, t2 T) {if t1 == t2 {fmt.Println("equal!")}
}

调用带类型为​​int​​或​​ThingerInt​​的变量的函数完全合法:

var a int = 10
var b int = 10
Comparer(a, b) // prints truevar a2 ThingerInt = 20
var b2 ThingerInt = 20
Comparer(a2, b2) // prints true

编译器不允许我们调用变量类型为​​ThingerSlice​​(或​​[]int​​)的函数:

var a3 ThingerSlice = []int{1, 2, 3}
var b3 ThingerSlice = []int{1, 2, 3}
Comparer(a3, b3) // compile fails: "ThingerSlice does not satisfy comparable"

但所调用的变量类型为​​Thinger​​时完全合法。如果使用​​ThingerInt​​,代码可正常编译、运行:

var a4 Thinger = a2
var b4 Thinger = b2
Comparer(a4, b4) // prints true

但也可以将​​ThingerSlice​​赋值给​​Thinger​​类型的变量。这时会出问题:

a4 = a3
b4 = b3
Comparer(a4, b4) // compiles, panics at runtime

编译器允许我们构建这段代码,但运行后程序会panic(参见​​panic和recover​​一节了解更多信息),消息为​​panic: runtime error: comparing uncomparable type main.ThingerSlice​​。可在​​The Go Playground​​ 或​​第8章的GitHub代码库​​的sample_code/more_comparable目录下测试这段代码。

在关可比较类型和泛型交互以及为何做出这种设计决策的更多技术细节,请阅读Go团队Robert Griesemer的博客文章​​All your comparable types​​。

未实现的功能

Go仍是一种小型且聚焦的编程语言,Go对泛型的实现并未包含部分在其它语言泛型中存在的特性。下面是一些Go泛型尚未实现的特性。

虽然我们可以构建一个同时能处理自定义和内置类型的树,但在Python、Ruby和C++中处理的方法却不同。它们有运算符重载,允许用户自定义类型指定运算符的实现。 Go没有添加这种特性。也就意味着我们不能使用​​range​​遍历自定义容器类型,也不能对其使用​​[]​​进行索引。

没添加运算符重载有一些原因。其一是Go语言中有极其大量的运算符。Go也不支持函数或方法重载,那就需要为不同的类型指定不同的运算函数。此外,重载的代码会不易理解,因为开发人员会为符号巧立各种含义(在C++中,​​<<​​对一些类型表示按位左移,而对另一些类型则在左侧值的右侧写值)。Go努力避免这类易读性问题。

另一个未实现的有用特性是,Go的泛型实现对方法没有附加类型参数。回看​​Map/Reduce/Filter​​函数,你可能觉得它们可像方法那样使用,如:

type functionalSlice[T any] []T// THIS DOES NOT WORK
func (fs functionalSlice[T]) Map[E any](f func(T) E) functionalSlice[E] {out := make(functionalSlice[E], len(fs))for i, v := range fs {out[i] = f(v)}return out
}// THIS DOES NOT WORK
func (fs functionalSlice[T]) Reduce[E any](start E, f func(E, T) E) E {out := startfor _, v := range fs {out = f(out, v)}return out
}

你以为可以这样用:

var numStrings = functionalSlice[string]{"1", "2", "3"}
sum := numStrings.Map(func(s string) int {v, _ := strconv.Atoi(s)return v
}).Reduce(0, func(acc int, cur int) int {return acc + cur
})

可惜对于函数式编程的拥趸们,并不能这样用。我们不能做链式方法调用,而要嵌套函数调用或使用更易读一次调用一次函数的方式,将中间值赋给变量。类型参数提案中详细讨论了未支持参数化方法的原因。

没有可变类型参数。在​​可变参数和切片​​一节中讨论到,要实现接收可变数量参数的函数,需要指定最后一个参数,其类型以​​...​​开头。比如,无法对可变参数指定某种类型模式,像可交替的​​string​​和​​int​​。所有的可变变量必须为同一种声明类型,是不是泛型皆可。

Go泛型未实现的其它特性就更加晦涩些了。有:

特化(Specialization)函数或方法可通过泛型版本外的一个或多个指定类型版本进行重载。因Go语言没有重载,这一特性不在考虑范围内。柯里化(Currying)允许我们通过指定某些类型参数根据另一个泛型函数或类型部分实例化函数。元编程允许我们指定在编译时运行的代码并生成运行时运行的代码。

地道的Go和泛型

添加泛型显然会改变一些地道使用Go的建议。使用​​float64​​来表示所有的数值类型的时代结束了。应当使用​​any​​来代替​​interface{}​​表示数据结构或函数参数中未指定的类型。可以用一个点函数处理不同的切片类型。但不要觉得要马上使用类型参数切换掉所有的代码。在新设计模式发明和深化的同时老代码依然正常可用。

现在判断泛型对性能的长期影响还为时尚早。在写本文时,它对编译时间并没有影响。Go 1.18的编译器要慢于之前的版本,但Go 1.20的编译器解决了这一问题。

有一些关于泛型对运行时间影响的影响。Vicent Marti写了一篇​​深入的文章​​,探讨了一些导致代码变慢的泛型案例并详细讲解了产生这一问题的实现细节。相反,Eli Bendersky写了一篇​​博客文章​​说明泛型让排序算法变快了。

一般来说,不要期望将带接口参数的函数修改为泛型类型参数的函数能提升性能。比如,将下面的小函数:

type Ager interface {age() int
}func doubleAge(a Ager) int {return a.age() * 2
}

转化为:

func doubleAgeGeneric[T Ager](a T) int {return a.age() * 2
}

会使得该函数在Go 1.20变慢约30%。(对于大型函数,没有显著的性能区别)。可以使用​​第8章的GitHub代码库​​的sample_code/perf directory目录下代码进行基准测试。

使用过其它语言泛型的开发者可能会感到意外。比如在C++中,编译器使用抽象数据类型的泛型来将运行时运算(确定所使用的实体类型)转化为编译时运算,为每种实体类型生成独立的函数。这会让二进制变大,但也让其变快。Vicent在博客文章中提到,当前的Go编译器仅为不同的底层类型生成独立函数。此外,所有指针类型共享同一个生成函数。为区分传递给共享生成函数的类型,编译器添加了额外的运行时查询。这会减慢性能。

随着Go未来版本中泛型实现渐趋成熟,运行时性能也会提升。目标并没有改变,还是要编写满足需求且易维护的快速运行代码。使用​​基准测试​​一节中讨论的基准测试和性能测试工具来度量和提升你的代码。

向标准库添加泛型

Go 1.18刚发布泛型时是很保守的。在全局添加了​​any​​和​​comparable​​接口,但并未在标准库中做出支持泛型的API调整。只做出了样式变化,将大部分标准库中的​​interface{}​​改成了​​any​​。

现在Go社区更适应了泛型,我们也看到了更多的变化。从Go 1.21起,标准库中包含了一些函数,使用泛型实现切片、字典和并发的常用算法。在​​复合类型​​一文中我们讲到了​​slices​​和​​maps​​包中的​​Equal​​和​​EqualFunc​​函数。这些包中的其它函数简化了切片和字典操作。​​slices​​包中的​​Insert​​、​​Delete​​和​​DeleteFunc​​ 函数让开发展不必构建极其复杂的切片处理代码。​​maps.Clone​​函数利用Go Runtime来提供更快速的方式,来创建字典的浅拷贝。在​​代码精确地只运行一次​​一节中,我们学到​​sync.OnceValue​​和​​sync.OnceValues​​,它们使用泛型来构建只运行一次并返回一到两个值的函数。推荐使用这些包中的函数,而不要自己去实现。未来版本的标准库还会包含更多用到泛型的函数和类型。

解锁未来特性

泛型可能是其它未来特性的基础。一个可能是sum types。就像类型元素用于指定可替换类型参数的类型一样,和类型可用于变量参数中的接口。这会出现一些有趣的特性。如今Go在JSON的常见场景存在问题:其字段可以是单个值也可是值列表。即使是有泛型,处理这种情况的唯一方式是装饰字段类型设为​​any​​。添加和类型可让我们创建指定字段可为字符串、字符串切片及其它类型的接口。然后类型switch可以枚举每种有效类型,提升类型案例。指定类型边界集的能力可以让现代语言(包括Rust和Swift)使用和类型替代枚举。而Go当前在枚举特性上存在不足,这会成为一种有吸引力的解决方案,但需要时间来评估和探讨这些想法 。

小结

本文中我们学习了泛型以及如何使用泛型来简化代码。对于Go来说泛型还处于早期除非。有它伴随Go语言不忘初心的成长还是很让人激动的。

本文来自正在规划的​Go语言&云原生自我提升系列​​,欢迎关注后续文章。

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

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

相关文章

血的教训------入侵redis之利用python来破解redis密码

血的教训------入侵redis之利用python来破解redis密码 利用强大的python来进行redis的密码破解&#xff0c;过程不亦乐乎&#xff0c;当然也可以用shell脚本 本篇文章只供学习交流&#xff0c;请勿他用&#xff0c;谢谢。 其他相关联的文章 [1]VMware安装部署kail镜像服务器【…

ESP32-Web-Server编程-JS 基础 2

ESP32-Web-Server编程-JS 基础 2 概述 上节介绍了 JS 编程的基础。如前所述&#xff0c;在 HTML 中&#xff0c;可以通过下述 两种方式使用 JS 程序&#xff1a; 直接在 HTML 文件中通过 script 标签中嵌入 JavaScript 代码。通过 src 元素引入外部的 JavaScript 文件。 在…

C#-创建用于测试的父类StartupBase用于服务注入

当写完C#代码&#xff0c;需要对某个方法进行测试。 创建一个XXXTests.cs文件之后&#xff0c;发现需要注入某个服务怎么办&#xff1f; 再创建一个StartupBase.cs文件&#xff1a; public abstract class StartupBase {public IConfiguration Configuration { get; }public …

西南科技大学电路分析基础实验A1(一阶电路的设计)

目录 一、实验目的 二、实验设备 三、预习内容(如:基本原理、电路图、计算值等) 四、实验数据及结果分析(预习写必要实验步骤和表格) 1. 观测一阶电

【香橙派】实战记录2——烧录安卓镜像及基本功能

文章目录 一、安卓烧录二、安卓基本功能1、蓝牙2、相机功能3、投屏 一、安卓烧录 检查环境&#xff1a;检查PC系统&#xff0c;确保有Microsoft Visual C 2008 Redistrbutable - x86&#xff0c;否则在官网下载的官方工具 - 安卓镜像烧录工具里运行vcredist_x86.exe。 插入存储…

spark学习一-------------------Spark算子最详细介绍

Spark学习–spark算子介绍 1.基本概念 spark算子&#xff1a;为了提供方便的数据处理和计算&#xff0c;spark提供了一系列的算子来进行数据处理。一般算子分为action&#xff08;执行算子&#xff09;算子Transformation&#xff08;懒执行&#xff09;算子。2.Transformatio…

鞋厂ERP怎么样?工厂要如何选项契合的ERP

鞋帽这类商品是我们的生活必需品&#xff0c;存在款式多、尺码多、用料复杂、营销渠道多、销售策略和价格策略灵活等情况&#xff0c;伴随电商等行业的发展&#xff0c;鞋帽行业的管理模式也在发生变化。 鞋厂规模的不同&#xff0c;遇到的管理问题各异&#xff0c;而如何解决…

十分钟搭建VScode C/C++运行环境

一、下载配置vscode 1.下载安装VScode 地址&#xff1a;https://code.visualstudio.com/download 下载后&#xff0c;运行安装程序 (VSCodeUserSetup-{version}.exe)。这只需要一分钟。安装程序会将 Visual Studio Code 添加到环境变量中%&#xff0c;可以使用CMD键入“code”…

Dockerfile语法和指令

简介 Dockerfile是由一系列指令和参数构成的脚本&#xff0c;一个Dockerfile里面包含了构建整个镜像的完整命令。通过docker build执行Dockerfile中一系列指令自动构建镜像。 常用指令 FROM&#xff1a;基础镜像&#xff0c;FROM领了必须是Dockerfile的首个命令。 LABEL&…

2023-2024-1-高级语言程序设计-字符数组

7-1 凯撒密码 为了防止信息被别人轻易窃取&#xff0c;需要把电码明文通过加密方式变换成为密文。输入一个以回车符为结束标志的字符串&#xff08;少于80个字符&#xff09;&#xff0c;再输入一个整数offset&#xff0c;用凯撒密码将其加密后输出。恺撒密码是一种简单的替换…

性价比高的照明品牌,考研考公必备护眼台灯推荐

据国家卫生健康委员会发布的调查数据显示,我国青少年儿童总体近视率为52.7%、高度近视人口超3000万。儿童是民族的未来和希望,青少年儿童眼健康问题更是牵动着每一个人的神经。遗传、双眼视功能不正常、用眼负荷过重等因素都是造成青少年近视的原因,其中,大量的电子产品侵入以及…

Flask Session 登录认证模块

Flask 框架提供了强大的 Session 模块组件&#xff0c;为 Web 应用实现用户注册与登录系统提供了方便的机制。结合 Flask-WTF 表单组件&#xff0c;我们能够轻松地设计出用户友好且具备美观界面的注册和登录页面&#xff0c;使这一功能能够直接应用到我们的项目中。本文将深入探…

动态网页从数据库取信息,然后展示。

把数据库的驱动放在bin目录下。 通过servlet 读取数据库的内容&#xff0c;生成session,然后跨页面传给展示页。 package src;import java.io.IOException; import java.io.PrintWriter; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSe…

大数据-之LibrA数据库系统告警处理(ALM-37008 MPPDB服务不可用)

告警解释 告警模块每30秒周期性检测MPPDB服务健康状态&#xff0c;当检测到MPPDB健康状态为“故障”时产生告警。 当检测到MPPDB健康状态为“良好”时告警恢复。 告警属性 告警ID 告警级别 可自动清除 37008 致命 是 告警参数 参数名称 参数含义 ServiceName 产生…

HJ92 在字符串中找出连续最长的数字串

题目&#xff1a; HJ92 在字符串中找出连续最长的数字串 题解&#xff1a; 找到第一个数字从第一个数字开始往后遍历&#xff0c;每走一步判断当前是否为数字&#xff0c;是数字就累加cnt如果当前位置不是数字&#xff0c;证明连续数字串已经断开&#xff0c;此时需要记录最…

ROC及曲线面积汇总学习

目录 ROC基础 生成模拟数据 率的计算 R语言计算测试 ROCR&#xff1a; pROC ROC绘制 单个ROC 两个ROC Logistic回归的ROC曲线 timeROC ROC基础 ROC曲线的横坐标是假阳性率&#xff0c;纵坐标是真阳性率&#xff0c;需要的结果是这个率表示疾病阳性的率&#xff08;…

QT基础开发笔记

用VS 写QT &#xff0c;设置exe图标的方法&#xff1a; 选定工程--》右键--》添加---》资源--》 QString 字符串用法总结说明 Qt QString 增、删、改、查、格式化等常用方法总结_qstring 格式化-CSDN博客 总结来说&#xff1a; QString 的 remove有两种用法&#xff0c;&am…

如何成为一名前端组长?

我认为要管理好前端团队&#xff1a; 本质上&#xff1a;让团队少走弯路&#xff0c;并引领团队走在正确的道路上。 理念上&#xff1a;让团队高效工作、快乐工作。 实施上&#xff1a;要想尽办法给团队、给成员赋能。 个人角度&#xff1a; 角色转变&#xff0c;开发人员 -&g…

【JavaSE】:接口(一)

接口 一.什么是接口二.语法规则三.接口的使用四.实现多个接口五.接口的继承 final关键字 inal修饰的变量&#xff0c;这个变量是不可修改的。final修饰后的方法&#xff0c;禁止子类继承的时候重写方法。final修饰后的类&#xff0c;是禁止被继承的。 super关键字 如果父类(超类…

Spring Boot 实现 PDF 水印,实战来了!

简介 PDF&#xff08;Portable Document Format&#xff0c;便携式文档格式&#xff09;是一种流行的文件格式&#xff0c;它可以在多个操作系统和应用程序中进行查看和打印。在某些情况下&#xff0c;我们需要对 PDF 文件添加水印&#xff0c;以使其更具有辨识度或者保护其版…