注册 登录

清河洛

Go中的泛型

qingheluo2024-08-19清河洛334
本该很早就编写关于Go的泛型相关的文章的,但是在通过网络上的只言片语初窥了Go泛型的使用方法后就直接在后续的开发中使用了泛型,直到在实际开发中不断的遇到问题,然后网络搜索后解决问题,经过几次这样的循环,导致了想系统的学习以下Go的泛型Go在1.18版本中正式支持了泛型,具体的设计思路是经过了一系列的进化和取舍后才有了最终的发布形态,如果想要了解Go的泛型设计进化历程,请查看该篇博客文章,写的很好空接口和泛型的区别 空接口 interface{} 空接口是没有定义任何方法的接口,因此任何类型都默认实现了空接口,可以用来存储任意类型的值 空接口在使用时需要通过类型断言或反射来还原原始类型,这...

本该很早就编写关于Go的泛型相关的文章的,但是在通过网络上的只言片语初窥了Go泛型的使用方法后就直接在后续的开发中使用了泛型,直到在实际开发中不断的遇到问题,然后网络搜索后解决问题,经过几次这样的循环,导致了想系统的学习以下Go的泛型

Go在1.18版本中正式支持了泛型,具体的设计思路是经过了一系列的进化和取舍后才有了最终的发布形态,如果想要了解Go的泛型设计进化历程,请查看该篇博客文章,写的很好

空接口和泛型的区别

空接口 interface{}
空接口是没有定义任何方法的接口,因此任何类型都默认实现了空接口,可以用来存储任意类型的值
空接口在使用时需要通过类型断言或反射来还原原始类型,这会带来运行时开销
空接口更多用于处理不确定类型的数据

泛型 Generics
泛型提供了编写若干特定类型或实现了特定方法的数据的通用函数或数据结构的代码的功能
泛型使得代码更加灵活、可重用,并且可以减少代码重复
泛型代码在编译时进行类型检查,提高了类型安全
泛型代码在编译时生成,针对具体类型进行优化,通常比使用空接口的性能更好
泛型旨在提高代码的重用性和灵活性而并不是要解决类型不确定的情况

泛型参数用于指定函数的参数类型限制和返回值类型限制,或指定一些数据结构中的数据类型限制

也就是说泛型参数主要是用于限制哪些数据类型可以通用该部分代码

只有指定的基础数据类型或实现了特定方法的数据才可以使用该部分代码

Go中的泛型参数使用中括号([])表示,里面是若干个自定义变量以及对每个变量的限定条件

泛型的限定条件

就是限定在使用时的数据类型或数据是否实现了特定方法

限定条件就是一个在接口的基础上增加了对基础数据类型的interface

type constraint interface{
    int | int8 | int16 |
    int32 | int64
}
表示仅接收指定数据类型,此时我们将constraint接口称之为约束条件

Go允许重新自定义类型,如type Myint int,此时虽然Myint的底层类型是int,但Myint没法匹配constraint约束

于是Go又引入了近似约束(Approximation Constraint)的概念,使用波浪线+底层数据类型

只要底层类型符合限定数据类型约束就可以,此时Myint可以匹配int约束

type constraint interface{
    ~int | ~int8 | ~int16 |
    ~int32 | ~int64
}

最后一个限制就是接口本身的功能,就是表示一定要实现某些特定方法的限定条件

type constraint interface{
    ~int | ~int8 | ~int16 |
    ~int32 | ~int64
    ToString() string
}

表示底层类型需要符合,并且必须实现了ToString()方法的数据才可以

限定条件的并集和交集

假定我们已经定义好了constraint1和constraint2两个限定条件

并集
type constraint interface{
    constraint1 | constraint2
}

交集
type constraint interface{
    constraint1
    constraint2
}

在限定条件时,如果不对数据做任何限定,我们可以使用空接口表示任何数据类型,但是空接口编写时字符数较多,于是Go定义了一个any关键字用于表示空接口

我们可以仍然使用空接口的写法,没有任何影响,使用any关键字仅仅是因为编写时代码字数更好,也可以略微提升代码的可读性

type any interface{}

限定条件的简写

Go还允许我们对限定条件在泛型定义时进行一定的简写

func name[T1 interface{~int | ~uint}] (T1){...}

我们可以省略掉这样的interface关键字
func name[T1 ~int | ~uint] (T1){...}

我们还可以直接使用已经定义好的限定条件进行并集
func name[T1 constraint1 | constraint2] (T1){...}

在限定条件相对来讲比较简单时

泛型的定义

泛型函数的泛型参数位于函数名和参数之间

func Func_name [val1 constraint1,val2 constraint2,...] (arg1 val1) reval val2{}

在定义函数func_name时
先定义若干个限定了数据类型或实现方法的变量(val1,val2,...)
之后在函数编写时在限定参数或返回值的类型时使用上面已经定义的变量
通过上述两个步骤将满足一定条件的数据类型在函数中进行了绑定

func Demo [T1 , T2 any] (val T1) T2 {...}

func Demo [T1 []T2, T2 any] (val T2) T1{...} // 在定义限定变量时使用了其他定义的限定变量

泛型结构的泛型参数位于变量名和结构定义之间

var variable [val1 constraint1,var2 constraint2,...] ...

var Demo [T1 , T2 any] map[T1]T2  // 创建一个泛型MAP
var Demo [T1 any] []T1            // 创建一个泛型切片
var Demo [T1 any] make (chan T1)  // 创建一个泛型通道

var Demo [T1 []T2, T2 any] map[T2]T1 // 在定义限定变量时使用了其他定义的限定变量

泛型的使用

在使用已经定义好的限定条件定义了我们需要的函数或数据结构后,就该去使用了,由于泛型并不是要解决数据类型未知的问题,所以泛型在实际使用时仍然是强类型的,必须指定数据类型

指定数据类型时仍然是使用中括号([])来表示

func Demo [T1, T2 any] (val1 T1, val2 T2){...}
Demo[int,string](3,"hello")  // 指定数据类型并传入参数

var Demo [T1 , T2 any] map[T1]T2
var demo Demo[int,string]   // 表示创建一个索引为int类型,值为string的map

泛型推导

为了进一步优化编写的代码量,Go支持在泛型的使用时进行自动类型推导,就好像在定义变量时var num=3时会对num进行类型推导一样

泛型的类型推导是从右向左进行推导的,就是说如果定义了多个限定变量,当我们使用时传入的类型数量少于定义的变量时,会从右向左进行类型推导

func Demo [T1, T2 any] (val1 T1, val2 T2){...}
Demo[int](3,"hello")
    我们仅指定了一个int类型,但是在定义时有两个
    于是会优先从左进行匹配,就是说T1匹配到int
    右侧的T2没有指定类型,就会自动进行类型推导

我们也可以完全省略中括号部分让Go对全部的限行变量根据实际传入的值进行类型推导

常用的限定条件

为方便开发者,Go内置一个constraints包,提供常用的类型约束

type (
    Integer interface{ ~int|~int8|~int16|~int32|~int64 }
        // 有符号整数类型
    Unsigned interface{ ~uint|~uint8|~uint16|~uint32|~uint64 }
        // 无符号整数类型
    Floats interface{ ~float32|~float64 }
        // 浮点数类型
    Complex interface{ ~complex64|~complex128 }
        // 复数类型    
    Number interface{ Integer|Unsigned|Floats|Complex }
        // 数字类型               
    Ordered interface{ Integer|Unsigned|Floats|~string }
        // 支持排序的类型,即支持操作符:< <= >= >
)

另外还有类似的fmt.Stringer等
type Stringer interface {
    String() string
}


网址导航