深入理解Golang的泛型

2023-07-12 17:36:26 浏览数 (1)

TOC

1. 什么是泛型

泛型程序设计(generic programming)是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。 -- 百度百科

2022年3月15日,争议非常大但同时也备受期待的泛型终于伴随着Go1.18发布了。

1.1 举个栗子

假设我们有一个功能函数:

代码语言:go复制
func Add(a int, b int) int {
    return a   b
}

从代码上,可以很容易看出,这是计算两个数相加的函数。通过传入int类型的ab,就可以返回ab相加后的结果。

1.2 问题: 如果a和b是float类型呢?

如果要解决上述问题,通常有两种解决方法:

  1. 增加一个函数 func AddFloat(a float, b float) float
代码语言:go复制
func AddFloat(a, b float32) float32 {
    return a   b
}
  1. 使用反射 func Add(a interface{}, b interface{}) interface
代码语言:go复制
func Add(a interface{}, b interface{}) interface{} {
    switch a.(type) {
    case int:
        return a.(int)   b.(int)
    case float32:
        return a.(float32)   b.(float32)
    default:
        return nil
    }
}

上述两个解决方案的缺点很明显。

方法1:会引入新的函数,如果还有其他类型的a,b需要相加的话,就需要再增加更多的函数。

方法2:使用了反射,性能会有影响

1.3 Golang新特性

如果不想增加一个新的功能逻辑一模一样的函数,同时也不想使用有性能问题的反射的话。就可以使用到泛型的新特性

代码语言:go复制
func Add[T int | float32 | float64](a, b T) T {
    return a   b
}

func main() {
    fmt.Println(Add(1, 2))
    fmt.Println(Add(1.1, 2.1))
}

1.4 ChatGPT4对泛型的回答

在Go语言中,泛型是一种编程特性,允许你编写更通用、可重用的代码。泛型可以让你编写一个函数或类型,而不是针对特定的数据类型。这样,你可以使用相同的函数或类型处理不同的数据类型,而无需为每种数据类型编写重复的代码。

Go 1.18版本引入了泛型特性,主要包括以下几个方面:

  1. 类型参数:类型参数是泛型函数或类型的一个占位符,表示一个未知的类型。类型参数用方括号[]括起来,放在函数名或类型名之后。例如,func MyFunc[T any](a T) {}中的T就是一个类型参数。
  2. 约束:约束是一种限制类型参数的方式,用于指定类型参数必须满足的条件。约束可以是接口类型或其他具有类型参数的类型。例如,func MyFunc[T io.Reader](a T) {}中的io.Reader就是一个约束,表示类型参数T必须实现io.Reader接口。
  3. 预定义约束:Go 1.18提供了一些预定义的约束,用于表示常见的类型集合。例如,any约束表示任何类型,comparable约束表示可比较的类型(支持==!=操作符)。
  4. 泛型函数:泛型函数是一种使用类型参数的函数,可以处理不同类型的参数。泛型函数的定义和普通函数类似,只是在函数名后面添加了类型参数列表。例如,func MyFunc[T any](a, b T) T {}
  5. 泛型类型:泛型类型是一种使用类型参数的类型,可以表示不同类型的数据结构。泛型类型的定义和普通类型类似,只是在类型名后面添加了类型参数列表。例如,type MySlice[T any] []T

2. 泛型的基本特性

2.1 类型参数(Type Parameters)

通用代码是使用开发者称为类型参数的抽象数据类型编写的。调用泛型方法时,类型参数将替换为类型参数。

类型参数列表出现在常规参数之前。为了区分类型参数列表和常规参数列表,类型参数列表使用方括号[]而不是圆括号()。正如常规参数具有类型一样,类型参数也具有元类型,也称为约束。

代码语言:go复制
func Print[T any](s T) {
    fmt.Println(s)
}

调用泛型方法时:

代码语言:go复制
Print(1.2)
Print("123")
Print[[]int]([]int{1, 2, 3})
Print([]int{1, 2, 3})

// 输出结果
// 1.2
// 123
// [1 2 3]
// [1 2 3]

调用泛型函数的时候,可以指定约束调用,也可以直接调用。

2.1 约束(Constraints)

通常,所有泛型代码都希望类型参数满足某些要求。这些要求被称为约束

看这一段代码:

代码语言:go复制
// any并没有约束后续计算的类型
func add[T any](a, b T) T {
    return a   b // 编译错误
}

