云原生系列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语言&云原生自我提升系列,欢迎关注后续文章。
相关文章:
云原生系列Go语言篇-泛型Part 2
类型推导和泛型 就像在使用:时支持类型推导一样,在调用泛型函数时Go同样支持类型推导。可在上面对Map、Filter和Reduce调用中看出。有些场景无法进行类型推导(如类型参数仅用作返回值)。这时,必…...

借助ETL快速查询金蝶云星空表单信息
随着数字化转型的加速,企业信息化程度越来越高,大量的数据产生并存储在云端,需要进行有效的数据管理和查询。金蝶云星空是金蝶云旗下的一款云ERP产品,为企业提供了完整的业务流程和数据管理功能,因此需要进行有效的数据…...
基于深度学习的驾驶员状态监测预警系统(正文)
摘 要 近年来驾驶员因疲劳驾驶而造成的交通事故逐年增多,驾驶员的驾驶状态对道路和人身安全产生重大影响,因此做好驾驶员驾驶状态的管理及预警是非常有必要的。 随着深度学习在目标检测算法应用的不断深入,YOLOv5等目标检测算法也相继具有了广…...
读书笔记之《价值》张磊
读书笔记之《价值》张磊 自序 这是一条长期主义之路 长期主义——把时间和信念投入能够长期产生价值的事情中,尽力学习最有效率的思维方式和行为标准,遵循第一性原理,永远探求真理。 真正的投资,有且只有一条标准,那…...

【shell】文本三剑客之sed详解
目录 一、sed简介(行编辑器) 二、基本用法 三、sed脚本格式(匹配地址 脚本命令) 1、不给地址,那么就是针对全文处理 2、单地址,表示#,指定的行,$表示最后一行,/pattt…...

Centos7 制作Openssh9.5 RPM包
Centos7 制作Openssh9.5 RPM包 最近都在升级Openssh版本到9.3.在博客里也放了openssh 9.5的rpm包. 详见:https://blog.csdn.net/qq_29974229/article/details/133878576 但还是有小伙伴不停追问这个rpm包是怎么做的,怕下载别人的rpm包里被加了盐. 于是做了个关于怎么用官方的o…...

