6

Golang 泛型实践

 2 years ago
source link: https://windard.com/blog/2022/05/17/Golang-Generic
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Golang 泛型实践

2022-05-17

泛型是什么

开宗明义,泛型,类型形参和类型实参

在函数定义中,定义要求的参数叫形参,实际传入的参数叫实参,在强类型编程语言中要求形参和实参的类型一致。

这样就会有一个问题,导致我们的函数限制非常强,特别是在 Golang 的数字类型有很多种的情况下。

package main

import "fmt"

func MinInt(a, b int) int {
    if a <= b {
        return a
    } else {
        return b
    }
}

func main() {
    fmt.Println(MinInt(1, 2))
}

像这样比较大小的函数,(a,b) 就是形参,(1,2) 就是实参,我们可能对于不同的类型都要分别再写一个函数。

如果能够不限制形参类型,在函数调用的时候再指定具体类型,这个问题是不是就可以解决了呢?

那么我们就需要引入类型形参 (Type Parameter) 和类型实参 (Type Argument) 的概念。


假设有这样一个函数,就是在函数定义的时候使用类型形参,然后将参数类型也当成参数传入,进行类型的参数化。

在函数调用的时候,不但需要传入参数,还需要传入参数的类型,可以把类型像方法的参数那样传递,指定实际执行的参数类型。