上述代码中,any约束允许任何类型作为类型参数,并且只允许函数使用任何类型所允许的操作。其接口类型是空接口:interface{}, ab类型都是T,并且Tany类型的。 因此ab是不能直接相加操作的。

因此,需要设置可相加的类型约束。

代码语言:go复制
// T类型的约束被设置成 int | float32 | float64
func Add[T int | float32 | float64](a, b T) T {
    return a   b
}

上述代码中将T的类型约束,设置成为int | float32 | float64, 而这三个类型都是可以相加操作的,因此,编译不会出现错误。

2.2 类型集(Type Sets)

类型集表示一堆类型的集合,用来在泛型函数的声明中约束类型参数的范围。上面示例中的anyinterface{}的别名,表示所有类型的集合,也就是不限制类型。

上述的代码示例中[T int | float32 | float64]只列举了三个类型,如果需要支持更多的类型,就可以使用类型集的特性。

代码语言:go复制
// 定义类型集 
type number interface {
    int | int32 | uint32 | int64 | uint64 | float32 | float64
}

// 约束T可为number类型集中的任一元素
func add[T number](a, b T) T {
    return a   b
}

2.3 约束元素

1. 任意类型约束元素

允许列出任何类型,而不仅仅是接口类型。例:

代码语言:go复制
// 其中 int 为基础类型
type Integer  interface { int } 

2. 近似约束元素

在日常coding中,可能会有很多的类型别名,例如:

代码语言:go复制
type (
    orderStatus   int32
    sendStatus    int32
    receiveStatus int32
    ...
)

Go1.18 中扩展了近似约束元素(Approximation constraint element)这个概念,以上述例子来说,即:基础类型为int32的类型。语法表现为:

代码语言:go复制
type AnyStatus interface{ ~int32 }

如果我们需要对上述自定义的status做一个翻译,就可以使用以下的方式:

代码语言:go复制
// 使用定义的类型集
func translateStatus[T AnyStatus](status T) string {
    switch status {
    case 1:
        return "成功"
    case -1:
        return "失败"
    default:
        return "未知"
    }
}

// 或者不使用类型集
func translateStatus[T ~int32](status T) string {
    switch status {
    case 1:
        return "成功"
    case -1:
        return "失败"
    default:
        return "未知"
    }
}

3. 联合约束元素

联合元素,写成一系列由竖线 ( |) 分隔的约束元素。例如:int | float32~int8 | ~int16 | ~int32 | ~int64。并集元素的类型集是序列中每个元素的类型集的并集。联合中列出的元素必须全部不同。

这里给所有有符号的数字类型添加一个通用的求和方法coding如下:

代码语言:go复制
type Integer interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

func addInteger[T Integer](a, b T) T {
    return a   b
}
代码语言:go复制
fmt.Println(addInteger(1, 2))
fmt.Println(addInteger(-1, -2))

// 执行结果:
// 3
// -3

4. 约束中的可比类型

Go1.18 中内置了一个类型约束 comparable约束,comparable约束的类型集是所有可比较类型的集合。这允许使用该类型参数==!=值。

代码语言:go复制
func inSlice[T comparable](s []T, x T) int {
    for i, v := range s {
        if v == x {
            return i
        }
    }
    return -1
}
代码语言:go复制
fmt.Println(inSlice([]string{"a", "b", "c"}, "c"))
// 执行结果:
// 2

2.4 类型推断

在许多情况下,可以使用类型推断来避免必须显式写出部分或全部类型参数。可以对函数调用使用的参数类型推断从非类型参数的类型中推断出类型参数。开发者可以使用约束类型推断从已知类型参数中推断出未知类型参数。

代码语言:go复制
func Print[T any](s T) {
    fmt.Println(s)
}

s := []int{1, 2, 3}

// 显示指定参数类型
Print[[]int](s)
// 推断参数类型
Print(s)

Tips: 如果在没有指定所有类型参数的情况下使用泛型函数或类型,则如果无法推断出任何未指定的类型参数,则会出现错误。

2.5 类型约束的两种写法

代码语言:go复制
// 推荐
type Student1[T int | string] struct {
    Name string
    Data []T
}

type Student2[T []int | []string] struct {
    Name string
    Data T
}

2.6 匿名函数不支持泛型

在Go中我们经常会使用匿名函数,如:

代码语言:go复制
fn := func(a, b int) int {
    return a   b 
}  // 定义了一个匿名函数并赋值给 fn 

fmt.Println(fn(1, 2)) // 输出: 3

那么Go支不支持匿名泛型函数呢?答案是不能——匿名函数不能自己定义类型形

代码语言:go复制
// 错误,匿名函数不能自己定义类型实参
fn := func[T int | float32](a, b T) T {
    return a   b
} 

fmt.Println(fn(1, 2))

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

代码语言:go复制
func MyFunc[T int | float32 | float64](a, b T) {
    
    // 匿名函数可使用已经定义好的类型形参
    fn2 := func(i T, j T) T {
        return i%j
    }

    fn2(a, b)
}

2.7 不支持泛型方法

目前Go的方法并不支持泛型,例如:

代码语言:go复制
type Person struct{}

// 不支持泛型方法
func (p *Person) Say[T int | string](s T) {
    fmt.Println(s)
}

但是, 我们可以通过定义泛型类型来实现:

代码语言:go复制
type Person[T int | string] struct{}

func (p *Person[T]) Say(s T) {
    fmt.Println(s)
}

执行:

代码语言:go复制
func main() {
    var p1 Person[int]
    p1.Say(1)

    var p2 Person[string]
    p2.Say("hello")
}

// 结果:
// 1
// hello

2.8 泛型类型的嵌套

泛型和普通的类型一样,可以互相嵌套定义出更加复杂的新类型,如下:

代码语言:go复制
// 先定义个泛型类型 Slice[T]
type Slice[T int|string|float32|float64] []T

// ✗ 错误。泛型类型Slice[T]的类型约束中不包含uint, uint8
type UintSlice[T uint|uint8] Slice[T]  

// ✓ 正确。基于泛型类型Slice[T]定义的新泛型类型 IntAndStringSlice[T]
type IntAndStringSlice[T int|string] Slice[T]  
// ✓ 正确 基于IntAndStringSlice[T]套娃定义出的新泛型类型
type IntSlice[T int] IntAndStringSlice[T] 

// 在map中套一个泛型类型Slice[T]
type SMap[T int|string] map[string]Slice[T]
// 在map中套Slice[T]的另一种写法
type SMap2[T Slice[int] | Slice[string]] map[string]T

示例:

代码语言:go复制
// sets 定义泛型集合
type sets[T int | string | float32] []T
type hobby[T string] sets[T]
type score[T int | float32] map[string]sets[T]

// Student 定义学生类
type Student struct {
    Name  string
    Hobby hobby[string]
    Score score[int]
    ExtraScore score[float32]
}

func main() {
    hobbies := sets[string]{"football", "basketball", "golf"}
    mathScore := sets[int]{100, 99, 98}
    englishScore := sets[int]{95, 92, 93}

    s := &Student{
        Name:  "zhangSan",
        Hobby: hobby[string](hobbies),
        Score: score[int]{
            "math":    mathScore,
            "english": englishScore,
        },
        ExtraScore: score[float32]{
            "physical": sets[float32]{9.9, 9.7, 9.4},
        },
    }
    fmt.Println(s)
}

// 结果:
// &{zhangSan [football basketball golf] map[english:[95 92 93] math:[100 99 98]] map[physical:[9.9 9.7 9.4]]}

3. 泛型实践

3.1 实现工具函数

虽然标准库里面已经提供了大量的工具函数,但是这些工具函数都没有使用泛型实现,为了提高使用体验,我们可以使用泛型进行实现。