C语言--每日选择题--Day30
第一题 1. i 5,j 7,i | j 等于多少? A:1 B:3 C:5 D:7 答案及解析 D |这个是按位或运算符,两个数的二进制位,有1为1,同0为0; i的二进…...
LeetCode 274. H指数——排序
274. H 指数 给你一个整数数组 citations ,其中 citations[i] 表示研究者的第 i 篇论文被引用的次数。计算并返回该研究者的 h 指数。 根据维基百科上 h 指数的定义:h 代表“高引用次数” ,一名科研人员的 h 指数 是指他(她&…...
【洛谷 B2038】奇偶 ASCII 值判断 题解(顺序结构+取余)
奇偶 ASCII 值判断 题目描述 任意输入一个字符,判断其 ASCII 是否是奇数,若是,输出 YES,否则,输出 NO 。 例如,字符 A 的 ASCII 值是 65,则输出 YES,若输入字符 B(ASCII 值是 66…...
Ubuntu 20.4 源代码方式安装 cdo(笔记)
目录 动机安装过程python 调用cdo 动机 我找到的处理 era5-land 代码在需要用到 cdo,但是 sudo apt-get install cdo 总是出现 abort (core dump) 等问题,所以放弃这种安装方式,不走捷径,安装源代码,也就是 cdo-x.x.x…...

电子学会C/C++编程等级考试2022年12月(三级)真题解析
C/C++等级考试(1~8级)全部真题・点这里 第1题:鸡兔同笼 一个笼子里面关了鸡和兔子(鸡有2只脚,兔子有4只脚,没有例外)。已经知道了笼子里面脚的总数a,问笼子里面至少有多少只动物,至多有多少只动物。 时间限制:1000 内存限制:65536输入 一行,一个正整数a (a < 327…...

二叉树的最近公共祖先(C++实现)
二叉树的最近公共祖先 题目思路代码(详细注释) 题目 二叉树的最近公共祖先 思路 我们可以通过两个栈来实现 实现一个FindPath函数,用来查找从根节点到目标节点的路径(路径可以用栈来保存) 路径保存好后,…...
【conda】容易遗忘的命令使用总结
1. 在空conda虚拟环境中安装python 退出到base环境 conda activate base 执行命令 conda install -n 空环境名 python版本名 例如: conda install -n test python3.10 2. 无需确认直接创建环境 在末尾加上-y,例如: conda create -n tes…...

蓝桥杯第一天-----时间显示
文章目录 前言一、题目描述二、测试用例三、题目分析四、具体代码实现总结 前言 本章中将相信介绍蓝桥杯中关于时间显示的题目。 链接:https://www.lanqiao.cn/problems/1452/learning/ 一、题目描述 二、测试用例 三、题目分析 1.输入的时间为毫秒,毫…...

多文件夹图片预处理:清除空值、重置大小、分割训练集
→ 清理空值 防止出现cannot identify image file 参考Python数据清洗----删除读取失败图片__简单版_python用pil读取图片出错删除掉-CSDN博客 import os import shutil import warnings import cv2 import iofrom PIL import Image warnings.filterwarnings("error&qu…...
【Java】集合 之 使用 Map
为什么使用Map 我们知道,List是一种顺序列表,如果有一个存储学生Student实例的List,要在List中根据name查找某个指定的Student的分数,应该怎么办? 最简单的方法是遍历List并判断name是否相等,然后返回指定…...

第二证券:股票几点到几点开盘?
作为股民或许投资者,我们都知道股票是每天都有开盘和收盘时间的。但是,关于股票的开盘时间,很多人并不是很清楚,特别是初学者。在本文中,我们将从多个视点分析股票开盘时间,并为大家供给一些有用的信息。 …...

goweb入门教程
本文是作者自己学习goweb时写的笔记,分享给大家,希望能有些帮助 前言: 关于web:本质 web中最重要的就是浏览器和服务器的request(请求)和response(响应); 一个请求对应一个响应。 一个请求对应一个响应&…...

量子计算:探索未来的计算技术
量子计算:探索未来的计算技术 引言 在过去的几十年里,我们见证了计算机技术从简单的计算和存储发展到复杂的数据处理和人工智能的飞速进步。然而,随着我们进一步探索科技的前沿,传统的计算方法开始显示出其局限性。在这种情况下,量子计算——一种基于量子力学原理的新型计…...
HarmonyOS应用开发者基础认证考试题目及答案
一、判断题 Ability是系统调度应用的最小单元,是能够完成一个独立功能的组件。一个应用可以包含一个或多个Ability。 正确(True) 所有使用Component修饰的自定义组件都支持onPageShow,onBackPress和onPageHide生命周期函数。 错误(False) 每调用一次ro…...

关于nvm与node.js
1 安装nvm 安装过程中手动修改 nvm的安装路径, 以及修改 通过nvm安装node后正在使用的node的存放目录【这句话可能难以理解,但接着往下看你就了然了】 2 修改nvm中settings.txt文件配置 nvm安装成功后,通常在该文件中会出现以下配置&…...

什么是库存周转?如何用进销存系统提高库存周转率?
你可能听说过这样一句话: “利润不是赚出来的,是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业,很多企业看着销售不错,账上却没钱、利润也不见了,一翻库存才发现: 一堆卖不动的旧货…...

linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...
论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一)
宇树机器人多姿态起立控制强化学习框架论文解析 论文解读:交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一) 论文解读:交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化…...

Docker 本地安装 mysql 数据库
Docker: Accelerated Container Application Development 下载对应操作系统版本的 docker ;并安装。 基础操作不再赘述。 打开 macOS 终端,开始 docker 安装mysql之旅 第一步 docker search mysql 》〉docker search mysql NAME DE…...

STM32HAL库USART源代码解析及应用
STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...
第7篇:中间件全链路监控与 SQL 性能分析实践
7.1 章节导读 在构建数据库中间件的过程中,可观测性 和 性能分析 是保障系统稳定性与可维护性的核心能力。 特别是在复杂分布式场景中,必须做到: 🔍 追踪每一条 SQL 的生命周期(从入口到数据库执行)&#…...

Chromium 136 编译指南 Windows篇:depot_tools 配置与源码获取(二)
引言 工欲善其事,必先利其器。在完成了 Visual Studio 2022 和 Windows SDK 的安装后,我们即将接触到 Chromium 开发生态中最核心的工具——depot_tools。这个由 Google 精心打造的工具集,就像是连接开发者与 Chromium 庞大代码库的智能桥梁…...
土建施工员考试:建筑施工技术重点知识有哪些?
《管理实务》是土建施工员考试中侧重实操应用与管理能力的科目,核心考查施工组织、质量安全、进度成本等现场管理要点。以下是结合考试大纲与高频考点整理的重点内容,附学习方向和应试技巧: 一、施工组织与进度管理 核心目标: 规…...
《Offer来了:Java面试核心知识点精讲》大纲
文章目录 一、《Offer来了:Java面试核心知识点精讲》的典型大纲框架Java基础并发编程JVM原理数据库与缓存分布式架构系统设计二、《Offer来了:Java面试核心知识点精讲(原理篇)》技术文章大纲核心主题:Java基础原理与面试高频考点Java虚拟机(JVM)原理Java并发编程原理Jav…...