• GO泛型相关


    通过引入 类型形参类型实参 这两个概念,我们让一个函数获得了处理多种不同类型数据的能力,这种编程方式被称为 泛型编程。

    2. Go的泛型

    • 类型形参 (Type parameter)
    • 类型实参(Type argument)
    • 类型形参列表( Type parameter list)
    • 类型约束(Type constraint)
    • 实例化(Instantiations)
    • 泛型类型(Generic type)
    • 泛型接收器(Generic receiver)
    • 泛型函数(Generic function)

    基本格式:

    type Slice[T int|float32|float64 ] []T
    • T 就是上面介绍过的类型形参(Type parameter),在定义Slice类型的时候 T 代表的具体类型并不确定,类似一个占位符
    • int|float32|float64 这部分被称为类型约束(Type constraint),中间的 | 的意思是告诉编译器,类型形参 T 只可以接收 int 或 float32 或 float64 这三种类型的实参
    • 中括号里的 T int|float32|float64 这一整串因为定义了所有的类型形参(在这个例子里只有一个类型形参T),所以我们称其为 类型形参列表(type parameter list)
    • 这里新定义的类型名称叫 Slice[T]

    泛型类型不能直接拿来使用,必须传入类型实参(Type argument) 将其确定为具体的类型之后才可使用。而传入类型实参确定具体类型的操作被称为 实例化(Instantiations)

    1. // 声明一个泛型
    2. type slice[T int | float32 | float64] []T
    3. func main() {
    4. // 这里传入了类型实参int,泛型类型Slice[T]被实例化为具体的类型 Slice[int]
    5. var a slice[int] = []int{1, 2, 3}
    6. fmt.Println(a) //[1 2 3]
    7. // 传入类型实参float32, 将泛型类型Slice[T]实例化为具体的类型 Slice[float32]
    8. var b slice[float32] = []float32{1.2, 123.123, 2123.1}
    9. fmt.Println(b) //[1.2 123.123 2123.1]
    10. }

    其他类型的泛型

    1. //泛型map
    2. type myMap[KEP int | string, VAL int | float32] map[KEP]VAL
    3. func main() {
    4. table := myMap[string, int]{
    5. "xiaoming": 190,
    6. "xiaohong": 150,
    7. }
    8. fmt.Printf("%+v", table) //map[xiaohong:150 xiaoming:190]
    9. }
    1. //泛型结构体
    2. type myStruct[T int|string] struct {
    3. name string
    4. data T
    5. }
    1. // 一个泛型接口(关于泛型接口在后半部分会详细讲解)
    2. type IPrintData[T int | float32 | string] interface {
    3. Print(data T)
    4. }
    1. // 一个泛型通道,可用类型实参 int 或 string 实例化
    2. type MyChan[T int | string] chan T

    3 类型形参的互相套用

    1. // 泛型嵌套
    2. type woStruct[T int | string, s []T] struct {
    3. Data s
    4. maxval T
    5. minval T
    6. }
    7. func main() {
    8. var test woStruct[int, []int] = woStruct[int, []int]{
    9. []int{1, 2, 3},
    10. 12,
    11. 31,
    12. }
    13. fmt.Printf("%+v", test) //{Data:[1 2 3] maxval:12 minval:31}
    14. }

    任何泛型类型都必须传入类型实参实例化才可以使用。上面的代码中,我们为T传入了实参 int,然后因为 S 的定义是 []T ,所以 S 的实参自然是 []int

    因为 S 的定义是 []T ,所以 T 一定决定了的话 S 的实参就不能随便乱传了

    几种语法错误

    • 定义泛型类型的时候,基础类型不能只有类型形参,如下:
    • 当类型约束的一些写法会被编译器误认为是表达式时会报错。如下:
    1. go
    2. 复制代码// 错误,类型形参不能单独使用
    3. type CommonType[T int|string|float32] T
    1. go
    2. 复制代码//✗ 错误。T *int会被编译器误认为是表达式 T乘以int,而不是int指针
    3. type NewType[T *int] []T
    4. // 上面代码再编译器眼中:它认为你要定义一个存放切片的数组,数组长度由 T 乘以 int 计算得到
    5. type NewType [T * int][]T
    6. //✗ 错误。和上面一样,这里不光*被会认为是乘号,| 还会被认为是按位或操作
    7. type NewType2[T *int|*float64] []T
    8. //✗ 错误
    9. type NewType2 [T (int)] []T

    为了避免这种误解,解决办法就是给类型约束包上 interface{} 或加上逗号消除歧义(关于接口具体的用法会在后半篇提及)

    1. go
    2. 复制代码type NewType[T interface{*int}] []T
    3. type NewType2[T interface{*int|*float64}] []T
    4. // 如果类型约束中只有一个类型,可以添加个逗号消除歧义
    5. type NewType3[T *int,] []T
    6. //✗ 错误。如果类型约束不止一个类型,加逗号是不行的
    7. type NewType4[T *int|*float32,] []T

    因为上面逗号的用法限制比较大,这里推荐统一用 interface{} 解决问题

    匿名结构体不支持泛型

    4. 泛型receiver

    1. type mySlice[T int | string | float32] []T
    2. //泛型方法
    3. func (m mySlice[T]) sum() T {
    4. var sum T
    5. for _, v := range m {
    6. sum += v
    7. }
    8. return sum
    9. }
    10. func main() {
    11. var t mySlice[int] = []int{1, 2, 3, 4, 5}
    12. fmt.Println(t.sum()) //15
    13. var f mySlice[float32] = []float32{1.2, 3.4, 5.6}
    14. fmt.Println(f.sum()) //10.200001
    15. }
    • 首先看receiver (s MySlice[T]) ,所以我们直接把类型名称 MySlice[T] 写入了receiver中
    • 然后方法的返回参数我们使用了类型形参 T ****(实际上如果有需要的话,方法的接收参数也可以实用类型形参)
    • 在方法的定义中,我们也可以使用类型形参 T (在这个例子里,我们通过 var sum T 定义了一个新的变量 sum )

    动态判断变量的类型

    泛型不像接口一样可以通过类型断言来判断其类型

    但可以通过反射来实现动态判断其类型

    1. func (receiver Queue[T]) Put(value T) {
    2. // Printf() 可输出变量value的类型(底层就是通过反射实现的)
    3. fmt.Printf("%T", value)
    4. // 通过反射可以动态获得变量value的类型从而分情况处理
    5. v := reflect.ValueOf(value)
    6. switch v.Kind() {
    7. case reflect.Int:
    8. // do something
    9. case reflect.String:
    10. // do something
    11. }
    12. // ...
    13. }

    泛型函数

    1. func main() {
    2. //在调用函数的时候声明类型
    3. fmt.Println(add[float64](float64(6.123), float64(8.12312)))
    4. //自动类型推断
    5. fmt.Println(add(19, 123)) //142
    6. }
    7. //定义一个函数泛型
    8. func add[T int | float32 | float64](a T, b T) T {
    9. return a + b
    10. }

    匿名函数不能自己定义类型形参:

    但是匿名函数可以使用别处定义好的类型实参,如:

    1. go
    2. 复制代码func MyFunc[T int | float32 | float64](a, b T) {
    3. // 匿名函数可使用已经定义好的类型形参
    4. fn2 := func(i T, j T) T {
    5. return i*2 - j*2
    6. }
    7. fn2(a, b)
    8. }


     

    既然函数都支持泛型了,那你应该自然会想到,方法支不支持泛型?很不幸,目前Go的方法并不支持泛型

    6. 变得复杂的接口

    1. type IntUintFloat interface {
    2. int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
    3. }
    4. type Slice[T IntUintFloat] []T

    这段代码把类型约束给单独拿出来,写入了接口类型 IntUintFloat 当中。需要指定类型约束的时候直接使用接口 IntUintFloat 即可。

    不过这样的代码依旧不好维

    6.1 ~ : 指定底层类型

    上面定义的 Slie[T] 虽然可以达到目的,但是有一个缺点:

    1. go
    2. 复制代码var s1 Slice[int] // 正确
    3. type MyInt int
    4. var s2 Slice[MyInt] // ✗ 错误。MyInt类型底层类型是int但并不是int类型,不符合 Slice[T] 的类型约束

    这里发生错误的原因是,泛型类型 Slice[T] 允许的是 int 作为类型实参,而不是 MyInt (虽然 MyInt 类型底层类型是 int ,但它依旧不是 int 类型)。

    为了从根本上解决这个问题,Go新增了一个符号 ~ ,在类型约束中使用类似 ~int 这种写法的话,就代表着不光是 int ,所有以 int 为底层类型的类型也都可用于实例化。

    限制:使用 ~ 时有一定的限制:

    1. ~后面的类型不能为接口
    2. ~后面的类型必须为基本类型
    1. type MyInt int
    2. type _ interface {
    3. ~[]byte // 正确
    4. ~MyInt // 错误,~后的类型必须为基本类型
    5. ~error // 错误,~后的类型不能为接口
    6. }

    6.2 从方法集(Method set)到类型集(Type set)

    当满足以下条件时,我们可以说 类型 T 实现了接口 I ( type T implements interface I)

    • T 不是接口时:类型 T 是接口 I 代表的类型集中的一个成员 (T is an element of the type set of I)
    • T 是接口时: T 接口代表的类型集是 I 代表的类型集的子集(Type set of T is a subset of the type set of I)

    6.2.2 类型的并集

    并集我们已经很熟悉了,之前一直使用的 | 符号就是求类型的并集( union )

    6.2.3 类型的交集

    接口可以不止书写一行,如果一个接口有多行类型定义,那么取它们之间的 交集

    1. go
    2. 复制代码type AllInt interface {
    3. ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint32
    4. }
    5. type Uint interface {
    6. ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
    7. }
    8. type A interface { // 接口A代表的类型集是 AllInt 和 Uint 的交集
    9. AllInt
    10. Uint
    11. }
    12. type B interface { // 接口B代表的类型集是 AllInt 和 ~int 的交集
    13. AllInt
    14. ~int
    15. }

    上面这个例子中

    • 接口 A 代表的是 AllInt 与 Uint 的 交集,即 ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
    • 接口 B 代表的则是 AllInt 和 ~int 的交集,即 ~int

    除了上面的交集,下面也是一种交集:

    1. go
    2. 复制代码type C interface {
    3. ~int
    4. int
    5. }

    很显然,~int 和 int 的交集只有int一种类型,所以接口C代表的类型集中只有int一种类型
     

    6.2.4 空集

    当多个类型的交集如下面 Bad 这样为空的时候, Bad 这个接口代表的类型集为一个空集

    1. go
    2. 复制代码type Bad interface {
    3. int
    4. float32
    5. } // 类型 int 和 float32 没有相交的类型,所以接口 Bad 代表的类型集为空


     

    6.2.5 空接口和 any

    上面说了空集,接下来说一个特殊的类型集——空接口 interface{} 。因为,Go1.18开始接口的定义发生了改变,所以 interface{} 的定义也发生了一些变更:

    空接口代表了所有类型的集合

    1. // 空接口代表所有类型的集合。写入类型约束意味着所有类型都可拿来做类型实参
    2. type Slice[T interface{}] []T

    因为空接口是一个包含了所有类型的类型集,所以我们经常会用到它。于是,Go1.18开始提供了一个和空接口 interface{} 等价的新关键词 any ,用来使代码更简单:

    type Slice[T any] []T // 代码等价于 type Slice[T interface{}] []T


     

    6.2.6 comparable(可比较) 和 可排序(ordered)

    Go直接内置了一个叫 comparable 的接口,它代表了所有可用 != 以及 == 对比的类型

    6.3.1 基本接口(Basic interface)

    接口定义中如果只有方法的话,那么这种接口被称为基本接口(Basic interface)。这种接口就是Go1.18之前的接口,用法也基本和Go1.18之前保持一致

    6.3.2 一般接口(General interface)

    如果接口内不光只有方法,还有类型的话,这种接口被称为 一般接口(General interface) ,如下例子都是一般接口:

    1. go
    2. 复制代码type Uint interface { // 接口 Uint 中有类型,所以是一般接口
    3. ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
    4. }
    5. type ReadWriter interface { // ReadWriter 接口既有方法也有类型,所以是一般接口
    6. ~string | ~[]rune
    7. Read(p []byte) (n int, err error)
    8. Write(p []byte) (n int, err error)
    9. }

    一般接口类型不能用来定义变量,只能用于泛型的类型约束中。所以以下的用法是错误的:

    1. go
    2. 复制代码type Uint interface {
    3. ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
    4. }
    5. var uintInf Uint // 错误。Uint是一般接口,只能用于类型约束,不得用于变量定义

    这一限制保证了一般接口的使用被限定在了泛型之中,不会影响到Go1.18之前的代码,同时也极大减少了书写代码时的心智负担


     


     

  • 相关阅读:
    在 Ubuntu 20.04 上,将 Subversion 从默认的 1.13.0 降级到 1.9.7。 解决svn无法保存密码问题
    【wpf】实战 ItemsControl + 用户控件 + 绑定
    CF1877A Goals of Victory
    PID 控制理论
    基于xml的Spring应用(理解spring注入)
    python-jupyter实现OpenAi语音对话聊天
    面向对象编程 上 (1)
    CSS进阶篇——展示 (display)
    SpringCloud - Spring Cloud 之 Config分布式配置;server,client配置(十五)
    SecureRandom那些事|真伪随机数
  • 原文地址:https://blog.csdn.net/weixin_51595939/article/details/136420923