代码语言:go复制
func MaxInt(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func MaxInt64(a, b int64) int64 {
    if a > b {
        return a
    }
    return b
}

// ...其他类型

使用泛型实现:

代码语言:go复制
func Max[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

其中constraints.Ordered表示可排序类型,也就是可以使用三路运算符的类型[>, =, <],包含了所有数值类型和string。可以通过go get golang.org/x/exp引入。

3.2 实现数据结构

简单的实现一个基于泛型的队列。

代码语言:go复制
// Queue - 队列
type Queue[T any] struct {
    items []T
}

// Put 将数据放入队列尾部
func (q *Queue[T]) Put(value T) {
    q.items = append(q.items, value)
}

// Pop 从队列头部取出并从头部删除对应数据
func (q *Queue[T]) Pop() (T, bool) {
    var value T
    if len(q.items) == 0 {
        return value, true
    }

    value = q.items[0]
    q.items = q.items[1:]
    return value, len(q.items) == 0
}

// Size 队列大小
func (q Queue[T]) Size() int {
    return len(q.items)
}

队列的使用:

代码语言:go复制
type Stu struct {
    Name string
}

func main() {
    var q1 Queue[int]    // 可存放int类型数据的队列
    q1.Put(1)
    q1.Put(2)
    q1.Put(3)
    fmt.Println(q1.Pop())
    fmt.Println(q1.Pop())
    fmt.Println(q1.Pop())

    var q2 Queue[string]    // 可存放string类型数据的队列
    q2.Put("A")
    q2.Put("B")
    q2.Put("C")
    fmt.Println(q2.Pop())
    fmt.Println(q2.Pop())
    fmt.Println(q2.Pop())

    var q3 Queue[Stu]       // 可存放Stu类型数据的队列
    q3.Put(Stu{Name: "zhangSan"})
    q3.Put(Stu{Name: "liSi"})
    q3.Put(Stu{Name: "wangWu"})
    fmt.Println(q3.Pop())
    fmt.Println(q3.Pop())
    fmt.Println(q3.Pop())
}

// 结果:
// 1 false
// 2 false
// 3 true

// A false
// B false
// C true

// {zhangSan} false
// {liSi} false
// {wangWu} true

Queue[T] 因为是泛型类型,所以要使用的话必须实例化

3.3 实现多类型缓存

实现一个Map,可以缓存不同类型的数据

代码语言:go复制
var (
    keyName = "name"
    keyAge  = "age"
    cache   = make(map[string][]any)
)

func TestCache(t *testing.T) {
    cache[keyName] = append(cache[keyName], "zhangSan")
    cache[keyName] = append(cache[keyName], "liSi")
    cache[keyName] = append(cache[keyName], "wangWu")

    cache[keyAge] = append(cache[keyAge], 18)
    cache[keyAge] = append(cache[keyAge], 19)
    cache[keyAge] = append(cache[keyAge], 20)

    fmt.Println(cache)
}

执行结果:

代码语言:go复制
=== RUN   TestCache
map[age:[18 19 20] name:[zhangSan liSi wangWu]]
--- PASS: TestCache (0.00s)
PASS

如果上述示例中,在**keyName**中追加的不是字符串而是数字,是否会报错?

代码语言:txt复制
var (
    keyData = "data"
    cache   = make(map[string][]any)
)

func TestCache(t *testing.T) {
    cache[keyData] = append(cache[keyData], "zhangSan")
    cache[keyData] = append(cache[keyData], 18)
    cache[keyData] = append(cache[keyData], 99.5)
    cache[keyData] = append(cache[keyData], map[string]string{"Country": "China"})

    fmt.Println(cache)
}

执行结果:

代码语言:go复制
=== RUN   TestCache
map[data:[zhangSan 18 99.5 map[Country:China]]]
--- PASS: TestCache (0.00s)
PASS

4. 接口的定义

上面的例子中,我们学习到了一种接口的全新写法,而这种写法在Go1.18之前是不存在的。

在Go1.18之前,Go官方对 接口(interface)的定义是:接口是一个方法集(method set)

An interface type specifies a method set called its interface

而Go1.18开始将接口的定义正式更改为了 类型集(Type set)

An interface type defines a type set

还记得下面这种用接口来简化类型约束的写法吗?

代码语言:go复制
// 定义类型集 
type number interface {
    int | int32 | uint32 | int64 | uint64 | float32 | float64
}

接口类型 number 代表了一个 类型集合, 所有以 intint32等 为底层类型,都在这一类型集之中

4.1 接口实现(implement)定义的变化

当满足以下条件时,我们可以说 类型 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)

4.2 类型的并集和交集

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

代码语言:go复制
// number 是下列基础类型的并集
type number interface {   
    int | int32 | uint32 | int64 | uint64 | float32 | float64
}

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

代码语言:go复制
type Int interface {
    int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64
}

type Uint interface {
    uint | uint8 | uint16 | uint32 | uint64
}

// 接口Status代表 Int和Uint的交集
type Status interface {  
    Int
    Uint
}

4.3 空集

如果定义的多行类型,并没有实际的交集,那么就会产生空集空集可以正常编译,但是没有实际使用意义。

例如:

代码语言:go复制
type Bad interface {
    ~int
    ~float 
} // 类型 ~int 和 ~float 没有相交的类型,所以接口 Bad 代表的类型集为空

4.4 空接口和 any

空接口 interface{},Go1.18开始定义也发生了改变:

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

因此:

代码语言:go复制
// 空接口代表所有类型的集合。写入类型约束意味着所有类型都可拿来做类型实参
type Slice[T interface{}] []T

var s1 Slice[int]               // 正确
var s2 Slice[map[string]string] // 正确
var s3 Slice[chan int]          // 正确
var s4 Slice[interface{}]       // 正确

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

代码语言:go复制
type Slice[T any] []T // 代码等价于 type Slice[T interface{}] []T

实际上 any 的定义就位于Go语言的 builtin.go 文件中:

// any is an alias for interface{} and is equivalent to interface{} in all ways. type any = interface{}

所以从 Go 1.18 开始,所有可以用到空接口的地方其实都可以直接替换为any,如:

代码语言:go复制
var s []any             // 等价于 var s []interface{}
var m map[string]any    // 等价于 var m map[string]interface{}

func MyPrint(value any){
    fmt.Println(value)
}

4.5 接口的两种类型

基本接口(Basic interface)

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

例如:

  • 最常用的,定义接口变量并赋值
代码语言:go复制
// 接口中只有方法,所以是基本接口
type MyError interface { 
    Error() string
}

// 用法和 Go1.18之前保持一致
var err MyError = fmt.Errorf("hello world")
  • 也可用在类型约束
代码语言:go复制
// io.Reader 和 io.Writer 都是基本接口,也可以用在类型约束中
type MySlice[T io.Reader | io.Writer]  []Slice

一般接口(General interface)

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

例如:

代码语言:go复制
// 接口 Uint 中有类型,所以是一般接口
type Uint interface { 
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

// ReadWriter 接口既有方法也有类型,所以是一般接口
type ReadWriter interface {  
    ~string | ~[]rune

    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}

// 错误。Uint是一般接口,只能用于类型约束,不得用于变量定义
var uintInf Uint 

一般接口类型不能用来定义变量,只能用于泛型的类型约束中

如何实现一般接口?

代码语言:go复制
// StringReadWriter 实现了接口 ReadWriter
type StringReadWriter string

func (s StringReadWriter) Read(p []byte) (n int, err error) {
    // ...
}

func (s StringReadWriter) Write(p []byte) (n int, err error) {
    // ...
}

5. 性能对比

5.1 累加性能测试

定义三个函数,分别是:

  • int类型切片元素之和
  • 泛型切片元素之和
  • interface切片元素之和
代码语言:go复制
// addSlice 累加int切片元素
func addSlice(s []int) int {
    var total int
    for i := 0; i < len(s); i   {
        total  = s[i]
    }
    return total
}

// addSliceT 累加泛型切片元素
func addSliceT[T int | float32](s []T) T {
    var total T
    for i := 0; i < len(s); i   {
        total  = s[i]
    }
    return total
}

// addSliceInterface 累加interface切片元素
func addSliceInterface(s []interface{}) interface{} {
    switch s[0].(type) {
    case int:
        var total int
        for i := 0; i < len(s); i   {
            total  = s[i].(int)
        }
        return total
    case float32:
        var total float32
        for i := 0; i < len(s); i   {
            total  = s[i].(float32)
        }
        return total
    default:
        return 0
    }
}

使用Benchmark对性能测试:

代码语言:go复制
var (
    sInt       = []int{1, 3, 5, 7, 9, 11, 13, 15, 17, 19}
    sInterface = []interface{}{1, 3, 5, 7, 9, 11, 13, 15, 17, 19}
)

// 对addSlice性能压测 
func BenchmarkAddInt(b *testing.B) {
    for i := 0; i <= b.N; i   {
        for j := 0; j < 10000; j   {
            addSlice(sInt)
        }
    }
}

// 对addSliceT性能压测
func BenchmarkAddT(b *testing.B) {
    for i := 0; i <= b.N; i   {
        for j := 0; j < 10000; j   {
            addSliceT(sInt)
        }
    }
}

// 对addSliceInterface性能压测
func BenchmarkAddInterface(b *testing.B) {
    for i := 0; i <= b.N; i   {
        for j := 0; j < 10000; j   {
            addSliceInterface(sInterface)
        }
    }
}

第一次压测结果:

代码语言:go复制
goos: darwin
goarch: amd64
pkg: test/utils/fanxing
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkAddInt-12                 30957             38413 ns/op
BenchmarkAddT-12                   30904             38616 ns/op
BenchmarkAddInterface-12           17510             68794 ns/op
PASS
ok      test/utils/fanxing      5.401s

第二次压测结果:

代码语言:go复制
goos: darwin
goarch: amd64
pkg: test/utils/fanxing
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkAddInt-12                 30919             39277 ns/op
BenchmarkAddT-12                   31081             38491 ns/op
BenchmarkAddInterface-12           17378             68681 ns/op
PASS
ok      test/utils/fanxing      5.214s

第三次压测结果:

代码语言:go复制
goos: darwin
goarch: amd64
pkg: test/utils/fanxing
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkAddInt-12                 31316             40461 ns/op
BenchmarkAddT-12                   28424             47638 ns/op
BenchmarkAddInterface-12           16078             68605 ns/op
PASS
ok      test/utils/fanxing      5.422s

对比三次压测结果:单位(ns/op)

函数

第一次

第二次

第三次

AddInt

38413

39277

40461

AddT

38616

38491

47638

AddInterface

68794

68681

68605

从表格中可以很清晰的看出,使用泛型的性能要比使用到反射的性能高很多。

5.2 数据混装性能测试

混装缓存性能测试, 定义两个缓存map

  • 指定map值类型
  • map值类型为泛型
代码语言:go复制
var (
    keyData  = "data"
    cache    = make(map[string][]string)
    mixCache = make(map[string][]any)
)

func BenchmarkCache(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i   {
        for j := 0; j < 10000; j   {
            cache[keyData] = append(cache[keyData], "你好")
        }
    }
}

func BenchmarkMixCache(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i   {
        for j := 0; j < 10000; j   {
            if j%2 == 1 {
                mixCache[keyData] = append(mixCache[keyData], "你好")
            } else {
                mixCache[keyData] = append(mixCache[keyData], 10)
            }
        }
    }
}

第一次压测结果:

代码语言:go复制
goos: darwin
goarch: amd64
pkg: test/utils/fanxing
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkCache-12                   1429            715062 ns/op          842966 B/op          0 allocs/op
BenchmarkMixCache-12                4275            730063 ns/op          902718 B/op          0 allocs/op
PASS
ok      test/utils/fanxing      4.688s

第二次压测结果:

代码语言:go复制
goos: darwin
goarch: amd64
pkg: test/utils/fanxing
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkCache-12                   1723            635823 ns/op          715680 B/op          0 allocs/op
BenchmarkMixCache-12                4257            745344 ns/op          906535 B/op          0 allocs/op
PASS
ok      test/utils/fanxing      5.886s

第三次压测结果:

代码语言:go复制
goos: darwin
goarch: amd64
pkg: test/utils/fanxing
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkCache-12                   1507            713448 ns/op          799336 B/op          0 allocs/op
BenchmarkMixCache-12                3342            771164 ns/op          918445 B/op          0 allocs/op
PASS
ok      test/utils/fanxing      4.136s

对比三次压测结果:单位(ns/op)

函数

第一次

第二次

第三次

Cache

715062

635823

713448

MixCache

730063

745344

771164

从压测结果中可以看出,二者的性能不没有太大的差距

6 常见的错误

6. 1. 网上搜的示例

代码语言:go复制
// 定义类型集
type Addable interface {
    type int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        complex64, complex128
}

6.2 误认为是表达式时会报错

代码语言:go复制
// 错误。T *int会被编译器误认为是表达式 T乘以int,而不是int指针
type NewType[T *int] []T
type NewType [T * int][]T 

// 错误。和上面一样,这里不光*被会认为是乘号,| 还会被认为是按位或操作
type NewType2[T *int|*float64] []T 

为了避免这种误解,解决办法就是给类型约束包上 interface{}

代码语言:go复制
type NewType[T interface{*int}] []T
type NewType2[T interface{*int|*float64}] []T 

// 如果类型约束中只有一个类型,可以添加个逗号消除歧义
type NewType3[T *int,] []T

// 错误。如果类型约束不止一个类型,加逗号是不行的
type NewType4[T *int|*float32,] []T 

总结

  1. Golang泛型主要提供了一种代码抽象、封装的能力,让我们能够写出更能复用的代码,避免代码到处拷贝,从而能够提高代码的可维护性,可读性,还能从避免类型转换中得到一点性能提升。
  2. 让语言本身的易用性大幅的增加。

0 人点赞