浅谈Go1.18中的泛型编程

目录
  • 前言
  • 以前的Go泛型
  • 泛型是什么
  • Go的泛型
  • 泛型函数
  • 泛型类型
  • 类型集合
  • 和接口的差异
  • 总结

前言

经过这几年的千呼万唤,简洁的Go语言终于在1.18版本迎来泛型编程。作为一门已经有了14年历史的强类型语言,很难相信它到现在才开始有一个正式的泛型。

以前的Go泛型

虽然直到1.18版本才加入泛型,但是在2014年便有相关的讨论要在Go中加入泛型设计。但是由于各种原因没有实现。而之后的接口(interface)的提出,让泛型进一步搁置。但是由于接口的缺陷,最终Go团队还是在1.18的版本中加入了泛型。实际上,这一版本的泛型设计在语言层面和接口非常相似(在实现层面肯定是不一样的,泛型是编译时,接口是运行时),对于他们之间的差异,也会在后面提到。

本文主要讲述1.18beta1版本中的泛型,后续有改动,可能会更改文章。

泛型是什么

在我看来泛型其实用C++的模板一词来描述就非常的准确。在写代码的时候,我们经常需要写很多重复的逻辑,一般这个时候我们就会使用函数来对其进行封装。但是由于Go是一种强类型语言,所以在定义和书写函数的时候需要在调用前标明类型。当然如果这一重复的逻辑只需要固定的类型,这样就足够了,但是很多时候我们需要不同的类型进行类似的逻辑,譬如我们刚刚看到的GIF。对于普通开发人员来说这种情况可能遇到的比较少,但是在一些库开发人员来说,这种情况变得非常的普遍。

泛型程序设计(generic programming)是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。Ada、Delphi、Eiffel、Java、C#、F#、Swift 和 Visual Basic .NET 称之为泛型(generics);ML、Scala 和 Haskell 称之为参数多态(parametric polymorphism);C++ 和 D称之为模板。具有广泛影响的1994年版的《Design Patterns》一书称之为参数化类型(parameterized type)。

其中,C++的模版应该是做的最完善的,不仅支持简单的模板替换,还可以处理一些简单的逻辑,经过不断的迭代,已经形成了一种生成代码的编程方式,因此也叫做模板元编程(Template metaprogramming)。当然由于其和C++编程方式完全不一致,所以可读性非常的差。而在Go的泛型设计中,为了保证泛型的简洁,Go并不支持模版元编程(心塞,还想试试在Go里面往往骚操作呢)。

Go的泛型

接下来就是Go泛型的使用介绍了,Go支持泛型函数和泛型类型。

泛型函数

先来一个最简单的泛型函数

func ink19FirstGen[T any](t T) {
 fmt.Println(t)
}

这是一个非常简单的的函数,就是使用fmt.Println打印输入的参数。相比于以前的函数,多了[T any]部分,这就是Go泛型的参数列表。

参数列表中的参数由两部分组成,参数名和约束,其中T就是参数,any为参数的约束。从表达上来说,和Go语言一贯的风格相似,名在前,类型在后。

在Go语言中,使用接口interface做为类型的约束,其中any = interface{},即为无限制,但是以其说是无限制,倒不如说是完全限制,由于any里面没有定义任何的方法,所以在函数里面也没办法调用t的任何方法。

这里有一个非常重要的问题,就是相比较于C++的模板,Go会在定义函数的时候就对函数进行解析。所以在函数中使用了的方法,一定要在约束的接口中出现。

type ink19Inf interface {
 Test()
}

func ink19FirstGen[T ink19Inf](t T) {
 t.Test()
}

和普通参数类似的,如果是相同的约束,参数类型也支持简化

func ink19FirstGen[T ,T2 ink19Inf](t T, t2 []T2) {
 t.Test()
}

泛型类型

和C++中的模板类类似的,Go里面也有泛型类型,它的定义也很简单

type ink19Vector[T any] []T

结构相比与以前的类型定义多了[T any]部分,这一部分的结构和泛型函数那一部分类似就不多介绍了。

对于泛型类型,Go也可以定义相关的方法,譬如:

func (m *ink19Vector[T]) Push(v T) *ink19Vector[T] {
 *m = append(*m, v)
 return m
}

在泛型结构体中,结构体也可以定义自己的类型的变量,形成链表

type List[T1, T2 any] struct {
 next *List[T1, T2]
 t1 T1
 t2 T2
}

PS:依据提案中的说法,第二行的参数列表应该和定义中的顺序一致,以防止无限递归。但是在1.18beta1版本的实测中,顺序不一致的写法并不会报错。