func min[T Numeric](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func main() {
    fmt.Println(min[int](45, 74))
    fmt.Println(min[float64](4.141, 2.01))
}

这样就能让一个函数处理多种不同类型数据,可以在函数调用的时候再明确参数类型,这种方式就被称为泛型编程。

那么其实并不是泛型编程中就不需要关心参数类型,只是将参数类型进行后验确定,在 Golang 编译时还是会根据实际参数类型对函数进行展开编译。

所以在 Go1.18 引入泛型之后,对编译性能会一定的影响,大概会有 18% 的性能下降,但是对运行性能无影响。

引入泛型的好处

  1. 在编译期间对类型进行检查以提高类型安全
  2. 通过指定类型消除强制类型转换
  3. 能够减少代码重复性,提供更通用的功能函数。

但是也泛型也不是银弹,不是所有场景下都需要泛型编程,主要在针对不同类型的数据写完全相同的逻辑代码的情况下,可以考虑使用泛型。

泛型怎么用

泛型就是泛指的类型,即类型参数化,在定义时无需确定参数的类型,可以在调用的时候再指定参数类型。

在这里我们去掉了函数,因为实际泛型的用处不止在函数中,也可以在类型,接口,函数等各种地方。

泛型的应用主要有三种

常用于一些集合类型,比如使用泛型栈的设计,因为 Golang 强类型语言,一般的栈只能是固定类型,泛型栈可以在实例化的再确定具体的元素类型。

比如设计一个支持整形和字符串的列表和字典,这里的泛型占位符 T,K,V 都可以,只需要保持在同一个定义中前后一致就可以。

package main

import "fmt"

type ListType[T int | int32 | int64 | string] []T

type MapType[K int | int32, V int64 | string] map[K]V

func main() {
    var intList ListType[int]
    intList = []int{1, 2, 3}
    fmt.Println(intList)
    strList := ListType[string]{"1", "2", "3"}
    fmt.Println(strList)

    intMap := MapType[int, string]{1: "1", 2: "2"}
    int32Map := MapType[int32, int64]{1: 2, 3: 4}
    fmt.Println(intMap)
    fmt.Println(int32Map)
}

还有一个简单的泛型类的例子

这里的泛型占位符 T 就是类型形参,在 T 的可选类型中,int | int32 就是 T 的类型约束 (Type Constraint)

就是在实际传入类型实参的时候,只能使用类型参数列表中限定的类型,其中 any 表示任意类型,同 interface{}

接口是对类的进一步抽象,将类抽象为接口是一种基本技能,在接口定义时只需要给出函数签名而无需完成其具体逻辑。

Golang 中会自动查找类上定义的方法,如果某个类实现了接口定义的全部函数,即可认为类实现了这个接口。

在面向对象的编程语言中,使用接口的时,在函数定义中无需指定其具体的实现类,只需要传入参数对象实现了接口定义的方法即可。

在函数定义的时候,也可以将接口作为参数类型,这样就可以传入任意一个实现了该接口的对象。

注意 在这里的接口都是指 interface, 而非 interface{}.

因为 interface{} 作为一个空接口在 Golang 中也被认为是基础对象类型,类似于 Java 中的 Object

所以为了避免歧义以及减少书写成本, 在 Go1.18 之后,新增 any 类型代替 interface{} 类型,可以在代码中做全量替换。

可以使用这行代码进行全量替换 gofmt -w -r 'interface{} -> any' ./...

对于一个泛型接口,可以在接口定义的时候声明泛型,不限制其具体的实现类型。

同样的像上面提到的泛型栈,就可以使用泛型接口来进行抽象。

type GenericStackInterface[T any] interface {
    Push(element T)
    Pop() T
}

除了泛型接口,在 Golang 1.18 中 interface 还新增了类型集合的概念,可以在接口定义中添加多种类型。

注意相当于之前的接口是函数集合,可以用来声明一些函数,但是在 Go1.18 中扩充了类型集合,可以在接口中声明一些类型,在同一个接口中,可以同时存在类型集合和函数集合。

type Numeric interface {
    int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}

像这种就是类型集合,以前的接口就是原始的方法集合,也被称为基本接口,基本接口也是一个空的类型集合。

在类型集合中,在同一行内的多种类型,使用 | 相连,表示类型取并集,如果分成多行,则各行之间取交集。

如果交集为空,则为取空集,空集不等于空接口,空接口表示可以使用任意类型,空集表示无法使用任何类型。

在泛型接口定义时,泛型类型集合与接口集合,不能同时使用类型形参和类型集合,在类型集合中也不能使用递归定义。

泛型除了可以在类定义和接口定义中,可以在函数定义中使用,作为入参或出参的类型。

比如在上面泛型栈的设计中,对于入栈和出栈的操作,就已经用到了泛型函数的定义。

再比如,一开始就提到的比较大小的场景,我们可以定义类型形参,允许多种不同类型的整形比较

func minInt[T int | int8 | int16 | int32](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func maxInt[T int | int8 | int16 | int32](a, b T) T {
    if a > b {
        return a
    }
    return b
}

但是这样的话,每个函数中都需要对各种数字类型给排列一遍,我们可以使用类型集合,将所有数字类型先定义出来。

比如优化后的比大小操作

package main

type Numeric interface {
    int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}

func min[T Numeric](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func max[T Numeric](a, b T) T {
    if a > b {
        return a
    }
    return b
}

这里的所有的数字类型,都被包含在 Numric 中,这样就可以在泛型函数中比较大小,这种带类型形参的函数被称为泛型函数。

前面提到泛型函数在调用的时候是需要指定其具体类型的,但是对于某些简单类型也会自动推导,可以省略。

func main() {
    maths.MinInt(1, 2)
    fmt.Println(min(45, 74))
    fmt.Println(min[int](45, 74))
    fmt.Println(min[int32](45, 74))
    fmt.Println(max(4.141, 2.01))
    fmt.Println(max[float64](4.141, 2.01))
    // 编译报错:cannot use 4.141 (untyped float constant) as int64 value in argument to max[int64] (truncated)
    fmt.Println(max[int64](4.141, 2.01))
    // IDE报错:Cannot use string as the type Numeric
    // 编译报错:string does not implement Numeric
    fmt.Println(max[string](4.141, 2.01))
}

但是注意两点

  1. 可以执行类型,如果指定类型和实际传入参数类型不一致,就有编译报错
  2. 不指定类型,编译器会自动识别类型,如果类型不一致,也会有编译报错

因为 Golang 的泛型是在编译时会根据指定的具体类型将泛型确定下来,运行时还是有强类型限制的

对于在通用的数字类型,Golang 1.18 中也内置了 constraints.Ordered 表示所有可供排序的内置类型,所以上面的泛型函数可以改写成这样

package main

import (
    "golang.org/x/exp/constraints"
)

func minType[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func maxType[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

如果进入源代码查看其具体代码的话,会发现在 Ordered 类型中也是有各种数字类型组合起来的。

但是有一点奇怪的地方就是这里的类型集合中,各种类型前加了一个波浪线~, 表示衍生类型,即使用 type 自定义的类型也可以被识别到,只要底层类型一致即可。

比如 ~int 可以包含 inttype MyInt int 等多种类型

// Signed is a constraint that permits any signed integer type.
// If future releases of Go add new predeclared signed integer types,
// this constraint will be modified to include them.
type Signed interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}

// Unsigned is a constraint that permits any unsigned integer type.
// If future releases of Go add new predeclared unsigned integer types,
// this constraint will be modified to include them.
type Unsigned interface {
	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

// Integer is a constraint that permits any integer type.
// If future releases of Go add new predeclared integer types,
// this constraint will be modified to include them.
type Integer interface {
    Signed | Unsigned
}

// Float is a constraint that permits any floating-point type.
// If future releases of Go add new predeclared floating-point types,
// this constraint will be modified to include them.
type Float interface {
    ~float32 | ~float64
}

// Complex is a constraint that permits any complex numeric type.
// If future releases of Go add new predeclared complex numeric types,
// this constraint will be modified to include them.
type Complex interface {
    ~complex64 | ~complex128
}

// Ordered is a constraint that permits any ordered type: any type
// that supports the operators < <= >= >.
// If future releases of Go add new ordered types,
// this constraint will be modified to include them.
type Ordered interface {
    Integer | Float | ~string
}

除了 Ordered 类型之外,还提供了一个内置接口类型, comparable ,这里的可比较指 的是可以用 ==!= 是否相同进行比较,而非可以进行 >< 进行大小比较,但是注意在接口集合中,不能使用 comparable 接口和其他类型的并集。

在泛型的使用时,可以用在类型定义和接口定义中,作为类型约束和类型集合使用,也可以用在通道 (Channel) 中。

但是也有一些不能用的时候,比如

  1. 不能单独定义泛型,
  2. 比如匿名结构体不能使用泛型,
  3. 比如匿名函数不能自己定义类型形参。
  4. 有就是只能在泛型类中使用泛型方法,不能在非泛型类中使用泛型方法。

类型形参只能被定义一次,类型实参也只能被传入确定一次,不能用类型实参确认多次。

Go 1.18 泛型全面讲解:一篇讲清泛型的全部
泛型的作用与定义
泛型是双刃剑?Go1.18 编译会慢近 20%
Go 1.18 正式发布了!支持泛型、性能优化…
浅谈Go1.18中的泛型编程
Golang 泛型浅析
Java 泛型优点之编译时类型检查
为什么要使用泛型?
秒懂Java之泛型
Go语言泛型设计
函数式编程在 Go 泛型下的实用性探索
Go泛型快速入门
Go 泛型简明教程
Go 泛型初步
Go 1.18新特性学习笔记04: Go泛型的基本语法
Go 泛型的这 3 个核心设计,你都知道吗?
Go泛型介绍
Tutorial: Getting started with generics
An Introduction To Generics


本文固定链接:https://windard.com/blog/2022/05/17/Golang-Generic
原创文章,转载请注明出处:Golang 泛型实践 By Windard


Recommend

  • 49
    • www.hoohack.me 5 years ago
    • Cache

    GO语言泛型编程实践

    紧接着上次说到的RDB文件解析功能,数据解析步骤完成后,下一个问题就是如何保存解析出来的数据,Redis有多种数据类型,string、hash、list、zset、set,一开始想到的方案是为每一种数据定义一种数据结构,根据不同的数据类型,将数据保存...

  • 40
    • 微信 mp.weixin.qq.com 5 years ago
    • Cache

    Golang 1.x版本泛型编程

  • 13
    • www.purewhite.io 3 years ago
    • Cache

    Golang 泛型初探

    Golang 泛型初探 发表于 2021-03-09 分类于 go 阅读次数:126 Disqus:0 Comments

  • 9
    • segmentfault.com 3 years ago
    • Cache

    Java泛型最佳实践

    相信大家对Java泛型并不陌生,无论是开源框架还是JDK源码都能看到它,毫不夸张的说,泛型是通用设计上必不可少的元素,所以真正理解与正确使用泛型,是一门必修课,本文将解开大家对泛型的疑惑,并通过大量实践,让你get到泛型正确的使用姿势,下...

  • 6

    Golang引入泛型:Go将Interface{}替换为“Any” 现在 Go 将拥有泛型:Go将Interface{}替换为“Any” ,这是一个类型别名: type any = interfa...

  • 2
    • www.v2ex.com 2 years ago
    • Cache

    Golang 如何使用 struct 泛型?

    V2EX  ›  Go 编程语言 Golang 如何使用 struct 泛型?   blue7wings · 8 小时 51 分...

  • 3
    • l1905.github.io 2 years ago
    • Cache

    golang泛型白话篇

    golang泛型 目前golang中,最新版本1.8版本中新增了方法泛型。泛型的合理使用, 能减少我们写很多类似的代码。 一些成熟的语言, 比如JAVA支持泛型(接口泛型、类泛型、方法泛型), 或者模版, 可以减少写重复的代码,这里做一些...

  • 3
    • juejin.cn 2 years ago
    • Cache

    Golang 泛型初识

    本文主要是介绍Golang泛型的基本要素,泛型的一些通用代码的实践。 分为以下四个部分 泛型的基本元素 泛型是什么? 泛型编程是一种计算机编程风格,编程范式,其中算法...

  • 6

    【1-7 Golang】Go语言快速入门—泛型 tomato01 · 大约23小时之前 · 298 次...

  • 6

    在编程世界中,代码的可重用性和可维护性是至关重要的。为了实现这些目标,Java 5 引入了一种名为泛型(Gener...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK