目录

了解Go语言的泛型

Go语言之前一直被吐槽没有泛型,随着1.18版本的发布,Go语言也开始支持泛型了。本文主要简单了解一下什么是泛型,在代码中如何实现以及应用。

泛型的定义

泛型是一种编程语言的特性,允许在编写代码时不指定具体的数据类型,而在运行时动态确定,常用于通用算法或者数据容器存储。

例如有这么一个需求,分别对一组int类型和float类型数组的元素进行求和,通过泛型,我们可以实现只编写一个函数,可同时适用于int类型和float类型数组的求和。这种特性使得我们能够编写更加灵活、通用的代码,而不必为每种数据类型编写相似的逻辑。

在代码中的使用

一般写法

格式

1
2
3
func 函数名[泛型名称 泛型约束](参数列表) 返回值 {
    函数内容
}

例子

1
2
3
func Pri[T any](input T){
	fmt.Println(input)
}

如上,Pri是一个打印传入参数的方法,[T any]表示泛型名称为T,可以是任何(any)类型。

类型约束

以上面的泛型的定义中描述的内容为例子,实现一个适用于int类型和float类型数组求和的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func  Sum[T int|float64] (slice []T) T {
    var sum T
    for _, v := range slice {
        sum += v
    }
    return sum
}

func main() {
    a := []int{1, 2, 3}
    fmt.Println(Sum(a))
    b := []float64{1.2, 2.3, 3.4}
    fmt.Println(Sum(b))
}
//结果
//6
//6。9

其中[T int|float64]表示泛型T可以为int类型或者float64类型,这个是类型约束,表示使用时参数需要在约束条件内。

接口作为泛型约束

除了以上写法之外,可以在结构体中定义对应的类型约束:

1
2
3
4
5
6
7
type Type interface{
	int|string|float64
}

func Pri[T Type](input T){
	fmt.Println(input)
}

[T Type]表示约束为int、string、float64都可,这个适用于已经确定哪一些类型会经常作为泛型约束。

自定义泛型类型

有些时候会针对一些特定类型进行合并,为之后参数扩展做准备,此时可以这样写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type MyInt int
type Type interface{
    ~int|string|float64
}
func Pri[T Type](input T){
    fmt.Println(input)
}
var d MyInt
d = 88
Pri(d)
// 88

type MyInt int表示MyInt是基于int类型定义的,~int表示任何基于int类型定义的参数,前后呼应。注意~只能加在int,string等基本类型前,自定义的类型不适用。

泛型的大致原理

Go的泛型实际上有点类似java的方法重载,在同一个类中,可以存在多个方法名相同,参数类型不同,参数个数不同,或者两者都不同的函数。

编译器在编译泛型函数时只生成一份函数副本,通过新增一个字典参数来供调用方传递类型参数(Type Parameters),这样就可以用一个函数实例支持多种类型参数。 这种实现方式称为字典传递(Dictionary passing)。

Go 实现泛型的方式,就是在编译阶段,通过将类型信息以字典的方式传递给泛型函数。当然这个字典不仅包含了类型信息,还包含了此类型的内存操作函数,如 make/len/new 等。

1.18版本前的泛型实现方式

在Go语言中,1.18前还没有原生支持泛型的语法,不过可以使用一些技巧来实现类似泛型的功能。

  • 使用interface{}作为类型参数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main
 
import (
    "fmt"
)
 
func Sum(a ...interface{}) (sum interface{}) {
    for _, v := range a {
        switch v := v.(type) {
        case int:
            if sum == nil {
                sum = 0
            }
            sum = v + sum.(int)
        case float64:
            if sum == nil {
                sum = 0.0
            }
            sum = v + sum.(float64)
        // 你可以继续添加其他类型的处理
        default:
            panic("unsupported type")
        }
    }
    return
}
 
func main() {
    fmt.Println(Sum(1, 2))
    fmt.Println(Sum(1.0, 2.0))
}
  • 使用反射实现泛型
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main
 
import (
    "fmt"
    "reflect"
)
 
func Sum(a interface{}) (sum interface{}) {
    v := reflect.ValueOf(a)
    if v.Kind() != reflect.Slice {
        panic("should be a slice")
    }
 
    for i := 0; i < v.Len(); i++ {
        if sum == nil {
            sum = 0
        }
        sum = v.Index(i).Interface() + sum
    }
    return
}
 
func main() {
    s := []int{1, 2, 3, 4}
    fmt.Println(Sum(s))
}
  • 使用代码生成工具实现泛型,这个实际上就是多写几个参数不同的方法

泛型与interface的性能对比测试

以使用interface{}实现泛型为例子,那么已经有类似的方法实现泛型的效果了,那么为什么还需要泛型。

个人觉的除了有规范开发格式的效果之外,性能上也存在一定差异,例如interface{}实现泛型效果,因为其中有断言的过程,会额外增加性能上的开销,以下是网上一个测试interface实现泛型和官方提供的泛型函数在性能上的对比,大家可以参考一下: Go:泛型与interface{}的基准测试比较,性能解析

参考文章

Go泛型的理解和使用小结

B站视频:【GO语言】泛型的常用功能介绍与实例示范