Go暂时不支持方法的泛型。

类型集合

虽然通过接口限制类型可以满足绝大部分的要求,但是仍然有一些需求满足不了,譬如运算符。假如我们有一个函数,可以传入任意可比较的参数,然后返回较小的那一个。很自然的,我们可以写下如下的代码:

func whoismin[T any](a, b T) T {
  if a < b {
    return a
  }
  return b
}

但是,很遗憾的,由于我们对T的约束是any。所以其实来说,我们没办法对a和b做任何的操作,对比也是。所以在这里,我们会收到报错

invalid operation: cannot compare a < b (operator < not defined on T)

为了解决这一问题,提案中提出了类型集合的概念。

对于一个类型,认为它代表的类型集合就是只包含这个类型的集合,即对于类型M来说,其代表的类型集合为{M}。而对于接口来说,其对应的类型集合是无限的,只要一个类型满足接口的所有方法签名,那么这个类型就是属于这个接口的类型集合中。其实很容易理解类型集合就是那个识别符可以代表的类型的集合。

考虑集合的操作,对于下面这个例子

type ink19Inf1 interface {
 What1()
}

type ink19Inf2 interface {
 What2()
}

type ink19Inf3 interface {
 What1()
 What2()
}

假设ink19Inf1的类型集合为A,ink19Inf2的为B,ink19Inf3的为C。那么很容得到C=A⋂B。即C为A和B的交集。当然只有交集是不行的,后面还有说明实现并集。

为了进一步的说明类型集合,我们先来回忆一下接口的定义,对于之前的接口来说,接口的元素一共有两种:方法签名和其他接口。

type ink19Inf1 interface {
 What()
}

type ink19Inf2 interface {
 ink19Inf1
 It()
}

比如ink19Inf2中的第一个元素就是其他接口,第二个元素是其他签名。但是仅仅只是有这两种元素,对于泛型约束来说是完全不够的。为此,提案中加入了另外三种不同的元素,需要注意的是,如果一个接口加入了这额外三种元素,那么这个接口就不能再作为普通的接口使用了,只能用作泛型。

第一个增加的是类型元素。以前的接口是不能用类型作为接口的,但是在作为约束中可以这样操作。作为元素的时候就是提供了一个只包含自己本身的类型作为元素的类型集合。

第二个是增加了近似约束元素,写法是在类型前面增加~符号,如

type ink19Inf1 interface {
 ~int
}

这一个元素的意义是为接口提供了一个所有以int为底层类型的集合。所以被~修饰的类型也应该是一个底层类型,不然提供的集合就是空集,没有任何意义。具体的区别可以看下面的这个例子。

type ink19Inf3 interface {
 int
}

type ink19Inf4 interface {
 ~int
}

type MyInt int

首先我们定义了两个接口,第一个接口使用的是额外的第一种元素, 因此它的类型集合只包含了int。另一个使用了第二种元素,它的类型集合包含了所有以int为底层类型的类型。然后我们定义了一个MyInt类型,它是以int为底层类型的类型。需要注意的是,在Go中MyInt和int是两种不同的类型。最后我们写两个方法来分别使用两个接口为约束。

func ink19Print1[T ink19Inf3](t T) {
 fmt.Println(t)
}

func ink19Print2[T ink19Inf4](t T) {
 fmt.Println(t)
}

var data MyInt = 1
ink19Print1(data)  // 错误
ink19Print2(data)

第三个元素是联合约束。使用方法如下

type ink19Inf5 interface {
 int | float32 | bool | ~string | ink19Inf3
}

使用方法非常简单,就是将并集的元素一个一个使用|连接就就好了。需要注意的是联合约束的元素只支持类型,近视约束和其他只包含以上三种额外元素的接口(即,不支持包含方法签名的接口)。

回到之前的问题,对于需要使用操作符的情况,有了以上的工具后就可以解决了。

纵观整个Go语言,由于并不支持操作符,所以有操作符(除了==和!=)的其实只有有限的几种类型,譬如:int,float32,string等等。

所以对于需要使用比较运算符的约束的时候,可以使用如下的一个约束接口:

type Ordered interface {
 ~int | ~int8 | ~int16 | ~int32 | ~int64 |
  ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
  ~float32 | ~float64 |
  ~string
}

为了方便使用,Go标准库里面提供了一个constraints来提供相关的约束。

上面提到,对于除了==和!=以外的操作符可以通过对所有的类型进行枚举来实现。但是对于这两个操作符,用户自定义的类型也会有这两个操作符,没办法枚举实现。官方给出的方法是通过使用一个一个内建的约束comparable来完成操作。譬如

