go基础-泛型

这篇具有很好参考价值的文章主要介绍了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,三个切片的元素类型分别是intfloat64string。仅此而已,再无其他区别。访问切片

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
}

空集合, intfloat32 没有相交的类型,所以接口 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) {
    ...
}

参考文章来源地址https://www.toymoban.com/news/detail-746073.html

  • Go 1.18 泛型全面讲解:一篇讲清泛型的全部

到了这里,关于go基础-泛型的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • GO基础进阶篇 (十三)、泛型

    先看一下这段代码。 上面的代码中,我们想要打印参数arr的信息。运行报错 想要解决的话,按照之前我们的学习,可以将函数改编如下(使用断言) 但这样会有一个坏处,当我们想要打印另一个非string的数组时,就不得不再写一个方法 这样处理,就会导致有无限多相似的代

    2024年01月18日
    浏览(42)
  • Go泛型解密:从基础到实战的全方位解析

    本篇文章深入探讨了Go语言的泛型特性,从其基础概念到高级用法,并通过实战示例展示了其在实际项目中的应用。 关注【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人

    2024年02月08日
    浏览(45)
  • 01 java 学习 数据类型、基础语法、封装、继承、多态、接口、泛型、异常等

    目录 环境搭建和基础知识  什么是JRE: 什么是JDK: 基础数据类型  分支选择if else switch和c一毛一样 for和while循环还有数组基本和c一样 封装 函数调用、传参、命名规范、数组新命名规范 java输入Scanner scanner = new Scanner(System.in); 类的创建和使用以及封装修饰符  构造方法:含义、

    2024年02月11日
    浏览(41)
  • 【Java 基础篇】Java类型通配符:解密泛型的神秘面纱

    在Java中,类型通配符(Type Wildcard)是泛型的重要概念之一。它使得我们能够更加灵活地处理泛型类型,使代码更通用且可复用。本文将深入探讨Java类型通配符的用法、语法和最佳实践。 类型通配符是一个用问号 ? 表示的通配符,它可以用于泛型类、方法和通配符边界。类型

    2024年02月07日
    浏览(51)
  • python的可变类型和不可变类型

    ​ 在id不变的情况下,value可以改变 ​ 在熟知的类型中,整数,浮点数,复数,布尔值,字符串,元组和冻结集合属于不可变类型 ​ 对不可类型的变量重新赋值,实际上是重新创建一个不可变类型的对象,并将原来的变量重新指向新创建的对象 ​ value一旦改变,id也跟着改

    2024年02月13日
    浏览(43)
  • go基础09-Go语言的字符串类型

    字符串类型是现代编程语言中最常使用的数据类型之一。在Go语言的先祖之一C语言当中,字符串类型并没有被显式定义,而是以字符串字面值常量或以’\\0’结尾的字符类型(char)数组来呈现的: 这给C程序员在使用字符串时带来一些问题,诸如: ● 类型安全性差; ● 字符

    2024年02月09日
    浏览(56)
  • 2.go语言基础类型漫游

    目录 本篇前瞻 Leetcode习题9 题目描述 原题解析 代码编写 有符号整形 基本数据类型 整形 有符号整形 无符号整形 浮点型 布尔型 字符 本篇小结 下一篇预告 欢迎来go语言的基础篇,这里会帮你梳理一下go语言的基本类型,注意本篇有参考go圣经,如果你有完整学习的需求可以看

    2024年02月12日
    浏览(34)
  • 2.如何选择go语言基础类型

    目录 本篇前瞻 Leetcode习题9 题目描述 原题解析 代码编写 有符号整形 基本数据类型 整形 有符号整形 无符号整形 浮点型 布尔型 字符 本篇小结 下一篇预告 欢迎来go语言的基础篇,这里会帮你梳理一下go语言的基本类型,注意本篇有参考go圣经,如果你有完整学习的需求可以看

    2024年02月12日
    浏览(44)
  • Go语言基础之基本数据类型

    Go语言中有丰富的数据类型,除了基本的整型、浮点型、布尔型、字符串外,还有数组、切片、结构体、函数、map、通道(channel)等。Go 语言的基本类型和其他语言大同小异。 整型 整型分为以下两个大类: 按长度分为:int8、int16、int32、int64 对应的无符号整型:uint8、uint1

    2024年02月12日
    浏览(41)
  • 【Go 基础篇】Go语言字符类型:解析字符的本质与应用

    字符类型是计算机编程中用于表示文本和字符的数据类型,是构建字符串的基本单位。在Go语言(Golang)中,字符类型具有独特的特点和表示方式,包括Unicode编码、字符字面值以及字符操作。本篇博客将深入探讨Go语言中的字符类型,介绍字符的编码方式、字符字面值的表示

    2024年02月13日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包