go基础-泛型
概述
在强类型变成语言中,类型是确定不可变,如函数入参是确定类型、链表元素是确定类型,这极大限制了函数功能。也有些解决方案,为每种类型都实现一版函数,会导致大量重复代码;使用类型转换,使用特殊形参(如Object、any),在函数内部判断并转换类型后再执行逻辑,导致大量类型转换的代码,结构混乱,Java 未支持泛型之前就使用这个套路。最优解决方案是泛型,即类型参数化,编写函数暂时不确定类型,使用占位符代替,调用时候类型也是通过参数传递进去,替换占位符。主流强类型语言都支持泛型,Java、C++、C#等。
千呼万唤始出来,1.18版本正式增加泛型支持,但是体感貌似一般,使用场景并没有那么广泛,特别是泛型约束能力,相比Java、C++弱了很多。更多是对函数式编程的支持,如slice、map、func等,在面向对象编程支持不够。不确定后期是否会继续演变,Go对兼容性保持还算可以,后续演变也不用过分担忧。
基本使用
Go 语言不是主流的面向对象,泛型支持上也有所区别,如Java一切皆为对象,只要确定对象泛型即可统一的泛型模式。Go的每种类型都有区别,可简单分为两类,类型泛型:切片泛型、哈希泛型、结构体泛型、基础类型泛型等;函数泛型:函数泛型、方法泛型等。另外泛型语法不是主流的<>尖括号,而使用的[]中括号,这不重要,思路都是相似的
类型泛型,风格基本一致,使用type定义别名,名称后面紧跟泛型定义,然后是基础类型。
type MyGen[T any] int
函数泛型,语法也比较类似,函数名称后面紧跟泛型定义
func MyFun[T any](x T, n int) {
...
}
切片泛型
定义切片泛型。使用type定了名称为genSlice的新类型,底层类型是个切片。紧跟在名称后面的[T any]就是泛型定义,T表示泛型占位符,any表示泛型约束,可以看到切片的元素类型也使用了T占位符
type genSlice[T any] []T
any是标准库提供的特殊类型,表示任意类型,其本质是个空接口 type any = interface{},类似Java中的Object类型。更多泛型约束信息独立章节介绍
使用泛型切片,与普通切片几乎一模一样。唯一区别,创建时需要传递参数类型,这就是所谓的类型参数化。
arr := make(genSlice[int], 0)
arr2 := genSlice[float64]{1.1, 1.2, 2.1}
var arr3 = genSlice[string]{"a", "b", "cd"}
创建了三个切片,相比普通切片多了个参数,使用大括号传递的类型参数,在底层会替换占位符T,三个切片的元素类型分别是int、float64、string。仅此而已,再无其他区别。访问切片
arr = append(arr, 10)
arr = append(arr, 20)
for _, i := range arr {
fmt.Printf("%T=%v\n", i, i)
}
输出如下
int=10
int=20
哈希泛型
也就是map切片,map有两个参数,可以定一个或两个泛型,其他方面与切片泛型几乎一样
type genMap1[V any] map[string]V // 一个泛型, key 是字符串, value是泛型, 创建时传入
type genMap2[K string|int, V any] map[K]V // 两个泛型, key 是泛型且限定为字符串或整数, value是泛型且没有限定
创建实例
m1 := genMap1[int]{"k1": 1}
m2 := genMap2[string, string]{"k1": "v1"}
管道泛型
泛型定义
type genChan[T any] chan T
创建实例
chan1 := make(genChan[int])
var chan2 genChan[string] // nil, 引用类型需要初始化
基础类型泛型
相比容器类型有一些区别。基础类型不能只有类型形参,如下缺乏原始类型。
type CommonType[T int|string|float32] T // err
正确定义,给出原始类型int
type CommonType[T int | string | float32] int
由于原始类型是int,导致泛型约束无效,原始类型限定更强
var v1 CommonType[int] = 10 // ok
var v2 CommonType[string] = "test" // err
函数泛型
定义泛型函数
func add[T int|string](x, y T) T {
return x+y
}
调用泛型函数,一样的套路,调用时多个传递一个参数,表示类型
n := add[int](1, 2)
fmt.Println(n)
注意,匿名函数不支持泛型,匿名函数不能定义类型参数,以下案例编译不通过
func[T int | float32](a, b T) T { // err
return a+b
}
闭包函数则可以使用泛型,闭包函数是对外部类型的应用,而不是类型参数。
func MyFunc[T int | float32]() func(a, b T) T {
return func(a, b T) T {
return a + b
}
}
结构体泛型
定义泛型结构体
type genStruct[T int | string] struct {
Name string
Data T
}
创建实例
p1 := genStruct[int]{"xx", 10}
p2 := new(genStruct[string])
注意,匿名结构体目前还不支持泛型,以下代码编译不通过。
struct[T int|string] {
caseName string
got T
want T
}[int]{
caseName: "test OK",
got: 100,
want: 100,
}
需要特别注意的是结构体方法,泛型结构体并不代表方法泛型。判断一个方法是否支持泛型,要看是否有定义类型参数,这与Java特性一样。如下setName就是普通方法,因为并没有定义泛型参数。名称前面的(p *person[T]) 是类型绑定,而不是类型参数。setName虽然是普通方法,但是内部可以使用结构体中定义的泛型
type person[T string | int] struct {
name T
}
func (p *person[T]) setName(name T) T { // 普通方法
p.name = name
return p.name
}
截止目前go 还不支持泛型方法(1.9版本),如果后续支持,定义方式应该如下,增加了类型参数E
func (p *person[T]) setName[E string](name T, surname E) T {
...
}
泛型也支持相互套用
type WowStruct[T int | float32, S []T] struct {
Data S // S是T类型切片
MaxValue T
MinValue T
}
看起来有点复杂,只要记住一点:任何泛型类型都必须传入类型实参实例化才可以使用。
var ws WowStruct[int, []int]
接口泛型
接口泛型最为特殊,因为方法不支持泛型,接口定义却支持泛型,看似好像两者冲突了。其实用了个鸡贼的方式实现,绕开了问题根本,本质是类型转换。接口泛型定义如下,看起来没有特别之处
type DataProcessor[T any] interface {
Process(oriData T) (newData T)
Save(data T) error
}
实现泛型接口
type CSVProcessor struct {
}
func (c CSVProcessor) Process(oriData string) (newData string) {
return oriData
}
func (c CSVProcessor) Save(oriData string) error {
return nil
}
注意:与实现非泛型接口有区别,结构体的方法签名与泛型接口内方法签名不一样,按照规范两者没有实现关系。讨巧就在这里,给泛型传入类型参数,然后类型转换。
func MyFun(E DataProcessor[string]) { // 形参是泛型接口
println(E.Process("name"))
}
var processor DataProcessor[string] = CSVProcessor{} // 类型转换
MyFun(processor)
包装泛型
以下方式也都是错误定义
// 错误, 它认为你要定义一个存放切片的数组,数组长度由 T 乘以 int 计算得到
type NewType [T * int][]T
//✗ 错误。和上面一样,这里不光*被会认为是乘号,| 还会被认为是按位或操作
type NewType2[T *int|*float64] []T
//✗ 错误
type NewType2 [T (int)] []T
解决方法也都一样,使用接口包装。在“接口类型约束”章节详细说明
type NewType interface {
*int | *float64
}
泛型约束
除了类型参数化,泛型有个重要作用:类型约束。简单理解就是限定泛型占位符的范围,比如类型参数只能是数组、字符串、数组、接口、函数等。Java的泛型约束可支持上边界、下边界、或类型等;Go泛型约束弱一些,没有上边界、下边界。主要原因还是Go的特殊面向对象,没有Java完整的类型系统。
基本类型约束
泛型约束为int类型
func MyFunc[T int]() T {
...
}
这种单个基础数据类型约束没有意义,直接使用基础类型效果一样。更多时候会约束一个范围,如下约束范围是所有的整数类型。
func MyFunc[T int|int8|int16|int32|int64|float32|float64]() T {
...
}
接口类型约束
接口是泛型约束中使用最广泛的类型,这与Java一致。面向接口编程,面向接口约束。
在基本类型约束中有个案例
func MyFunc[T int|int8|int16|int32|int64|float32|float64]() T {
...
}
这种写法繁琐且不方便复用,如果其他函数有相同的约束需求,需要再写一遍,为此Go 提供特殊的接口定义,专门用于泛型约束。
// 整型
type Int interface {
int|int8|int16|int32|int64|float32|float64
}
// 无符号整型
type Uint interface {
uint | uint8 | uint16 | uint32
}
// 浮点
type Float interface {
float32 | float64
}
接口约束之间也继续组合,约束为所有数值类型
type Number interface {
Int | Uint | Float
}
使用接口约束,与使用其他约束一样
func MyFunc[T Number]() T {
...
}
上面接口定义各类型使用|符号分割,是并集关系,也支持交集关系
// 并集
type AllInt interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint32
}
// 并集
type Uint interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}
// 交集, 接口A代表的类型集是 AllInt 和 Uint 的交集
type A interface {
AllInt
Uint
}
// 交集, 接口B代表的类型集是 AllInt 和 ~int 的交集
type B interface {
AllInt
~int
}
空集合, int 和 float32 没有相交的类型,所以接口 Bad 代表的类型集为空,没有任何类型可以匹配,这与any空接口刚好相反,后者可表示任意类型。
type Bad interface {
int
float32
}
注意:并集和交集只能用于基本类型,如下定义异常
type A interface{
funcA()
}
type B interface{
funcB()
}
type C interface{
A | B // err
}
穿透别名,项目中经常使用type定义别名,泛型约束无法穿透别名,如下案例编译不通过
type MyInt int
func MyFunc[T MyInt]() T {
...
}
MyFunc[int]() // err,int != MyInt
Go专门为此提供了特殊语法,在类型前面增加~符号,可穿透别名约束到底层类型。
type MyInt interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
func MyFunc[T MyInt]() T {
...
}
MyFunc[int]() // ok
MyFunc[MyInt]() // ok
使用~符号穿透别名约束有一些限制条件:~后面的类型不能为接口,~后面的类型必须为基本类型。
type MyInt int
type _ interface {
~int // ok
~[]byte // ok
~MyInt // err,~后的类型必须为基本类型
~error // err,~后的类型不能为接口
}
接口新语法有更深层次的意义,影响深远,在1.18版本之前接口是一堆函数声明的集合,称为方法集(method set)。如下ReadWriter 定义了一个接口 ,包含了 Read() 和 Write() 两个方法。所有同时定义了这两种方法的类型被视为实现了这一接口。
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
但是如果换一个角度来重新思考,会发现还能这样理解:可以把 ReaderWriter 接口看成代表一个“类型的集合”,所有实现了 Read() Writer() 这两个方法的类型都在接口代表的类型集合当中。从 1.18 开始接口的定义就从 **方法集(method set)** 变为了 **类型集(type set)**。
以此配合接口增加了新技能,接口不仅可以定义方法声明,也定义底层类型。来个复杂点的案例,接口类型 ReadWriter 代表了一个类型集合,所有以 string 或 []rune 为底层类型,并且实现了 Read()和Write() 这两个方法的类型都在 ReadWriter 代表的类型集当中
type ReadWriter interface {
~string | ~[]rune
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
从此接口可分为两种类型:Basic interface基本接口,接口内仅包含方法声明,就是Go1.18 之前的接口。General interface一般接口,接口内有定义原始类型,以及方法声明。以下两个接口都是一般接口,因为内部都有定义原生类型
type Uint interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 // 原始类型
}
type ReadWriter interface {
~string | ~[]rune // 原始类型
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
容器类型约束
常用的约束
func MyFun[T any](arr []T) {
for i := range arr {
log.Println(i)
}
}
以下两种写法和实现的功能其实是差不多的,实例化之后结构体相同
type WowStruct[T int|string] struct {
Name string
Data []T
}
type WowStruct2[T []int|[]string] struct {
Name string
Data T
}
但是像下面这种情况的时候,使用前一种写法会更好
type WowStruct3[T int | string] struct {
Data []T
MaxValue T
MinValue T
}
函数类型约束
也比较常规,就是使用函数签名约束,更多使用接口约束
func MyFunc[T any](arg T) {
...
}
参考
go基础-泛型的更多相关文章
- Java17-java语法基础——泛型
Java18-java语法基础——泛型 一.泛型概念和作用 1.泛型概念: 泛型是JavaSE1.5的新特性,泛型的本质是参数化类型,也就是说,所操作的数据类型被指定为一个参数.这种参数类型可以用在类 ...
- 一天一个Java基础——泛型
这学期的新课——设计模式,由我仰慕已久的老师传授,可惜思维过快,第一节就被老师挑中上去敲代码,自此在心里烙下了阴影,都是Java基础欠下的债 这学期的新课——算法设计与分析,虽老师不爱与同学互动式的讲 ...
- [C#基础] 泛型
为什么泛型? 在泛型中,最重要的应用便是集合类,因此我们模拟一个简单的集合类 对于上述示例,可以有如下应用 从上可看出,自定义的代码太丑陋了,只能用于string类型. 当然我们可以用object作为 ...
- Java 基础 -- 泛型、集合、IO、反射
package com.java.map.test; import java.util.ArrayList; import java.util.Collection; import java.util ...
- [c#基础]泛型集合的自定义类型排序
引用 最近总有种感觉,自己复习的进度总被项目中的问题给耽搁了,项目中遇到的问题,不总结又不行,只能将复习基础方面的东西放后再放后.一直没研究过太深奥的东西,过去一年一直在基础上打转,写代码,反编译,不 ...
- java基础-泛型3
浏览以下内容前,请点击并阅读 声明 8 类型擦除 为实现泛型,java编译器进行如下操作进行类型擦除: 如果类型参数有限制则替换为限制的类型,如果没有则替换为Object类,变成普通的类,接口和方法. ...
- java基础 泛型
泛型的存在,是为了使用不确定的类型. 为什么有泛型? 1. 为了提高安全 2. 提高代码的重用率 (自动 装箱,拆箱功能) 一切好处看代码: package test1; import java.la ...
- java基础-泛型2
浏览以下内容前,请点击并阅读 声明 6 类型推测 java编译器能够检查所有的方法调用和对应的声明来决定类型的实参,即类型推测,类型的推测算法推测满足所有参数的最具体类型,如下例所示: //泛型方法的 ...
- java基础-泛型1
浏览以下内容前,请点击并阅读 声明 泛型的使用能使类型名称作为类或者接口定义中的参数,就像一般的参数一样,使得定义的类型通用性更强. 泛型的优势: 编译具有严格的类型检查 java编译器对于泛型代码的 ...
- Spring基础—— 泛型依赖注入
一.为了更加快捷的开发,为了更少的配置,特别是针对 Web 环境的开发,从 Spring 4.0 之后,Spring 引入了 泛型依赖注入. 二.泛型依赖注入:子类之间的依赖关系由其父类泛型以及父类之 ...
随机推荐
- 用 Tensorflow.js 做了一个动漫分类的功能(一)
前言: 浏览某乎网站时发现了一个分享各种图片的博主,于是我顺手就保存了一些.但是一张一张的保存实在太麻烦了,于是我就想要某虫的手段来处理.这样保存的确是很快,但是他不识图片内容,最近又看了 mobil ...
- Proxmox VE软件防火墙的配置
1 软件防火墙的基本概念 防火墙是计算机网络中用于保护网络安全的关键技术.防火墙可以是硬件设备部署在网络出口,也可以是软件部署在终端设备出口.本文主要介绍软件防火墙. 软件防火墙可以根据网络流量的方向 ...
- 【go笔记】目录操作
基本目录操作 涉及:创建目录.重命名目录.删除目录 package main import ( "fmt" "os" "time" &quo ...
- pentaho(keetle)数据同步实践
pentaho(keetle)数据同步实践 1 pentaho简介 pentaho可读作"彭塔湖",在keetle被pentaho公司收购后改名而来. pentaho是一款开源ET ...
- 52条SQL语句,性能优化!
52条SQL语句,性能优化! SQL语句性能优化 1, 对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引. 2,应尽量避免在 where 子句中对 ...
- 《Kali渗透基础》13. 无线渗透(三)
@ 目录 1:无线通信过程 1.1:Open 认证 1.2:PSK 认证 1.3:关联请求 2:加密 2.1:Open 无加密网络 2.2:WEP 加密系统 2.3:WPA 安全系统 2.3.1:WP ...
- 了解 HarmonyOS
引言 在开始 HarmonyOS 开发之前,了解其背景.特点和架构是非常重要的.本章将为你提供一个全面的 HarmonyOS 概览. 目录 什么是 HarmonyOS HarmonyOS 的发展历程 ...
- 有Root与无Root安装git-lfs
有Root与无Root安装git-lfs 直接安装 先查看arm还是AMD 例如当前使用Rocky Linux 8.8版本的内核.因此,应该下载适用于Rocky Linux 8.x的Git LFS安装 ...
- KRPANO最新完整汉化中文版 (KRPANO-1.19-PR10-WIN汉化版)
KRPano 最新版本汉化krpano-1.19-pr10-win,由KRPano技术解密群:551278936 提供. 下载地址:http://pan.baidu.com/s/1bBmD5c 如果需 ...
- 小札 Maximum Weight Closure of a Graph
1. Introduction Define a closure of a directed graph \(G=(V,E)\) as an induced set of vertexes of ...