func IsSame[T comparable](a T, b T) bool {
 return a == b
}

和接口的差异

由于本人对于Go的接口使用并不多,所以如果有不足的地方请及时指正。

  • 实现方法上,泛型是编译时,接口是运行时;
  • 可以实现操作符的约束;
  • 返回的参数可以是特定的类型,而接口只能返回固定的接口类型;
  • 相比较于接口,泛型的约束可以有更多的操作。

总结

以上就是Go语言泛型的使用。总的来说,比较完整,实现了大部分的功能,相比于接口,有一定的差异。从体验上来说有较高的提升,但是其缺点也非常的多。首先是其后面提出的三种元素,它将接口和类型限制隔离开了,这是一个特别奇葩的操作,感觉不符合Go语言的简洁实现。添加的三种元素中,我们主要来看第三种,联合。代码在分析的时候会对每一个元素测试,看看能不能通过编译。所以从集合的角度上来看,如果我们把一个类型可以进行的操作可做是一个集合,那么这一个联合就是在一个限定的类型集合里面(枚举出的)对每一个类型的操作集合进行一个交集操作。回到原来,其实出现这个语法特性的最大原因就是Go语言不支持操作符重载,所以没办法对操作符进行枚举,那为什么不直接在这个版本实现操作符重载呢?或者直接不考虑这一部分,让传入的结构体只能使用方法,不能使用操作符。并且,即使加入了这三种元素,还是有两种操作符!=和==无法使用现在有的实现,只能使用一个内建的符号来代表这一类的方法,个人感觉非常丑陋。

