Go 1.18泛型的局限性初探
前言
Go 1.18 版本之后正式引入泛型,它被称作类型参数(type parameters),本文初步介绍 Go 中泛型的使用。长期以来 go 都没有泛型的概念,只有接口 interface 偶尔类似的充当泛型的作用,然而接口终究无法满足一些基本的泛型需求,比如这篇文章里,我们会尝试用 Go 的泛型循序渐进地实现一些常见的函数式特性,从而探索 Go 泛型的优势和不足。
Go 1.18
在 Go1.18 可以通过如下命令安装体验:
go install golang.org/dl/go1.18@latest
go1.18 download
例1: 泛型版本的求和函数
import (
"golang.org/x/exp/constraints"
)
func Sum[T constraints.Integer](values ...T) T {
var sum T
for _, v := range values {
sum += v
}
return sum
}
constraints 原本是放在标准库的包,但是近期被移除了,改到了 x/exp 中,参见 #50792
这个版本实现了对任意多个同类型的整数求和。Sum 后面的中括号 [] 内就是定义类型参数的地方,其中 T 为类型参数名,constraints.Integer 是对该类型参数的约束,即 T 应该满足的条件,在这里我们要求 T 是一个整数。剩下的代码就和普通没有泛型的代码一致了,只不过后面 T 可以当作一个类型来使用。
泛型语法
函数名后可以附带一个方括号,包含了该函数涉及的类型参数(Type Paramters)的列表:
func F[T any](p T) { ... }这些类型参数可以在函数参数和函数体中(作为类型)被使用
自定义类型也可以有类型参数列表:
type M[T any] []T每个类型参数对应一个类型约束,上述的 any 就是预定义的匹配任意类型的约束
类型约束在语法上以 interface 的形式存在,在 interface 中嵌入类型 T 可以表示这个类型必须是 T:
type Integer1 interface {
int
}
- 嵌入单个类型意义不大,我们可以用 | 来描述类型的 union:
type Integer2 interface {
int | int8 | int16 | int32 | int64
}
- ~T 语法可以表示该类型的「基础类型」是 T,比如说我们的自定义类型 type MyInt int 不满足上述的 Integer1 约束,但满足以下的约束:
type Integer3 interface {
~int
}
高阶函数实例
filter 操作是高阶函数的经典应用,它接受一个函数 f(func (T) bool)和一个线性表 l([] T),对 l 中的每个元素应用函数 f,如结果为 true,则将该元素加入新的线性表里,否则丢弃该元素,最后返回新的线性表。
func Filter[T any](f func(T) bool, src []T) []T {
var dst []T
for _, v := range src {
if f(v) {
dst = append(dst, v)
}
}
return dst
}
func main() {
src := []int{-2, -1, -0, 1, 2}
dst := Filter(func(v int) bool { return v >= 0 }, src)
fmt.Println(dst)
}
// Output:
// [0 1 2]
让人开心的改变 : )
实现一个三元操作
众所周知Go语言不支持三元运算符操作,现在有了泛型,让我们来模拟一个:
// IFF if yes return a else b
func IFF[T any](yes bool, a, b T) T {
if yes {
return a
}
return b
}
// IFN if yes return func, a() else b().
func IFN[T any](yes bool, a, b func() T) T {
if yes {
return a()
}
return b()
}
func main() {
a := -1
assert.Equal(t, utils.IFF(a > 0, a, 0), 0)
assert.Equal(t, utils.IFN(a > 0, func() int { return a }, func() int { return 0 }), 0)
}
令人沮丧
泛型类型系统的不足
众多函数式特性的实现依赖于一个强大类型系统,Go 的类型系统显然不足以胜任, 在 Go 语言中引入泛型之后,类型系统有哪些水土不服的地方。
编译期类型判断
当我们在写一段泛型代码里的时候,有时候会需要根据 T 实际上的类型决定接下来的流程,可 Go 的完全没有提供在编译期操作类型的能力。运行期的 workaround 当然有,怎么做呢:将 T 转化为 interface{},然后做一次 type assertion, 比如我想实现一个通用的字符串类型到数字类型的转换函数:
import "strconv"
type Number interface {
int | int32 | int64 | uint32 | uint64 | float64
}
func Str2Number[N Number](strNumber string) (N, error) {
var num N
switch (interface{})(num).(type) {
case int:
cn, err := strconv.Atoi(strNumber)
return N(cn), err
case int32:
cn, err := strconv.ParseInt(strNumber, 10, 32)
return N(cn), err
case int64:
cn, err := strconv.ParseInt(strNumber, 10, 64)
return N(cn), err
case uint32:
cn, err := strconv.ParseUint(strNumber, 10, 32)
return N(cn), err
case uint64:
cn, err := strconv.ParseUint(strNumber, 10, 64)
return N(cn), err
case float64:
cn, err := strconv.ParseFloat(strNumber, 64)
return N(cn), err
}
return 0, nil
}
无法辨认「基础类型」
在类型约束中可以用 ~T 的语法约束所有 基础类型为 T 的类型,这是 Go 在语法层面上首次暴露出「基础类型」的概念,在之前我们只能通过 reflect.(Value).Kind 获取。而在 type assertion 和 type switch 里并没有对应的语法处理「基础类型」:
type Int interface {
~int | ~uint
}
func IsSigned[T Int](n T) {
switch (interface{})(n).(type) {
case int:
fmt.Println("signed")
default:
fmt.Println("unsigned")
}
}
func main() {
type MyInt int
IsSigned(1)
IsSigned(MyInt(1))
}
// Output:
// signed
// unsigned
乍一看很合理,MyInt 确实不是 int。那我们要如何在函数不了解 MyInt 的情况下把它当 int 处理呢, 比较抱歉的是目前在1.18中没办法对这个进行处理。
类型约束不可用于 type assertion
一个直观的想法是单独定义一个 Signed 约束,然后判断 T 是否满足 Signed:
type Signed interface {
~int
}
func IsSigned[T Int](n T) {
if _, ok := (interface{})(n).(Signed); ok {
fmt.Println("signed")
} else {
fmt.Println("unsigned")
}
}
但很可惜,类型约束不能用于 type assertion/switch,编译器报错如下:
interface contains type constraints
尽管让类型约束用于 type assertion 可能会引入额外的问题,但牺牲这个支持让 Go 的类型表达能力大大地打了折扣。
总结
确实可以实现部分函数式特性能以更通用的方式。
灵活度比代码生成更高 ,用法更自然,但细节上的小问题很多。
1.18 的泛型在引入 type paramters 语法之外并没有其他大刀阔斧的改变,导致泛型和这个语言的其他部分显得有些格格不入,也使得泛型的能力受限。 至少在 1.18 里,我们要忍受泛型中存在的种种不一致。
受制于 Go 类型系统的表达能力,我们无法表示复杂的类型约束,自然也无法实现完备的函数式特性。
推广
推广下个人项目,目前也正在使用Go 1.18的特性也踩了很多坑:
YoyoGo is a simple, light and fast , dependency injection based micro-service framework written in Go. Support Nacos ,Consoul ,Etcd ,Eureka ,kubernetes.
https://github.com/yoyofx/yoyogo
Go 1.18泛型的局限性初探的更多相关文章
- [.net 面向对象编程基础] (18) 泛型
[.net 面向对象编程基础] (18) 泛型 上一节我们说到了两种数据类型数组和集合,数组是指包含同一类型的多个元素,集合是指.net中提供数据存储和检索的专用类. 数组使用前需要先指定大小,并且检 ...
- .NET Core CSharp初级篇 1-8泛型、逆变与协变
.NET Core CSharp初级篇 1-8 本节内容为泛型 为什么需要泛型 泛型是一个非常有趣的东西,他的出现对于减少代码复用率有了很大的帮助.比如说遇到两个模块的功能非常相似,只是一个是处理in ...
- go1.18泛型的简单尝试
今天golang终于发布了1.18版本,这个版本最大的一个改变就是加入了泛型.虽然没有在beta版本的时候尝试泛型,但是由于在其他语言的泛型经验,入手泛型不是件难事~ 官方示例 Tutorial: G ...
- 18_集合框架_第18天_集合、Iterator迭代器、增强for循环 、泛型_讲义
今日内容介绍 1.集合 2.Iterator迭代器 3.增强for循环 4.泛型 01集合使用的回顾 *A:集合使用的回顾 *a.ArrayList集合存储5个int类型元素 public stati ...
- 【Java心得总结三】Java泛型上——初识泛型
一.函数参数与泛型比较 泛型(generics),从字面的意思理解就是泛化的类型,即参数化类型.泛型的作用是什么,这里与函数参数做一个比较: 无参数的函数: public int[] newIntAr ...
- Java 泛型总结
1. 泛型类 class Gen<T> { private T t; public T get(){ return t; } public void set(T argt){ t = ar ...
- Z从壹开始前后端分离【 .NET Core2.2/3.0 +Vue2.0 】框架之九 || 依赖注入IoC学习 + AOP界面编程初探
本文梯子 本文3.0版本文章 更新 代码已上传Github+Gitee,文末有地址 零.今天完成的绿色部分 一.依赖注入的理解和思考 二.常见的IoC框架有哪些 1.Autofac+原生 2.三种注入 ...
- [.net 面向对象程序设计进阶] (18) 多线程(Multithreading)(三) 利用多线程提高程序性能(下)
[.net 面向对象程序设计进阶] (18) 多线程(Multithreading)(二) 利用多线程提高程序性能(下) 本节导读: 上节说了线程同步中使用线程锁和线程通知的方式来处理资源共享问题,这 ...
- Java笔记--泛型总结与详解
泛型简介: 在泛型没有出来之前,编写存储对象的数据结构是很不方便的.如果要针对每类型的对象写一个数据结构, 则当需要将其应用到其他对象上时,还需要重写这个数据结构.如果使用了Object类型, ...
随机推荐
- LGP3702题解
很容易想到容斥,计算不包含质数的方案数和总方案数. 设 \(f[n][i]\) 表示长度为 \(n\) 的序列,每个元素的和对 \(p\) 取模的结果. 容易有 \(f[n][i]=\sum_{j=0 ...
- WebGL 的 Hello World
本文整理自 div 侠于 凹凸 2022 年技术分享,简单介绍了 WebGL 画一个基础图形的流程,希望你了解之后,在使用 3d 渲染库的时候可以少点迷糊. 四种常用的页面绘图工具 关于h5页面的图形 ...
- SpringMVC实现文件上传功能
文件上传 文件上传要求form表单的请求方式必须为post,并且添加属性enctype="multipart/form-data" SpringMVC中将上传的文件封装到Multi ...
- STM32注意事项
1. STM32 USB可配置在全速模式,时钟频率需为48MHz,且精度较高,无法使用芯片内部高速时钟实现(内部时钟精度一般为1%,但USB时钟需要0.1%) 2. 使用重映射功能时,需注意开启AFI ...
- 浅析MySQL恶意服务器读取文件原理
前言 注:本文不涉及对MySQL协议报文研究,仅讲解原理,并且做部分演示. 搭建MySQL恶意服务器读取文件这件事,虽然直接利用门槛较高,但是由于在网上看到了一种比较新颖的利用方式(利用社会工程学引诱 ...
- KestrelServer详解[3]: 自定义一个迷你版的KestrelServer
和所有的服务器一样,KestrelServer最终需要解决的是网络传输的问题.在<网络连接的创建>,我们介绍了KestrelServer如何利用连接接听器的建立网络连接,并再次基础上演示了 ...
- 一张自增表里面总共有 7 条数据,删除了最后 2 条数据,重启 MySQL 数据库,又插入了一条数据,此时 id 是几?如何获取当前数据库版本?
一张自增表里面总共有 7 条数据,删除了最后 2 条数据,重启 MySQL 数据库,又插入了一条数据,此时 id 是几? 一般情况下,我们创建的表的类型是InnoDB,如果新增一条记录(不重启mysq ...
- elasticsearch 是如何实现 master 选举的 ?
想了解 ES 集群的底层原理,不再只关注业务层面了. 前置前提: 1.只有候选主节点(master:true)的节点才能成为主节点. 2.最小主节点数(min_master_nodes)的目的是防止脑 ...
- 解释 Spring 支持的几种 bean 的作用域?
Spring 框架支持以下五种 bean 的作用域:singleton : bean 在每个 Spring ioc 容器中只有一个实例.prototype:一个 bean 的定义可以有多个实例.req ...
- IIS MVC 发布错误 403.14-Forbidden Web 服务器被配置为不列出此目录的内容
转:http://blog.csdn.net/csethcrm/article/details/37820135 IIS MVC 发布错误 403.14-Forbidden Web 服务器被配置为不 ...