到此这篇关于浅谈Go1.18中的泛型编程的文章就介绍到这了,更多相关Go  泛型编程内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Goland支持泛型了(上机实操)

    事情出因 一大早上被一篇公众号的推文震惊了,Goland竟然支持go的泛型了.据我所知: Go的泛型不是还在设计草图吗?最乐观估计也要2021年8月份.你说Go语言现在都没开发好泛型,你支持这个特性有什么用呢? 带着好奇心点开推文,没发现对泛型的说明,只看到一个Goland对泛型的使用的说明链接: https://blog.jetbrains.com/go/2020/11/24/experimenting-with-go-type-parameters-generics-in-goland/,心

  • Golang 使用接口实现泛型的方法示例

    在C/C++中我们可以使用泛型的方法使代码得以重复使用,最常见例如stl functions:vector<int> vint or vector<float> vfloat等.这篇文章将使用interface{...}接口使Golang实现泛型. interface{...}是实现泛型的基础.如一个数组元素类型是interface{...}的话,那么实现了该接口的实体都可以被放置入数组中.注意其中并不一定必须是空接口(简单类型我们可以通过把他转化为自定义类型后实现接口).为什么i

  • GoLand 2020.3 正式发布有不少新功能(支持泛型)

    这是 2020 年第 3 个版本,也是最后一个版本.在 GoLand 2020.3 中,您可以探索 goroutines dumps,运行并导航到单个表测试(table tests),并从对 Testify 测试框架的扩展支持中获得更多信息.你还将发现许多新的代码编辑功能,包括对 time 包的支持,更智能的处理包方法,UI 改进,用于 Web 开发和使用数据库的各种新功能以及用于协作开发和结对编程的新服务.具体看看有哪些新特性. 01 调试器改进 Dump Goroutines 调试器中新的转

  • C++算法与泛型算法(algorithm、numeric)

    本文包括的算法有: 只读算法:find().count().accumulate().equal() 写算法:fill().fill_n().back_inserter().copy().copy_backward().replace().replace_copy().next_permutation().prev_permutation() 重排元素算法:sort().stable_sort().unique() 一.算法简介 大多数算法在头文件algorithm中.标准库还在头文件numer

  • Go 泛型和非泛型代码详解

    目录 1. 开启泛型 2.无泛型代码和泛型代码 2.1. AddSlice 2.2. 带方法的约束 StringConstraint 1. 开启泛型 在 Go1.17 版本中,可以通过: export GOFLAGS="-gcflags=-G=3" 或者在编译运行程序时加上: go run -gcflags=-G=3 main.go 2.无泛型代码和泛型代码 2.1. AddSlice 首先看现在没有泛型的代码: package main ​ import ( "fmt&qu

  • 浅谈Go1.18中的泛型编程

    目录 前言 以前的Go泛型 泛型是什么 Go的泛型 泛型函数 泛型类型 类型集合 和接口的差异 总结 前言 经过这几年的千呼万唤,简洁的Go语言终于在1.18版本迎来泛型编程.作为一门已经有了14年历史的强类型语言,很难相信它到现在才开始有一个正式的泛型. 以前的Go泛型 虽然直到1.18版本才加入泛型,但是在2014年便有相关的讨论要在Go中加入泛型设计.但是由于各种原因没有实现.而之后的接口(interface)的提出,让泛型进一步搁置.但是由于接口的缺陷,最终Go团队还是在1.18的版本中

  • 浅谈Go语言中的结构体struct & 接口Interface & 反射

    结构体struct struct 用来自定义复杂数据结构,可以包含多个字段(属性),可以嵌套: go中的struct类型理解为类,可以定义方法,和函数定义有些许区别: struct类型是值类型. struct定义 type User struct { Name string Age int32 mess string } var user User var user1 *User = &User{} var user2 *User = new(User) struct使用 下面示例中user1和

  • 浅谈iOS应用中的相关正则及验证

    1.手机号码的验证正则 正则表达式: ^((13[0-9])|(15[^4,\\D])|(18[0,0-9]))\\d{8}$ 详细解释 解释: •^...$: ^:开始 $:结束 中间为要处理的字串 •(13[0-9]): 以13开头接下来一位为0-9之间的数 13 : 以13开头 [0-9]:分割语法,13后面是0-9之间的数 •| : 或(or), 将前后两个匹配条件进行or运算 • (15[^4\\D]) : 以15开头接下来一位是除4之外的0-9数字 15 : 以15开头 [^4\\D

  • 浅谈mysql explain中key_len的计算方法

    mysql的explain命令可以分析sql的性能,其中有一项是key_len(索引的长度)的统计.本文将分析mysql explain中key_len的计算方法. 1.创建测试表及数据 CREATE TABLE `member` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(20) DEFAULT NULL, `age` tinyint(3) unsigned DEFAULT NULL, PRIMARY KEY

  • 浅谈numpy数组中冒号和负号的含义

    在实际使用numpy时,我们常常会使用numpy数组的-1维度和":"用以调用numpy数组中的元素.也经常因为数组的维度而感到困惑. 总体来说,":"用以表示当前维度的所有子模块 "-1"用以表示当前维度所有子模块最后一个,"负号用以表示从后往前数的元素" 测试代码 import numpy as np b = np.arange(start=0, stop=24, dtype=int) print('b.shape', b

  • 浅谈XML Schema中的elementFormDefault属性

    elementFormDefault属性与命名空间相关,其值可设置为qualified或unqualified 如果设置为qualified: 在XML文档中使用局部元素时,必须使用限定短名作为前缀 sean.xsd: <?xml version="1.0" encoding="UTF-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:sean=&

  • 浅谈iOS开发中static变量的三大作用

    (1)先来介绍它的第一条也是最重要的一条:隐藏 当我们同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性.为理解这句话,我举例来说明.我们要同时编译两个源文件,一个是a.c,另一个是main.c. 下面是a.c的内容 char a = 'A'; // global variable void msg() { printf("Hello\n"); } 下面是main.c的内容 int main(void) { extern char a; // extern v

  • 浅谈pyhton学习中出现的各种问题(新手必看)

    目前比较杂乱无章,后续还会有一些添加补充 1.标识符 (1)标识符是区分大小写的. (2)标示符以字母或下划线开头,可包括字母,下划线和数字. (3)以下划线开头的标识符是有特殊意义的. 2.参数前加星号(*)的意义 面对实际情况时无法提前得知要传入的参数的个数,因此在参数前加星号从而允许函数接受任意多的参数,情况如下: (1)参数前加一个星号(*),传入的参数存储为元组的形式: (2)参数前加两个星号(*),传入的参数存储为字典的形式,并且调用时采用例如'a=1,b=2,c=3'的形式. 3.

  • 浅谈c语言中一种典型的排列组合算法

    c语言中的全排列算法和组合数算法在实际问题中应用非常之广,但算法有许许多多,而我个人认为方法不必记太多,最好只记熟一种即可,一招鲜亦可吃遍天 全排列: #include<stdio.h> void swap(int *p1,int *p2) { int t=*p1; *p1=*p2; *p2=t; } void permutation(int a[],int index,int size) { if(index==size) { for(int i=0;i<size;i++) print

  • 浅谈spring容器中bean的初始化

    当我们在spring容器中添加一个bean时,如果没有指明它的scope属性,则默认是singleton,也就是单例的. 例如先声明一个bean: public class People { private String name; private String sex; public String getName() { return name; } public void setName(String name) { this.name = name; } public String get

随机推荐